{
  "openapi": "3.0.3",
  "info": {
    "title": "FundlyHub API",
    "version": "3.1.2",
    "description": "Complete REST API for FundlyHub — the AI-powered fundraising platform.\n\n## Authentication\nThe web app authenticates with httpOnly session cookies (`access_token`,\n`id_token`, `refresh_token`) that `POST /cognito/signin` and the OAuth\ncallback set. Sign-in does **not** return tokens in its body.\n\nScripts and integrations authenticate with an **API key**: create one\nwith `POST /api-keys` from a signed-in session and send it as\n`Authorization: Bearer fh_live_…`. A Cognito JWT is also accepted in the\nsame header. An API key acts as its owner, with every permission the\nowner holds — treat it like a password. A key expires (at most one year\nafter it is created), cannot create or revoke API keys, and stops working\nwhile its owner's account is not active. Send one credential per\nrequest: either the session cookies or an `Authorization` header.\n\nA missing credential answers `401`, and so does a session token that is\npresent but invalid or expired (`Invalid token`, `code: TOKEN_INVALID`) or\nthat cannot be verified (`code: TOKEN_VERIFICATION_FAILED`): refresh the\nsession and retry. An API key that is unknown, expired or revoked, or whose\nowner's account is not active, answers `401` (`Invalid API key`). `403` is\nreserved for an authenticated caller who is not allowed to do something.\n\nOperations that show no `security` requirement are genuinely public. Where\nan operation lists both `BearerAuth` and `{}`, authentication is optional\nand changes what comes back — an owner sees their own drafts, a signed-in\ndonor gets the gift attributed to their account.\n\nSome endpoints additionally require a **specific permission**; where that\nis the case the permission is named in the operation's description, and a\nsession without it gets `403` with `required_permission` in the body.\nSeveral write endpoints also require a **verified email address**\n(`403` with `code: EMAIL_NOT_VERIFIED`) or sit behind a **feature flag**\n(`403` with `feature_key`). A flag with no stored setting counts as on.\n\n## Response envelopes\nMost read endpoints wrap their payload: a single object comes back as\n`{ \"data\": { … } }`, and a collection as `{ \"data\": [ … ] }`, with\npaginated collections adding `{ \"pagination\": { \"limit\", \"offset\",\n\"total\" } }`. This is a convention rather than a rule — a handful of\nendpoints return a bare object or a bare array, and each operation below\ndocuments the shape it actually returns. Errors are always unwrapped:\n`{ \"error\": \"…\", \"message\": \"…\" }`, sometimes with a machine-readable\n`code` or a `details` array of validation failures.\n\n## Rate limits\nA global limiter of **100 requests/minute per IP** covers the API.\nPublic browsing reads — campaign, organization, category, platform-stats\nand search reads, and the profile reads — are exempt from it, and most\nof them count against a **300/min per IP** bucket instead (an operation\nthat can answer `429` lists it). Other routes add a tighter bucket on\ntop of the global one:\n\n- **Authentication** (sign-up, sign-in, password reset and change, unsubscribe writes, platform tips): 10/min per IP\n- **Verification resends and private-contact changes**: 5/min per user (per IP for a signed-out resend)\n- **Public writes that send mail or store free text** (receipt emails, donor notes, DMCA notices, ambassador applications): 5/min per IP\n- **Financial and endorsement endpoints** (earnings, Stripe Connect, endorsements, receipt resends): 100/min per account\n- **Organization creation**: 5/hour per (IP, user)\n- **Organization member lookups and adds** (`GET /org-admin/{slug}/users/search`, `POST /org-admin/{slug}/members`): 30/min per user, one bucket for both\n- **Campaign media and outcome-report writes**: 30 per 15 minutes per (IP, user)\n- **Referral clicks**: 120/min per caller; **card share images**: 120/min per IP; **campaign presence heartbeats**: 4/min per IP and campaign\n- **AI endpoints**: 10/min per user, applied inside the handler\n- **AI image generation**: 10/hour and 30/day per user\n\nExceeding a limit returns `429` with `RateLimit-Policy`,\n`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and\n`Retry-After` headers. The AI endpoints' in-handler limit answers a bare\n`429` without them.\n",
    "contact": {
      "name": "FundlyHub Developer Support",
      "url": "https://fundlyhub.org"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.staging.fundlyhub.org/api/v1",
      "description": "Staging"
    },
    {
      "url": "https://api.fundlyhub.org/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "User registration, login, and session management via AWS Cognito"
    },
    {
      "name": "Fundraisers",
      "description": "Create and manage fundraising campaigns"
    },
    {
      "name": "Donations",
      "description": "Process and track donations"
    },
    {
      "name": "Categories",
      "description": "Browse fundraiser categories"
    },
    {
      "name": "Organizations",
      "description": "Manage nonprofit organizations"
    },
    {
      "name": "Users",
      "description": "User profiles and preferences"
    },
    {
      "name": "Search",
      "description": "Full-text search and autocomplete"
    },
    {
      "name": "Payouts",
      "description": "Earnings and payout management"
    },
    {
      "name": "Milestones",
      "description": "Campaign milestones and progress tracking"
    },
    {
      "name": "Payments",
      "description": "Stripe payment processing"
    },
    {
      "name": "App Attest",
      "description": "Apple App Attest for the FundlyHub iOS app. A request carrying a valid assertion may skip reCAPTCHA on `POST /payments/create-intent` and `POST /donations`, guest or signed in. See the Native Clients guide for the byte-level protocol."
    },
    {
      "name": "AI",
      "description": "AI-powered content tools"
    },
    {
      "name": "RBAC",
      "description": "The signed-in caller's own roles and permissions"
    },
    {
      "name": "Platform",
      "description": "Platform stats and health"
    },
    {
      "name": "Donors",
      "description": "A donor's own giving history, summary, and annual statements"
    },
    {
      "name": "Notifications",
      "description": "In-app notifications for the authenticated user. Which events also generate email is governed by the toggles on `/users/{id}/preferences`, not by anything under `/notifications`."
    },
    {
      "name": "Social",
      "description": "Following users and organizations"
    },
    {
      "name": "Comments",
      "description": "Fundraiser comments and replies"
    },
    {
      "name": "Updates",
      "description": "Project updates, milestones, and funding stats"
    },
    {
      "name": "Shares",
      "description": "Social-share event tracking"
    },
    {
      "name": "Organization Admin",
      "description": "The org-scoped admin panel at `/org-admin/{slug}/*`. Every operation needs a bearer session, resolves `{slug}` to an organization (an unknown or malformed slug answers `404 Organization not found`, never `403`), and then checks an **org-scoped permission** held through the caller's `org_owner`, `org_admin` or `org_viewer` role on that organization. A caller without the permission gets `403` with `message: \"Requires permission: <name>\"`.\n\nRole → permission map: `org_viewer` holds `org.read`, `org.read_donations`, `org.read_payouts` and `org.read_donors`. `org_admin` adds `org.update_settings`, `org.manage_members`, `org.manage_dbas`, `org.manage_locations` and `org.upload_documents`. `org_owner` adds `org.manage_children` and `org.manage_payouts`."
    },
    {
      "name": "Translations",
      "description": "The owner-side Translations tab: a campaign's, its milestones' and its updates' rows in the other supported locales (`en`, `ru`, `uk`, `es`), hand-edited or regenerated. The public read paths already serve the translated text; these endpoints are for authors."
    },
    {
      "name": "Endorsements",
      "description": "Ambassadors publicly vouching for other people's campaigns, and creators asking ambassadors to promote theirs."
    },
    {
      "name": "Media",
      "description": "A campaign's photo and video gallery. Behind the `features.fundraiser_video` flag."
    },
    {
      "name": "Images",
      "description": "Stock-photo search, AI cover generation and server-side image copying for the campaign builder, plus an allowlisted image proxy."
    },
    {
      "name": "Storage",
      "description": "Direct uploads of campaign images to the platform's CDN bucket."
    },
    {
      "name": "Achievements",
      "description": "The public badge catalogue, single badges, the \"just earned\" feed, and individual earned cards with their share pictures and verify QR codes."
    },
    {
      "name": "API Keys",
      "description": "Long-lived `fh_live_…` keys for CLI and agent access. A key is sent as `Authorization: Bearer fh_live_…` and authenticates as the user who created it, on every endpoint that accepts a bearer session."
    },
    {
      "name": "Creator Subscriptions",
      "description": "Creator monetisation tiers (each mirrored to a Stripe Product and Prices) and the fan-side recurring subscriptions to them."
    },
    {
      "name": "Ambassadors",
      "description": "The ambassador programme: the public directory, invitations and applications, referral links and click tracking, and the ambassador portal under `/me/referrals/*`."
    },
    {
      "name": "Email Preferences",
      "description": "Public, token-authorised unsubscribe and resubscribe for any address FundlyHub mails, including guest donors with no account."
    },
    {
      "name": "Meta",
      "description": "Crawler-facing discovery files (`llms.txt`, sitemaps), generated from live data and proxied by the frontend at the site's public paths."
    },
    {
      "name": "DMCA",
      "description": "DMCA §512 takedown notices and counter-notices. Currently dark behind the `features.dmca_workflow` flag."
    },
    {
      "name": "Support",
      "description": "Live-chat (Chatwoot) identity for the signed-in user."
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "In the browser, authentication rides on the httpOnly session cookies set by `/cognito/signin` or the Google / Apple sign-in at `/cognito/oauth/{provider}`. For scripts and for Swagger UI testing, paste an API key (`fh_live_…`, created with `POST /api-keys`); a Cognito JWT is accepted too. Sign-in does not return a token in its body."
      }
    },
    "schemas": {
      "Fundraiser": {
        "type": "object",
        "description": "A campaign, as the detail reads (`GET /fundraisers/{id}`, `/fundraisers/slug/{slug}`) return it. The listing (`GET /fundraisers`) returns a shorter `FundraiserCard` instead. Fields marked *slug read only* are absent from `GET /fundraisers/{id}`. The campaign's owner also receives the private fields they entered (such as `beneficiary_contact`) and their `trustStatus`; nobody else does.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "example": "Help Support Local Food Bank"
          },
          "slug": {
            "type": "string",
            "example": "help-support-local-food-bank"
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "Short plain-text blurb for cards and listings."
          },
          "story_html": {
            "type": "string",
            "nullable": true,
            "description": "The campaign body, sanitised HTML."
          },
          "goal_amount_cents": {
            "type": "integer",
            "description": "Fundraising goal in CENTS. `5000000` is $50,000.00. This was published as `goal` in dollars before the API unified on a single money unit; a client that still reads `goal` will now read `undefined` rather than a figure a hundred times too small.",
            "example": 5000000
          },
          "total_raised_cents": {
            "type": "integer",
            "description": "Amount raised so far, in CENTS. `1250000` is $12,500.00. Published as `raised` in dollars before the cents cutover. *Slug read only.*",
            "example": 1250000
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending",
              "active",
              "paused",
              "ended",
              "rejected"
            ],
            "description": "`pending` is awaiting review, `ended` is closed (shown as \"Closed\" in the app), `rejected` is set only by FundlyHub review.",
            "example": "active"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "personal",
              "for_others",
              "charity"
            ]
          },
          "is_project": {
            "type": "boolean"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "The category's id (as a string) or slug."
          },
          "category_name": {
            "type": "string",
            "nullable": true,
            "description": "*Slug read only.*"
          },
          "tags": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "cover_image_focal_x": {
            "type": "number",
            "nullable": true
          },
          "cover_image_focal_y": {
            "type": "number",
            "nullable": true
          },
          "images": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "video_url": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true,
            "example": "Austin, TX"
          },
          "beneficiary_name": {
            "type": "string",
            "nullable": true
          },
          "beneficiary_contact": {
            "type": "string",
            "nullable": true,
            "description": "How to reach the beneficiary. Returned only to the campaign's owner; absent for every other caller."
          },
          "owner_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "org_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "profiles": {
            "type": "object",
            "nullable": true,
            "description": "The owner. *Slug read only.*",
            "properties": {
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "email_verified": {
                "type": "boolean"
              },
              "profile_slug": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "donor_count": {
            "type": "integer",
            "description": "Unique donors. On the slug read this is the live count; on `GET /fundraisers/{id}` it is a stored counter."
          },
          "donation_count": {
            "type": "integer",
            "description": "*Slug read only.*"
          },
          "percentage_funded": {
            "type": "number",
            "description": "*Slug read only.*"
          },
          "shares_count": {
            "type": "integer",
            "description": "*Slug read only.*"
          },
          "source_language": {
            "type": "string",
            "description": "The language the campaign was written in (`en`, `ru`, `uk`, `es`)."
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "goal_reached_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cover_tint": {
            "type": "string",
            "nullable": true,
            "pattern": "^#[0-9A-F]{6}$",
            "description": "The campaign page's background colour: a very light tint taken from the cover photo's most characteristic hue, as an upper-case `#RRGGBB` (a black-and-white cover gives `#F9F9FA`). Computed by FundlyHub shortly after the cover is set or changed, so it is `null` until then (and when there is no cover); fall back to your own plain background while it is `null`. Every value is light enough for dark text.",
            "example": "#F2FAFC"
          },
          "donations_last_24h": {
            "type": "integer",
            "minimum": 0,
            "description": "Completed donations made to this campaign in the last 24 hours. Pending, failed and refunded donations are not counted. Up to 30 seconds behind.",
            "example": 12
          },
          "trustStatus": {
            "type": "object",
            "description": "Detail reads only, and only when the caller owns the campaign: the review verdict and what the owner should do about it.",
            "properties": {
              "aiDecision": {
                "type": "string"
              },
              "trustScore": {
                "type": "integer"
              },
              "payoutHold": {
                "type": "boolean"
              },
              "rejectionReasons": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "userMessages": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FundraiserCard": {
        "type": "object",
        "description": "One item of the public listing (`GET /fundraisers`): the campaign's public fields, with the owner, the category name and the raised figures joined in. It carries no story body (`story_html`) and none of the owner-only fields; read `GET /fundraisers/slug/{slug}` for the whole campaign.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "example": "Help Support Local Food Bank"
          },
          "slug": {
            "type": "string",
            "example": "help-support-local-food-bank"
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "goal_amount_cents": {
            "type": "integer",
            "description": "Fundraising goal in CENTS.",
            "example": 5000000
          },
          "total_raised_cents": {
            "type": "integer",
            "description": "Amount raised so far, in CENTS.",
            "example": 1250000
          },
          "donor_count": {
            "type": "integer",
            "description": "Unique donors."
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "ended"
            ],
            "description": "The listing only ever contains published campaigns."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "personal",
              "for_others",
              "charity"
            ]
          },
          "is_project": {
            "type": "boolean"
          },
          "category": {
            "type": "string",
            "nullable": true,
            "description": "The category's id (as a string) or slug."
          },
          "category_name": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "cover_image_focal_x": {
            "type": "number",
            "nullable": true
          },
          "cover_image_focal_y": {
            "type": "number",
            "nullable": true
          },
          "images": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "gallery": {
            "type": "array",
            "maxItems": 6,
            "description": "The campaign's photos for a listing tile, at most six: the cover first, then its other public images in gallery order, each URL once. Images only — no videos, no photos taken down, no uploads still processing. Empty when the campaign has no photo. Present on `GET /fundraisers` items, and on the campaign cards of `GET /users/{id}/campaigns`, `GET /users/{id}/donation-activity` and `GET /fundraisers/{id}/related`; treat a missing field as the cover alone.",
            "items": {
              "type": "string"
            },
            "example": [
              "https://cdn.fundlyhub.org/uploads/fundraisers/cover.jpg",
              "https://cdn.fundlyhub.org/uploads/fundraisers/second.jpg"
            ]
          },
          "video_url": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "The location as the creator typed it (free text)."
          },
          "location_city": {
            "type": "string",
            "nullable": true,
            "description": "The city `location` is in, from a server-side geocode (English name). `null` until the location is geocoded, or when it is not a place or names something larger than a city (a state, a country).",
            "example": "Sacramento"
          },
          "location_region": {
            "type": "string",
            "nullable": true,
            "description": "State or province: its ISO 3166-2 letter code where there is one (`CA`), otherwise its name.",
            "example": "CA"
          },
          "location_country": {
            "type": "string",
            "nullable": true,
            "description": "ISO 3166-1 alpha-2 country code.",
            "example": "US"
          },
          "location_lat": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Latitude of the CITY (its centroid), never of a street address, to at most 2 decimal places (about 1 km). `null` when there is no city-level point.",
            "example": 38.58
          },
          "location_lng": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Longitude of the city, as `location_lat`.",
            "example": -121.49
          },
          "distance_mi": {
            "type": "number",
            "format": "double",
            "description": "Miles from the `near` point to the campaign's city, 1 decimal. Present only on `GET /fundraisers?near=…` items.",
            "example": 3.2
          },
          "beneficiary_name": {
            "type": "string",
            "nullable": true
          },
          "owner_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "org_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "profiles": {
            "type": "object",
            "nullable": true,
            "description": "The owner, as shown on a card.",
            "properties": {
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "email_verified": {
                "type": "boolean"
              },
              "profile_slug": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "source_language": {
            "type": "string",
            "description": "The language the campaign was written in (`en`, `ru`, `uk`, `es`)."
          },
          "translation_source": {
            "type": "string",
            "enum": [
              "source",
              "machine",
              "human"
            ],
            "description": "Present when the card was localised: whether `title` and `summary` are the original text, a machine translation or a human one."
          },
          "original_title": {
            "type": "string",
            "nullable": true,
            "description": "The untranslated title, when `title` is a translation."
          },
          "original_summary": {
            "type": "string",
            "nullable": true,
            "description": "The untranslated summary, when `summary` is a translation."
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FundraiserCreate": {
        "description": "Body for `POST /fundraisers`.\n\nThree of the four fields this previously documented as REQUIRED —\n`description`, `goal`, `category_id` — are not fields the API has.\nA request built from the old shape failed validation outright.\nThe real required set is `title`, `slug` and `goal_amount_cents`.\n\n`owner_user_id` is set from the access token; it is stripped from the\nbody if sent, so a caller cannot create a campaign owned by someone\nelse.\n",
        "type": "object",
        "required": [
          "title",
          "slug",
          "goal_amount_cents"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200,
            "example": "Help Support Local Food Bank"
          },
          "slug": {
            "type": "string",
            "minLength": 3,
            "maxLength": 100,
            "pattern": "^[a-z0-9-]+$",
            "description": "Lowercase letters, digits and hyphens. Check availability first with `GET /fundraisers/check-slug/{slug}`.",
            "example": "help-support-local-food-bank"
          },
          "goal_amount_cents": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999999999999,
            "description": "Fundraising goal in CENTS. `5000000` is $50,000.00. Renamed from `goal_amount`, which was in dollars until the API unified on a single money unit — the rename is deliberate, so a client still sending `goal_amount` gets a validation error rather than a campaign with a goal a hundred times too small.",
            "example": 5000000
          },
          "summary": {
            "type": "string",
            "maxLength": 500,
            "description": "Short plain-text blurb for cards and listings."
          },
          "story_html": {
            "type": "string",
            "description": "The campaign body. Sanitised server-side."
          },
          "currency": {
            "type": "string",
            "default": "USD"
          },
          "category": {
            "type": "string",
            "description": "Category slug. Note this is a string, not the category's id."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string",
            "format": "uri"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "video_url": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "pending",
              "paused",
              "ended"
            ],
            "default": "draft",
            "description": "`rejected` cannot be set here — a campaign cannot self-reject. A campaign may also be moved to `pending` by review regardless of what is requested. Only an explicit `draft` skips the publish gate, and only an explicit `draft` is accepted from a caller whose email is not verified."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ],
            "default": "public"
          },
          "type": {
            "type": "string",
            "enum": [
              "personal",
              "for_others",
              "charity"
            ],
            "default": "personal"
          },
          "is_project": {
            "type": "boolean",
            "default": false
          },
          "beneficiary_name": {
            "type": "string"
          },
          "beneficiary_contact": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "description": "A real calendar date (`YYYY-MM-DD`). Empty string or null means no deadline; anything unparseable is a `400`."
          },
          "org_id": {
            "type": "string",
            "format": "uuid",
            "description": "Create on behalf of an organization the caller belongs to."
          },
          "cover_image_focal_x": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Manual focal-point override for the cover crop. Computed from the image when omitted."
          },
          "cover_image_focal_y": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "scrape_token": {
            "type": "string",
            "description": "The one-time token `POST /ai/scrape-campaign-url` issues with an imported campaign. Send it back unchanged when creating that campaign. Not stored."
          }
        }
      },
      "FundraiserUpdate": {
        "description": "Body for `PATCH /fundraisers/{id}`. Every field of `FundraiserCreate`\nexcept `slug` (which cannot change after creation) and\n`owner_user_id` (stripped if sent), all optional. Only the fields sent\nare written.\n\nThe previously documented `description` field does not exist — the\ncampaign text is `summary` and `story_html` — and `completed` and\n`archived` were never statuses.\n",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200
          },
          "summary": {
            "type": "string",
            "maxLength": 500
          },
          "story_html": {
            "type": "string"
          },
          "goal_amount_cents": {
            "type": "integer",
            "minimum": 1,
            "description": "Fundraising goal in CENTS — same unit and same name as `FundraiserCreate.goal_amount_cents`. `5000000` is $50,000.00. Documented as `goal` in dollars before the cents cutover; the update handler reads `goal_amount_cents` and ignores `goal`.",
            "example": 5000000
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "pending",
              "paused",
              "ended"
            ],
            "description": "`active` publishes (running the publish gate), `draft` unpublishes and is refused with `400` once the campaign has collected money."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "personal",
              "for_others",
              "charity"
            ]
          },
          "is_project": {
            "type": "boolean"
          },
          "currency": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string"
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "video_url": {
            "type": "string"
          },
          "beneficiary_name": {
            "type": "string"
          },
          "beneficiary_contact": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "end_date": {
            "type": "string",
            "format": "date"
          },
          "org_id": {
            "type": "string",
            "format": "uuid"
          },
          "cover_image_focal_x": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "cover_image_focal_y": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "Donation": {
        "type": "object",
        "description": "A donation record.\n**Every money field is an integer number of CENTS** — the same unit Stripe uses, and the unit the whole API now speaks. `5000` is $50.00.\nEndpoints return different projections: the public donor wall (`GET /fundraisers/{fundraiserId}/donations`) omits every donor identifier except the display name and avatar, returns **net** amounts (gross minus the Stripe fee) in `amount_cents`, and strips the name and avatar entirely from anonymous gifts.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "donor_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Derived from the session; never accepted from the client."
          },
          "donor_name": {
            "type": "string",
            "nullable": true
          },
          "donor_avatar": {
            "type": "string",
            "nullable": true,
            "description": "Donor-wall projection only, resolved from the donor's profile."
          },
          "amount_cents": {
            "type": "integer",
            "example": 5000,
            "description": "Gross donation amount in cents ($50.00 here); net of the Stripe fee on the donor-wall projection. Renamed from `amount`, which was in dollars until the API moved to a single money unit."
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "tip_amount_cents": {
            "type": "integer",
            "example": 500,
            "description": "Optional tip to FundlyHub, in cents. Renamed from `tip_amount` (dollars)."
          },
          "fee_amount_cents": {
            "type": "integer",
            "example": 175,
            "description": "Stripe processing fee in cents. Renamed from `fee_amount` (dollars)."
          },
          "net_amount_cents": {
            "type": "integer",
            "example": 4825,
            "description": "`amount_cents - fee_amount_cents`, in cents. Computed by the database, never accepted from a client."
          },
          "payment_status": {
            "type": "string",
            "enum": [
              "paid",
              "refunded",
              "failed",
              "pending"
            ],
            "description": "The gift's state. Written by the Stripe-verified webhook and `POST /payments/confirm` only — clients cannot set it."
          },
          "payment_provider": {
            "type": "string",
            "nullable": true
          },
          "payment_intent_id": {
            "type": "string",
            "nullable": true
          },
          "receipt_id": {
            "type": "string",
            "nullable": true
          },
          "comment": {
            "type": "string",
            "nullable": true,
            "description": "The donor's public message."
          },
          "is_anonymous": {
            "type": "boolean"
          },
          "anonymous_key": {
            "type": "string",
            "nullable": true,
            "description": "Opaque key for the anonymous identity behind this gift — the same value on every gift the same anonymous donor made, so a client can show them as one person (one generated name, one avatar colour) without learning who they are. It is a keyed hash, not an id, and cannot be turned back into a person. Null on a gift that shows a name."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DonationCreate": {
        "description": "Body for `POST /donations`.\n\n**Amounts are integer CENTS.** They were dollars on this endpoint until\nthe API unified on one money unit; the fields were renamed\n`amount` → `amount_cents` and `tip_amount` → `tip_amount_cents` so that\nan un-updated client fails loudly on the missing required field rather\nthan recording a donation a hundred times too small.\n\nBoth fields must be whole numbers. `50.00` is rejected: under the old\ncontract it meant fifty dollars, and there is no reading of it that is\na valid number of cents.\n\nThe donor is taken from the access token, not the body —\n`donor_user_id` is ignored if sent.\n",
        "type": "object",
        "required": [
          "fundraiser_id",
          "amount_cents"
        ],
        "properties": {
          "fundraiser_id": {
            "type": "string",
            "format": "uuid"
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 1,
            "description": "Donation amount in cents. `5000` is $50.00.",
            "example": 5000
          },
          "currency": {
            "type": "string",
            "default": "USD"
          },
          "tip_amount_cents": {
            "type": "integer",
            "minimum": 0,
            "default": 0,
            "description": "Optional tip to FundlyHub, in cents.",
            "example": 500
          },
          "comment": {
            "type": "string",
            "maxLength": 1000,
            "description": "The donor's public message. Documented as `message` until this was corrected — the handler only ever read `comment`, so anything sent as `message` was silently dropped and the donation was recorded without it."
          },
          "donor_name": {
            "type": "string"
          },
          "donor_email": {
            "type": "string",
            "format": "email"
          },
          "is_anonymous": {
            "type": "boolean",
            "default": false
          },
          "payment_provider": {
            "type": "string"
          },
          "payment_intent_id": {
            "type": "string"
          }
        }
      },
      "Category": {
        "type": "object",
        "description": "A fundraiser category. Counts and totals are not part of this object — they come from `/categories/stats` and `/categories/{id}/stats`.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 3
          },
          "name": {
            "type": "string",
            "example": "Medical"
          },
          "slug": {
            "type": "string",
            "example": "medical"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "icon": {
            "type": "string",
            "nullable": true
          },
          "color": {
            "type": "string",
            "nullable": true
          },
          "display_order": {
            "type": "integer"
          }
        }
      },
      "Organization": {
        "type": "object",
        "description": "An organization's public fields, as `GET /organizations` and `GET /organizations/{id}` return them. There is no `name`, `verified` or `owner_id`: the name is `legal_name` (with an optional `dba_name`), verification is `verification_status`, and ownership is an `org_owner` role assignment rather than a field.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "legal_name": {
            "type": "string",
            "example": "Local Food Bank Foundation"
          },
          "dba_name": {
            "type": "string",
            "nullable": true
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "mission": {
            "type": "string",
            "nullable": true
          },
          "website": {
            "type": "string",
            "nullable": true
          },
          "logo": {
            "type": "string",
            "nullable": true
          },
          "banner_image": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "categories": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "verification_status": {
            "type": "string",
            "description": "`pending`, `approved`, `verified`, `rejected` or `suspended`; public reads only ever see `approved` and `verified`."
          },
          "kind": {
            "type": "string",
            "enum": [
              "company",
              "conglomerate"
            ]
          },
          "parent_organization_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrganizationCreate": {
        "type": "object",
        "description": "Body for `POST /organizations`.",
        "required": [
          "legal_name"
        ],
        "properties": {
          "legal_name": {
            "type": "string"
          },
          "ein": {
            "type": "string",
            "example": "12-3456789"
          },
          "country": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "kind": {
            "type": "string",
            "enum": [
              "company",
              "conglomerate"
            ]
          },
          "parent_organization_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "UserProfile": {
        "type": "object",
        "description": "A public profile, as `GET /users/{id}` returns it. The picture is `avatar` (not `avatar_url`), and `email` is present only on the caller's own profile.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "The name the person gave, or `null` when they have not set one. Never derived from their email address."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "avatar": {
            "type": "string",
            "nullable": true
          },
          "bio": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "website": {
            "type": "string",
            "nullable": true
          },
          "profile_slug": {
            "type": "string"
          },
          "profile_visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "description": "One row of a search response.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Entity id; `guest:<key>` or `anon:<key>` for a donor row."
          },
          "type": {
            "type": "string",
            "enum": [
              "campaign",
              "user",
              "organization",
              "donor"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Null only on an anonymous donor row: that donor's display name is a localized alias the client derives from `anonymousKey`, so the row and the donor page never disagree about what to call somebody."
          },
          "subtitle": {
            "type": "string",
            "nullable": true
          },
          "link": {
            "type": "string",
            "description": "Relative path to the matched entity."
          },
          "image": {
            "type": "string",
            "nullable": true
          },
          "anonymousKey": {
            "type": "string",
            "nullable": true,
            "description": "Opaque anonymous identity key. Anonymous donor rows only."
          },
          "donorKind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "guest",
              "anon"
            ],
            "description": "Which donor scope the row came from. Donor rows only."
          },
          "given": {
            "type": "integer",
            "description": "Donor rows only. Minor units this identity gave — donation plus tip, over every settled gift of the identity, which is the same figure the leaderboard row and `/donors/{kind}/{key}` report. Sent as a number rather than as a rendered string so the client formats it in the reader's own language and in the currency below."
          },
          "gifts": {
            "type": "integer",
            "description": "Donor rows only. Settled gifts behind `given`."
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "ISO 4217 for `given`, from the identity's most recent gift. Donor rows only; null when the rows carried none. Minor units are the currency's own, so JPY 5000 is ¥5,000 and not ¥50.00."
          },
          "handle": {
            "type": "string",
            "description": "User rows only. The profile's handle (`profile_slug`), so the row can show what an `@handle` query matched."
          },
          "isPrivate": {
            "type": "boolean",
            "description": "User rows only. A private profile is findable by name, but its row carries no bio and was not matched on one."
          },
          "highlights": {
            "type": "object",
            "additionalProperties": true,
            "description": "Extra display fields — e.g. an organization row's `website`."
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchResult"
            }
          },
          "total": {
            "type": "integer",
            "description": "Total matching campaigns. Users and organizations are capped at `limit` and not counted here."
          },
          "executionTimeMs": {
            "type": "integer"
          },
          "cached": {
            "type": "boolean"
          }
        }
      },
      "Milestone": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "target_amount_cents": {
            "type": "integer",
            "nullable": true,
            "description": "Milestone target in cents. Renamed from `target_amount` (dollars)."
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set when the milestone is marked complete; null while outstanding."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayoutSchedule": {
        "type": "object",
        "properties": {
          "interval": {
            "type": "string",
            "enum": [
              "manual",
              "daily",
              "weekly",
              "monthly"
            ]
          },
          "weeklyPayoutDays": {
            "type": "array",
            "nullable": true,
            "description": "Single-element list holding Stripe's weekly anchor, or null.",
            "items": {
              "type": "string"
            }
          },
          "monthlyPayoutDays": {
            "type": "array",
            "nullable": true,
            "description": "Single-element list holding Stripe's monthly anchor (1-31), or null.",
            "items": {
              "type": "integer"
            }
          },
          "delayDays": {
            "type": "integer",
            "description": "Stripe's payout delay in days. Defaults to 2 when the account does not report one."
          }
        }
      },
      "Notification": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "description": "Event key, e.g. `donation_received`."
          },
          "category": {
            "type": "string",
            "enum": [
              "campaign",
              "donation",
              "social",
              "security",
              "payouts"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "title": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "May contain inline HTML links (donor names, campaign links)."
          },
          "icon": {
            "type": "string"
          },
          "action_url": {
            "type": "string",
            "nullable": true
          },
          "action_label": {
            "type": "string",
            "nullable": true
          },
          "is_read": {
            "type": "boolean"
          },
          "is_archived": {
            "type": "boolean"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignUpdateFeedItem": {
        "type": "object",
        "description": "A campaign update in the signed-in person's feed. The same shape as an item of `GET /projects/{fundraiserId}/updates` (the `project_updates` row with `type: update` and `author`), plus `fundraiser`, `is_read` and `read_at`. With `lang`, the translation fields that endpoint adds (`translation_source`, `original_title`, `original_body`) appear too.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "update"
            ]
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid"
          },
          "author_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active"
            ]
          },
          "source_language": {
            "type": "string",
            "nullable": true,
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "text_edited_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "avatar": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "fundraiser": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "title": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "cover_image": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "is_read": {
            "type": "boolean",
            "description": "True when the person marked it read, or when it was posted before they followed the campaign's creator or organization or first gave to it — older updates are history and never count as unread."
          },
          "read_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When it was first marked read; null when unread or read only by the starting-point rule."
          }
        }
      },
      "LikeState": {
        "type": "object",
        "description": "The caller's like on one comment or update, and that target's count, after the write (#1833).",
        "required": [
          "liked",
          "like_count"
        ],
        "properties": {
          "liked": {
            "type": "boolean",
            "description": "Whether the caller now likes the target."
          },
          "like_count": {
            "type": "integer",
            "minimum": 0,
            "description": "How many accounts like the target, this caller included."
          }
        }
      },
      "Gif": {
        "type": "object",
        "description": "A GIPHY GIF on a comment (#1963). Built by the server from GIPHY's own response; a client never supplies a URL. Every URL is `https://` on a `giphy.com` subdomain with no query string. Full size is GIPHY's `images.original`, the still is `images.original_still`, and the preview is `images.fixed_width` (200 px wide). Render `title` as text.",
        "required": [
          "id",
          "title",
          "width",
          "height",
          "mp4_url",
          "webp_url",
          "gif_url",
          "still_url",
          "preview_mp4_url",
          "preview_webp_url",
          "preview_width",
          "preview_height"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9]{6,40}$",
            "example": "xT4uQulxzV39haRFjG"
          },
          "title": {
            "type": "string",
            "description": "Plain text, possibly empty. Use as alt text."
          },
          "width": {
            "type": "integer",
            "minimum": 1,
            "example": 480
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "example": 270
          },
          "mp4_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.mp4"
          },
          "webp_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.webp"
          },
          "gif_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy.gif"
          },
          "still_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/giphy_s.gif"
          },
          "preview_mp4_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/200w.mp4"
          },
          "preview_webp_url": {
            "type": "string",
            "format": "uri",
            "example": "https://media2.giphy.com/media/xT4uQulxzV39haRFjG/200w.webp"
          },
          "preview_width": {
            "type": "integer",
            "minimum": 1,
            "example": 200
          },
          "preview_height": {
            "type": "integer",
            "minimum": 1,
            "example": 113
          }
        }
      },
      "GifPage": {
        "type": "object",
        "required": [
          "data",
          "next_offset"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Gif"
            }
          },
          "next_offset": {
            "type": "integer",
            "nullable": true,
            "description": "The `offset` for the next page, or null when there is none."
          }
        }
      },
      "AccountDeletionSchedule": {
        "type": "object",
        "required": [
          "scheduled_for",
          "requested_at"
        ],
        "properties": {
          "scheduled_for": {
            "type": "string",
            "format": "date-time",
            "description": "When the account will be deleted (30 days after the request), if nothing is owed to the person by then; otherwise once it has been paid out."
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "statusCode": {
            "type": "integer"
          }
        }
      },
      "OrgRoleTitle": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "executive_director"
          },
          "label": {
            "type": "string",
            "example": "Executive Director"
          },
          "category": {
            "type": "string",
            "enum": [
              "governance",
              "executive",
              "programs",
              "fundraising",
              "operations",
              "community"
            ]
          },
          "sort_order": {
            "type": "integer"
          }
        }
      },
      "OrgAdminPagination": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "OrgAdminFundraiser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "org_id": {
            "type": "string",
            "format": "uuid"
          },
          "owner_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "goal_amount_cents": {
            "type": "string",
            "description": "Goal in integer CENTS, serialized as a JSON string (a 64-bit integer).",
            "example": "500000"
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending",
              "rejected",
              "active",
              "paused",
              "ended"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ]
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "end_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "A calendar date, serialized as midnight server time."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminDonation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiser_title": {
            "type": "string"
          },
          "fundraiser_slug": {
            "type": "string",
            "nullable": true
          },
          "amount_cents": {
            "type": "integer",
            "description": "Gift amount in integer CENTS."
          },
          "currency": {
            "type": "string"
          },
          "tip_amount_cents": {
            "type": "integer",
            "nullable": true
          },
          "fee_amount_cents": {
            "type": "integer",
            "nullable": true
          },
          "payment_status": {
            "type": "string",
            "enum": [
              "paid",
              "pending",
              "failed",
              "refunded"
            ]
          },
          "is_anonymous": {
            "type": "boolean"
          },
          "comment": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "donor_display_name": {
            "type": "string",
            "description": "`Anonymous donor` for anonymous gifts or when no name is known."
          }
        }
      },
      "OrgAdminDonor": {
        "type": "object",
        "properties": {
          "donor_key": {
            "type": "string",
            "description": "Opaque, stable key for the donor: `anonymous`, `user:<profile id>` or `guest:<24 hex characters>`. Use it to tell rows apart, not to identify or contact a donor.",
            "example": "user:3f1c2a9e-5b7d-4e1a-9c2b-8d6f0a1b2c3d"
          },
          "donor_display_name": {
            "type": "string"
          },
          "donation_count": {
            "type": "integer"
          },
          "total_amount_cents": {
            "type": "string",
            "description": "Lifetime paid total at this organization in integer CENTS, serialized as a JSON string (a bigint sum).",
            "example": "125000"
          },
          "currency": {
            "type": "string"
          },
          "last_donated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminUserSearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "user"
            ]
          },
          "name": {
            "type": "string",
            "description": "The user's display name, or `FundlyHub user` when none is set."
          },
          "avatar": {
            "type": "string",
            "nullable": true,
            "description": "Avatar URL, or `null`."
          },
          "masked_email": {
            "type": "string",
            "description": "The account's email with the local part masked to its first character, e.g. `j***@example.org`, so the caller can confirm the match without the address being echoed back.",
            "example": "j***@example.org"
          }
        }
      },
      "OrgAdminMember": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "user_name": {
            "type": "string",
            "nullable": true
          },
          "user_email": {
            "type": "string",
            "nullable": true
          },
          "user_avatar": {
            "type": "string",
            "nullable": true
          },
          "role_name": {
            "type": "string",
            "enum": [
              "org_owner",
              "org_admin",
              "org_viewer"
            ]
          },
          "role_display_name": {
            "type": "string",
            "nullable": true
          },
          "hierarchy_level": {
            "type": "integer",
            "description": "45 owner, 35 admin, 25 viewer."
          },
          "assigned_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminSettings": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "legal_name": {
            "type": "string"
          },
          "website": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "logo": {
            "type": "string",
            "nullable": true
          },
          "banner_image": {
            "type": "string",
            "nullable": true
          },
          "mission": {
            "type": "string",
            "nullable": true
          },
          "social_links": {
            "type": "object",
            "description": "Platform → URL; keys are among twitter, linkedin, facebook, instagram, youtube, tiktok.",
            "additionalProperties": {
              "type": "string"
            }
          },
          "founded_year": {
            "type": "integer",
            "nullable": true
          },
          "verification_completed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "contact_email": {
            "type": "string",
            "nullable": true
          },
          "contact_email_public": {
            "type": "boolean"
          },
          "slug": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "company",
              "conglomerate"
            ]
          },
          "verification_status": {
            "type": "string"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminImageUpload": {
        "type": "object",
        "required": [
          "fileBase64",
          "contentType"
        ],
        "properties": {
          "fileBase64": {
            "type": "string",
            "format": "byte",
            "description": "The image, base64-encoded (no `data:` prefix)."
          },
          "contentType": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp"
            ]
          }
        }
      },
      "OrgAdminUpdate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "org_id": {
            "type": "string",
            "format": "uuid"
          },
          "author_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminDba": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "dba_name": {
            "type": "string"
          },
          "is_default": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminAddress": {
        "type": "object",
        "description": "Free-form address object of up to 16 string fields, each at most 512 characters. Field names are not fixed (e.g. `line1`, `city`, `region`, `postal_code`, `country`); null and blank values are dropped.",
        "maxProperties": 16,
        "additionalProperties": {
          "type": "string",
          "maxLength": 512
        },
        "example": {
          "line1": "100 Main St",
          "city": "Sacramento",
          "region": "CA",
          "postal_code": "95814",
          "country": "US"
        }
      },
      "OrgAdminLocation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "$ref": "#/components/schemas/OrgAdminAddress"
          },
          "is_primary": {
            "type": "boolean"
          },
          "is_publicly_visible": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminPayoutsStatus": {
        "type": "object",
        "properties": {
          "connected": {
            "type": "boolean"
          },
          "accountId": {
            "type": "string"
          },
          "chargesEnabled": {
            "type": "boolean"
          },
          "payoutsEnabled": {
            "type": "boolean"
          },
          "detailsSubmitted": {
            "type": "boolean"
          },
          "defaultCurrency": {
            "type": "string",
            "example": "usd"
          },
          "country": {
            "type": "string",
            "example": "US"
          },
          "businessType": {
            "type": "string",
            "example": "company"
          },
          "requirementsCurrentlyDue": {
            "type": "array",
            "description": "Stripe requirement keys still outstanding.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OrgAdminChildOrganization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "legal_name": {
            "type": "string"
          },
          "dba_name": {
            "type": "string",
            "nullable": true
          },
          "slug": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "example": "company"
          },
          "verification_status": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "website": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminCreatedChild": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "legal_name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "company"
            ]
          },
          "parent_organization_id": {
            "type": "string",
            "format": "uuid"
          },
          "verification_status": {
            "type": "string",
            "example": "pending"
          },
          "dbas": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "dba_name": {
                  "type": "string"
                },
                "is_default": {
                  "type": "boolean"
                }
              }
            }
          },
          "locations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "label": {
                  "type": "string",
                  "nullable": true
                },
                "address": {
                  "$ref": "#/components/schemas/OrgAdminAddress"
                },
                "is_primary": {
                  "type": "boolean"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrgAdminDocument": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "doc_type": {
            "type": "string",
            "enum": [
              "ein_letter",
              "501c3_determination",
              "w9",
              "voided_check",
              "other"
            ]
          },
          "original_filename": {
            "type": "string",
            "nullable": true
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "verification_status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected"
            ]
          },
          "rejection_reason": {
            "type": "string",
            "nullable": true
          },
          "reviewed_by_user_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "uploaded_by_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "is_publicly_visible": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TranslationFundraiserRow": {
        "type": "object",
        "description": "A campaign's row in one locale. All fields null and `source` null when not translated yet.",
        "properties": {
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "story_html": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "machine"
            ],
            "nullable": true
          },
          "translated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TranslationFundraiserSavedRow": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "fundraiser_id": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          {
            "$ref": "#/components/schemas/TranslationFundraiserRow"
          }
        ]
      },
      "TranslationMilestoneRow": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "machine"
            ],
            "nullable": true
          },
          "translated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TranslationMilestoneEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "translations": {
            "type": "array",
            "description": "One entry per locale other than the campaign's source language.",
            "items": {
              "$ref": "#/components/schemas/TranslationMilestoneRow"
            }
          },
          "original_row": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TranslationMilestoneRow"
              }
            ],
            "nullable": true,
            "description": "The milestone's row in the campaign's own language, if any."
          }
        }
      },
      "TranslationProjectUpdateRow": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "content": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "machine"
            ],
            "nullable": true
          },
          "translated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "outdated": {
            "type": "boolean",
            "description": "A hand translation saved before the update's text was last edited."
          }
        }
      },
      "TranslationProjectUpdateEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "source": {
            "type": "object",
            "properties": {
              "language": {
                "type": "string",
                "enum": [
                  "en",
                  "ru",
                  "uk",
                  "es"
                ]
              },
              "title": {
                "type": "string",
                "nullable": true
              },
              "content": {
                "type": "string"
              },
              "edited_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When an edit last changed the text; null if never."
              }
            }
          },
          "translations": {
            "type": "array",
            "description": "One entry per locale other than the update's own.",
            "items": {
              "$ref": "#/components/schemas/TranslationProjectUpdateRow"
            }
          }
        }
      },
      "TranslationUpdateSavedRow": {
        "type": "object",
        "properties": {
          "update_id": {
            "type": "string",
            "format": "uuid"
          },
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "content": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "machine"
            ]
          },
          "translated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TranslationLegacyUpdateRow": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ru",
              "uk",
              "es"
            ]
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "content": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "enum": [
              "human",
              "machine"
            ],
            "nullable": true
          },
          "translated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "TranslationMtError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Why translation is unavailable."
          }
        }
      },
      "ShareChannelStat": {
        "type": "object",
        "properties": {
          "clicks": {
            "type": "integer",
            "description": "Human visits through a shared link."
          },
          "gifts": {
            "type": "integer",
            "description": "Paid, non-self-referred gifts those visits led to."
          }
        }
      },
      "FundraiserTrustAssessment": {
        "type": "object",
        "properties": {
          "publishable": {
            "type": "boolean"
          },
          "profileBlockers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "campaignBlockers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "suggestions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "severity": {
                  "type": "string",
                  "enum": [
                    "warning",
                    "tip"
                  ]
                },
                "message": {
                  "type": "string"
                },
                "actionUrl": {
                  "type": "string"
                }
              }
            }
          },
          "trustScore": {
            "type": "number"
          },
          "aiAnalysis": {
            "type": "object",
            "nullable": true,
            "properties": {
              "storyScore": {
                "type": "number"
              },
              "titleSuggestion": {
                "type": "string"
              },
              "storySuggestions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "redFlags": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "overallVerdict": {
                "type": "string",
                "enum": [
                  "approve",
                  "needs_work",
                  "reject"
                ]
              },
              "categorySpecificIssues": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "verifiableDetails": {
                "type": "boolean"
              },
              "contradictions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "aiDecision": {
            "type": "string",
            "enum": [
              "auto_approve",
              "pending_review",
              "reject"
            ]
          },
          "rejectionReasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "aiAnalysisError": {
            "type": "string",
            "nullable": true,
            "enum": [
              "api_key_missing",
              "timeout",
              "rate_limited",
              "parse_error",
              "unknown"
            ]
          },
          "payoutHold": {
            "type": "boolean",
            "description": "Whether payouts would be held pending Stripe verification."
          },
          "userMessages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "FundraiserActivityEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "action": {
            "type": "string",
            "enum": [
              "created",
              "submitted_for_review",
              "approved",
              "rejected",
              "updated",
              "update_posted",
              "goal_reached",
              "deleted"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "nullable": true
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "role": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "system"
                ],
                "nullable": true
              }
            }
          },
          "details": {
            "type": "object",
            "description": "Present on some actions: `title` and `goalAmount` (created), `goalAmountCents` and `totalRaisedCents` (goal_reached), `reasons` (rejected).",
            "additionalProperties": true
          }
        }
      },
      "DonorFace": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "avatar": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "RelatedCampaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "goal_amount_cents": {
            "type": "number"
          },
          "raised_amount_cents": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "cover_image_focal_x": {
            "type": "number",
            "nullable": true
          },
          "cover_image_focal_y": {
            "type": "number",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "category_name": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "donor_count": {
            "type": "integer"
          },
          "days_left": {
            "type": "integer",
            "nullable": true
          },
          "is_project": {
            "type": "boolean"
          },
          "trust_score": {
            "type": "number",
            "nullable": true
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "organizer_name": {
            "type": "string",
            "nullable": true
          },
          "organizer_avatar": {
            "type": "string",
            "nullable": true
          },
          "gallery": {
            "type": "array",
            "maxItems": 6,
            "description": "The campaign card's photos, at most six: the cover first, then its other public images in gallery order, each URL once. Images only — no videos, no photos taken down, no uploads still processing. Empty when the campaign has no photo. The same list and rule as `gallery` on `GET /fundraisers` items.",
            "items": {
              "type": "string"
            }
          },
          "translation_source": {
            "type": "string",
            "enum": [
              "source",
              "machine",
              "human"
            ],
            "description": "Present when a reader language was resolved."
          },
          "original_title": {
            "type": "string",
            "nullable": true
          },
          "original_summary": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "OutcomeReportAttachment": {
        "type": "object",
        "required": [
          "kind",
          "url"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "image"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "OutcomeReport": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiserId": {
            "type": "string",
            "format": "uuid"
          },
          "authorId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string",
            "description": "Sanitised rich text (HTML)."
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OutcomeReportAttachment"
            }
          },
          "invoiceCount": {
            "type": "integer"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "hiddenAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CampaignEndorser": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar": {
            "type": "string",
            "nullable": true
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "description": "Profile slug."
          },
          "impressions": {
            "type": "integer",
            "description": "Human visits through this person's link."
          }
        }
      },
      "EndorsementResult": {
        "type": "object",
        "properties": {
          "endorsement": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "fundraiserId": {
                "type": "string",
                "format": "uuid"
              },
              "note": {
                "type": "string",
                "nullable": true
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "created": {
            "type": "boolean"
          }
        }
      },
      "EndorsementError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "not_found",
              "not_endorsable",
              "self_endorsement"
            ]
          }
        }
      },
      "EndorsementRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiserId": {
            "type": "string",
            "format": "uuid"
          },
          "ambassadorUserId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "failed"
            ]
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time"
          },
          "notifiedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "StockPhoto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "urls": {
            "type": "object",
            "properties": {
              "thumb": {
                "type": "string"
              },
              "small": {
                "type": "string"
              },
              "regular": {
                "type": "string"
              },
              "full": {
                "type": "string"
              }
            }
          },
          "alt": {
            "type": "string"
          },
          "photographer": {
            "type": "string"
          },
          "photographerUrl": {
            "type": "string"
          },
          "downloadLocation": {
            "type": "string",
            "description": "Pass to `POST /images/track-download` when the photo is chosen."
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          }
        }
      },
      "StorageBucket": {
        "type": "string",
        "enum": [
          "fundraiser-images",
          "fundraiser-gallery",
          "fundraiser-drafts",
          "creator-tiers"
        ]
      },
      "FundraiserMedia": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "video_link",
              "video_upload"
            ]
          },
          "url": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "nullable": true,
            "description": "Video provider, `cloudflare_stream` for uploads, or `unknown`."
          },
          "provider_video_id": {
            "type": "string",
            "nullable": true
          },
          "embed_url": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "aspect_ratio": {
            "type": "string",
            "nullable": true
          },
          "width": {
            "type": "integer",
            "nullable": true
          },
          "height": {
            "type": "integer",
            "nullable": true
          },
          "duration_seconds": {
            "type": "number",
            "nullable": true
          },
          "file_size_bytes": {
            "type": "integer",
            "nullable": true
          },
          "playback_status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "errored",
              "taken_down"
            ]
          },
          "moderation_status": {
            "type": "string",
            "enum": [
              "ok",
              "dmca_pending",
              "dmca_taken_down"
            ]
          },
          "storage_path": {
            "type": "string",
            "nullable": true
          },
          "position": {
            "type": "integer"
          },
          "is_cover": {
            "type": "boolean"
          },
          "created_by": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FundraiserMediaError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "NOT_FOUND",
              "FORBIDDEN",
              "CAP_EXCEEDED",
              "STREAM_NOT_CONFIGURED",
              "INVALID_URL",
              "INVALID_IMAGE_URL",
              "UNSUPPORTED_PROVIDER",
              "UPSTREAM_FAILED"
            ]
          }
        }
      },
      "AchievementError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "e.g. `NOT_FOUND`, `INVALID_INPUT`, `WITHDRAWN`, `RENDER_BUSY`."
          },
          "data": {
            "type": "object",
            "description": "On `WITHDRAWN`, the card's `code`.",
            "properties": {
              "code": {
                "type": "string"
              }
            }
          }
        }
      },
      "AchievementVocabulary": {
        "type": "object",
        "additionalProperties": true,
        "description": "Admin-authored labels and colours, keyed by family (for example `tier` and `track`), each mapping a key to its label and `style` colours. Dynamic."
      },
      "AchievementArt": {
        "type": "object",
        "additionalProperties": true,
        "description": "How to draw the badge art.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "sheet",
              "none"
            ]
          }
        }
      },
      "AchievementLabel": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "AchievementView": {
        "type": "object",
        "additionalProperties": true,
        "description": "One badge as a reader sees it. The main fields are listed; nested rarity, supply, series and progress objects are dynamic. Viewer fields (`earned`, `tier`, `progress`, …) are false or null for an anonymous reader.",
        "properties": {
          "slug": {
            "type": "string"
          },
          "track": {
            "type": "string"
          },
          "loop": {
            "type": "string"
          },
          "badge_art_url": {
            "type": "string",
            "nullable": true
          },
          "background_color": {
            "type": "string",
            "nullable": true
          },
          "art": {
            "$ref": "#/components/schemas/AchievementArt"
          },
          "status": {
            "type": "string"
          },
          "hidden_until_earned": {
            "type": "boolean"
          },
          "visibility": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "tagline": {
            "type": "string",
            "nullable": true
          },
          "persona_name": {
            "type": "string",
            "nullable": true
          },
          "persona_line": {
            "type": "string",
            "nullable": true
          },
          "how_to_earn": {
            "type": "string",
            "nullable": true
          },
          "what_counts": {
            "type": "string",
            "nullable": true
          },
          "story": {
            "type": "string",
            "nullable": true
          },
          "full_rules": {
            "type": "string",
            "nullable": true
          },
          "tiers": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "rarity": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "rarity_share_percent": {
            "type": "number",
            "nullable": true
          },
          "rarity_holders": {
            "type": "integer",
            "nullable": true
          },
          "rarity_population": {
            "type": "integer",
            "nullable": true
          },
          "rarity_computed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "series_number": {
            "type": "integer",
            "nullable": true
          },
          "issued": {
            "type": "integer",
            "nullable": true
          },
          "supply": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "earned": {
            "type": "boolean"
          },
          "holder": {
            "type": "object",
            "nullable": true,
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "user",
                  "organization"
                ]
              },
              "id": {
                "type": "string",
                "format": "uuid"
              }
            }
          },
          "tier": {
            "type": "string",
            "nullable": true
          },
          "awarded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "tier_awarded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "high_water_tier": {
            "type": "string",
            "nullable": true
          },
          "stats": {
            "type": "object",
            "nullable": true,
            "properties": {
              "value": {
                "type": "number"
              }
            }
          },
          "progress": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          },
          "visibility_override": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AchievementFeedRow": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "The card's short code (`/cards/{code}`)."
          },
          "serial": {
            "type": "integer"
          },
          "event": {
            "type": "string",
            "enum": [
              "issued",
              "upgraded"
            ]
          },
          "tier": {
            "$ref": "#/components/schemas/AchievementLabel"
          },
          "species": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "art": {
                "$ref": "#/components/schemas/AchievementArt"
              },
              "background_color": {
                "type": "string",
                "nullable": true
              },
              "track": {
                "$ref": "#/components/schemas/AchievementLabel"
              }
            }
          },
          "frame_rarity": {
            "$ref": "#/components/schemas/AchievementLabel"
          },
          "holder": {
            "type": "object",
            "properties": {
              "short_name": {
                "type": "string",
                "nullable": true
              },
              "initials": {
                "type": "string",
                "nullable": true
              },
              "profile_path": {
                "type": "string",
                "nullable": true
              },
              "source": {
                "type": "string",
                "enum": [
                  "name",
                  "handle",
                  "none"
                ]
              }
            }
          },
          "cause": {
            "type": "object",
            "nullable": true,
            "properties": {
              "title": {
                "type": "string"
              },
              "path": {
                "type": "string"
              }
            }
          },
          "cause_label": {
            "type": "string",
            "nullable": true
          },
          "issued_bucket": {
            "type": "object",
            "description": "When it was issued, rounded to the minute or the hour.",
            "properties": {
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "precision": {
                "type": "string",
                "enum": [
                  "minute",
                  "hour"
                ]
              }
            }
          }
        }
      },
      "AchievementEarnedCard": {
        "type": "object",
        "additionalProperties": true,
        "description": "One earned card. The main fields are listed; `evidence`, `stamp`, `ladder`, `rarity_line` and `cta` are dynamic objects.",
        "properties": {
          "code": {
            "type": "string"
          },
          "serial": {
            "type": "integer"
          },
          "issued": {
            "type": "integer"
          },
          "language": {
            "type": "string"
          },
          "species": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "tagline": {
                "type": "string",
                "nullable": true
              },
              "art": {
                "$ref": "#/components/schemas/AchievementArt"
              },
              "background_color": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "holder": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "user",
                  "organization"
                ]
              },
              "first_name": {
                "type": "string",
                "nullable": true
              },
              "short_name": {
                "type": "string",
                "nullable": true
              },
              "initials": {
                "type": "string",
                "nullable": true
              },
              "profile_path": {
                "type": "string",
                "nullable": true
              },
              "source": {
                "type": "string",
                "enum": [
                  "name",
                  "handle",
                  "none"
                ]
              }
            }
          },
          "tier": {
            "$ref": "#/components/schemas/AchievementLabel"
          },
          "earned_on": {
            "$ref": "#/components/schemas/AchievementCardDate"
          },
          "tier_reached_on": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AchievementCardDate"
              }
            ],
            "nullable": true
          },
          "frame_rarity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AchievementLabel"
              }
            ],
            "nullable": true
          },
          "issued_rarity": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AchievementLabel"
              }
            ],
            "nullable": true
          },
          "earned_sentence": {
            "type": "string",
            "nullable": true
          },
          "verify_url": {
            "type": "string"
          },
          "share": {
            "type": "object",
            "properties": {
              "og": {
                "type": "string"
              },
              "story": {
                "type": "string"
              },
              "post": {
                "type": "string"
              }
            }
          },
          "head": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "description": {
                "type": "string"
              }
            }
          },
          "vocabulary": {
            "$ref": "#/components/schemas/AchievementVocabulary"
          },
          "owner": {
            "type": "object",
            "additionalProperties": true,
            "description": "Present only for the card's holder.",
            "properties": {
              "publicly_readable": {
                "type": "boolean"
              },
              "visibility_override": {
                "type": "string",
                "enum": [
                  "default",
                  "public",
                  "private"
                ]
              },
              "full_name": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "AchievementCardDate": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string"
          },
          "precision": {
            "type": "string",
            "enum": [
              "day",
              "month"
            ]
          }
        }
      },
      "CodedError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "example": "INVALID_INPUT"
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "First 12 characters of the key, for recognising it.",
            "example": "fh_live_3f9c"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Currently always `[\"*\"]` (the key carries all of its owner's permissions). Scopes are reserved for future use and cannot be set yet."
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Omitted from the create response."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the key stops working. Every key created now has one; `null` appears only on keys created before expiry became mandatory, which keep working until revoked."
          },
          "revoked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Omitted from the create response."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ProfileCampaignCard": {
        "type": "object",
        "description": "A campaign card built from the campaign's public fields, with the owner and stats joined on.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "org_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "owner_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "goal_amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "images": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "video_url": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "unlisted",
              "private"
            ]
          },
          "beneficiary_name": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "cover_image_focal_x": {
            "type": "number",
            "nullable": true
          },
          "cover_image_focal_y": {
            "type": "number",
            "nullable": true
          },
          "approved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "source_language": {
            "type": "string",
            "nullable": true
          },
          "is_project": {
            "type": "boolean"
          },
          "type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "profiles_name": {
            "type": "string",
            "nullable": true
          },
          "profiles_profile_slug": {
            "type": "string",
            "nullable": true
          },
          "profiles_avatar": {
            "type": "string",
            "nullable": true
          },
          "profiles_email_verified": {
            "type": "boolean",
            "nullable": true
          },
          "category_name": {
            "type": "string",
            "nullable": true
          },
          "total_raised_cents": {
            "type": "integer"
          },
          "donor_count": {
            "type": "integer"
          },
          "gallery": {
            "type": "array",
            "maxItems": 6,
            "description": "The campaign card's photos, at most six: the cover first, then its other public images in gallery order, each URL once. Images only — no videos, no photos taken down, no uploads still processing. Empty when the campaign has no photo. The same list and rule as `gallery` on `GET /fundraisers` items.",
            "items": {
              "type": "string"
            }
          },
          "profiles": {
            "type": "object",
            "nullable": true,
            "description": "The owner, nested. Null when the owner has no name.",
            "properties": {
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "email_verified": {
                "type": "boolean",
                "nullable": true
              },
              "profile_slug": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "is_endorsed": {
            "type": "boolean",
            "description": "True for a campaign this person endorsed rather than runs."
          },
          "translation_source": {
            "type": "string",
            "enum": [
              "source",
              "machine",
              "human"
            ],
            "description": "Present when a reader language was requested."
          },
          "original_title": {
            "type": "string",
            "nullable": true,
            "description": "Present when `title` was translated."
          },
          "original_summary": {
            "type": "string",
            "nullable": true,
            "description": "Present when `summary` was translated."
          }
        }
      },
      "RecentGift": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "amount_cents": {
            "type": "integer",
            "description": "Net of the Stripe fee."
          },
          "currency": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "comment": {
            "type": "string",
            "nullable": true,
            "description": "The donor's receipt note, else the legacy gift message."
          },
          "donor_name": {
            "type": "string",
            "nullable": true,
            "description": "Null for an anonymous gift."
          },
          "donor_avatar": {
            "type": "string",
            "nullable": true
          },
          "donor_city": {
            "type": "string",
            "nullable": true
          },
          "anonymous_key": {
            "type": "string",
            "nullable": true,
            "description": "A stable public key for an anonymous donor's page."
          },
          "donor_href": {
            "type": "string",
            "nullable": true,
            "description": "The donor's page — `/@slug`, `/d/guest/<key>` or `/d/anon/<key>`."
          },
          "gift_ordinal": {
            "type": "integer",
            "nullable": true,
            "description": "How many paid gifts this signed-in donor has made. Null for anonymous or guest gifts."
          },
          "fundraiser": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "image_url": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "via": {
            "type": "object",
            "nullable": true,
            "description": "The ambassador whose link drove the gift, if any.",
            "properties": {
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "slug": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "HeroActivityEvent": {
        "type": "object",
        "description": "One chip. Discriminated on `kind`; which other fields are present depends on it — `donation`: actor, via, campaign, amount_cents, currency; `referral_views`: via, campaign, count; `referral_click`: via, campaign; `viewers`: campaign, count; `fundraiser_submitted`: nothing else; `achievement`: actor, achievement.",
        "required": [
          "id",
          "kind",
          "at",
          "live"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque and stable."
          },
          "kind": {
            "type": "string",
            "enum": [
              "donation",
              "referral_views",
              "referral_click",
              "viewers",
              "fundraiser_submitted",
              "achievement"
            ]
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "live": {
            "type": "boolean"
          },
          "actor": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "nullable": true
              },
              "anonymous_key": {
                "type": "string",
                "nullable": true
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "href": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "via": {
            "type": "object",
            "nullable": true,
            "properties": {
              "name": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              },
              "href": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "campaign": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "href": {
                "type": "string"
              }
            }
          },
          "amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "achievement": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "tier": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "PlatformNumbers": {
        "type": "object",
        "properties": {
          "users": {
            "type": "integer",
            "description": "All-time active users from Google Analytics, or `registered_accounts` when GA is unavailable."
          },
          "users_source": {
            "type": "string",
            "enum": [
              "analytics",
              "accounts"
            ]
          },
          "registered_accounts": {
            "type": "integer",
            "description": "Excludes banned accounts."
          },
          "donations": {
            "type": "integer",
            "description": "Settled gifts."
          },
          "median_gift_cents": {
            "type": "integer"
          },
          "fundraisers_launched": {
            "type": "integer"
          },
          "fundraisers_goal_reached": {
            "type": "integer"
          },
          "cities_seen": {
            "type": "integer"
          },
          "cities_known_ratio": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Share of settled gifts with a billing city."
          },
          "ambassadors": {
            "type": "integer"
          },
          "ambassadors_active": {
            "type": "integer",
            "description": "Drove a settled gift in the last 30 days."
          },
          "raised_for_causes_cents": {
            "type": "integer"
          },
          "commission_taken_cents": {
            "type": "integer",
            "description": "Always `0` — FundlyHub takes no commission."
          },
          "computed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlatformTeamMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "handle": {
            "type": "string",
            "description": "The profile slug."
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar": {
            "type": "string",
            "nullable": true
          },
          "role": {
            "type": "string",
            "enum": [
              "super_admin",
              "moderator"
            ]
          },
          "href": {
            "type": "string",
            "description": "Profile path."
          },
          "impact": {
            "type": "integer",
            "description": "Raised + given + driven, all time, in cents."
          }
        }
      },
      "PlatformAmbassador": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "handle": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "avatar": {
            "type": "string",
            "nullable": true
          },
          "href": {
            "type": "string"
          },
          "impact": {
            "type": "integer",
            "description": "raised + given + driven, cents."
          },
          "raised": {
            "type": "integer",
            "description": "Cents."
          },
          "given": {
            "type": "integer",
            "description": "Cents."
          },
          "driven": {
            "type": "integer",
            "description": "Cents."
          },
          "impressions": {
            "type": "integer"
          }
        }
      },
      "PlatformTipReceipt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "one_time",
              "recurring"
            ]
          },
          "status": {
            "type": "string",
            "description": "The payment status, e.g. `pending`, `paid`, `failed`, `refunded`."
          },
          "settled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "donor_name": {
            "type": "string",
            "nullable": true,
            "description": "The signed-in tipper's profile name; null for guests and anonymous tips."
          },
          "subscription": {
            "type": "object",
            "description": "Monthly tips only; absent on a failed one.",
            "properties": {
              "interval": {
                "type": "string",
                "enum": [
                  "month"
                ]
              },
              "amount_cents": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "confirming",
                  "active",
                  "cancelled",
                  "on_hold"
                ]
              },
              "started_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "next_charge_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "ends_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          }
        }
      },
      "CreatorTierInput": {
        "type": "object",
        "required": [
          "name",
          "amount_cents"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "maxLength": 1000,
            "nullable": true
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 1,
            "description": "Monthly price."
          },
          "currency": {
            "type": "string",
            "default": "USD",
            "description": "Upper-cased. Fixed once created."
          },
          "annual_amount_cents": {
            "type": "integer",
            "minimum": 1,
            "nullable": true,
            "description": "Omit for monthly-only billing."
          },
          "benefits": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 200
            }
          },
          "cover_image": {
            "type": "string",
            "maxLength": 2048,
            "nullable": true
          },
          "sort_order": {
            "type": "integer",
            "default": 0
          },
          "subscriber_limit": {
            "type": "integer",
            "minimum": 1,
            "nullable": true,
            "description": "Cap on active subscribers, e.g. \"first 100 supporters\"."
          }
        }
      },
      "CreatorTier": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "creator_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "annual_amount_cents": {
            "type": "integer",
            "nullable": true
          },
          "benefits": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cover_image": {
            "type": "string",
            "nullable": true
          },
          "is_active": {
            "type": "boolean",
            "description": "False once archived."
          },
          "sort_order": {
            "type": "integer"
          },
          "subscriber_limit": {
            "type": "integer",
            "nullable": true
          },
          "stripe_product_id": {
            "type": "string",
            "nullable": true
          },
          "stripe_monthly_price_id": {
            "type": "string",
            "nullable": true
          },
          "stripe_annual_price_id": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatorSubscriptionStatus": {
        "type": "string",
        "enum": [
          "incomplete",
          "trialing",
          "active",
          "past_due",
          "canceled",
          "unpaid",
          "paused"
        ]
      },
      "CreatorSubscription": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "subscriber_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "creator_user_id": {
            "type": "string",
            "format": "uuid"
          },
          "tier_id": {
            "type": "string",
            "format": "uuid"
          },
          "stripe_subscription_id": {
            "type": "string",
            "nullable": true
          },
          "stripe_price_id": {
            "type": "string"
          },
          "billing_interval": {
            "type": "string",
            "enum": [
              "month",
              "year"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/CreatorSubscriptionStatus"
          },
          "current_period_start": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "current_period_end": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "cancel_at_period_end": {
            "type": "boolean"
          },
          "canceled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "tier_name": {
            "type": "string"
          },
          "tier_amount_cents": {
            "type": "integer"
          },
          "tier_annual_amount_cents": {
            "type": "integer",
            "nullable": true
          },
          "tier_currency": {
            "type": "string"
          },
          "creator_name": {
            "type": "string",
            "nullable": true
          },
          "creator_profile_slug": {
            "type": "string",
            "nullable": true
          },
          "creator_avatar": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "UnsubscribeRequest": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The signed token from the email's link."
          },
          "all": {
            "type": "boolean",
            "description": "`true` applies to every kind of mail rather than the token's scope."
          }
        }
      },
      "UnsubscribeResult": {
        "type": "object",
        "properties": {
          "email_masked": {
            "type": "string",
            "example": "j***e@example.com"
          },
          "scope": {
            "type": "string",
            "description": "`all`, or the email category the link was minted for."
          }
        }
      },
      "ReferralFundsByCurrency": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "currency": {
              "type": "string"
            },
            "amount_cents": {
              "type": "integer"
            }
          }
        }
      },
      "ReferralClicksByType": {
        "type": "object",
        "properties": {
          "human": {
            "type": "integer"
          },
          "bot": {
            "type": "integer"
          },
          "unknown": {
            "type": "integer"
          }
        }
      },
      "ReferralClickResult": {
        "type": "object",
        "properties": {
          "target_path": {
            "type": "string",
            "description": "Where to redirect — the campaign or home page, with `utm_*` preserved and the code in `utm_content`."
          },
          "visitor_id": {
            "type": "string",
            "format": "uuid"
          },
          "resolved": {
            "type": "boolean",
            "description": "Whether the code exists."
          },
          "recorded": {
            "type": "boolean",
            "description": "Whether a click row was written."
          }
        }
      },
      "ReferralAnalytics": {
        "type": "object",
        "properties": {
          "totals": {
            "type": "object",
            "properties": {
              "clicks_by_traffic_type": {
                "$ref": "#/components/schemas/ReferralClicksByType"
              },
              "distinct_human_visitors": {
                "type": "integer"
              },
              "conversions": {
                "type": "integer"
              },
              "funds_attributed": {
                "$ref": "#/components/schemas/ReferralFundsByCurrency"
              },
              "human_conversion_rate": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              }
            }
          },
          "by_fundraiser": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fundraiser_id": {
                  "type": "string",
                  "format": "uuid",
                  "nullable": true
                },
                "fundraiser_title": {
                  "type": "string",
                  "nullable": true
                },
                "clicks_human": {
                  "type": "integer"
                },
                "clicks_bot": {
                  "type": "integer"
                },
                "clicks_unknown": {
                  "type": "integer"
                },
                "conversions": {
                  "type": "integer"
                },
                "funds_attributed": {
                  "$ref": "#/components/schemas/ReferralFundsByCurrency"
                }
              }
            }
          },
          "by_utm_source": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "utm_source": {
                  "type": "string",
                  "nullable": true
                },
                "clicks_human": {
                  "type": "integer"
                },
                "clicks_bot": {
                  "type": "integer"
                },
                "clicks_unknown": {
                  "type": "integer"
                },
                "conversions": {
                  "type": "integer"
                }
              }
            }
          },
          "time_series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "clicks_human": {
                  "type": "integer"
                },
                "clicks_bot": {
                  "type": "integer"
                },
                "clicks_unknown": {
                  "type": "integer"
                },
                "conversions": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "AmbassadorGift": {
        "type": "object",
        "description": "One gift attributed to the caller's referral links, as the ambassador portal returns it. These are the only fields returned: no donor email, no payment card details and no receipt reference. For an anonymous gift `donor_name` is always `null`.",
        "properties": {
          "donation_id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable row key."
          },
          "attributed_at": {
            "type": "string",
            "format": "date-time"
          },
          "donor_name": {
            "type": "string",
            "nullable": true,
            "description": "The donor's public name. Always `null` for an anonymous gift."
          },
          "donor_is_anonymous": {
            "type": "boolean"
          },
          "fundraiser_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "fundraiser_title": {
            "type": "string",
            "nullable": true
          },
          "amount_cents": {
            "type": "integer"
          },
          "tip_amount_cents": {
            "type": "integer"
          },
          "fee_amount_cents": {
            "type": "integer"
          },
          "net_amount_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "nullable": true
          },
          "payment_status": {
            "type": "string",
            "nullable": true
          },
          "clicked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the referral click that led to the gift happened, when known."
          },
          "seconds_to_convert": {
            "type": "number",
            "nullable": true
          },
          "is_self_referral": {
            "type": "boolean",
            "description": "The ambassador gave through their own link. Listed, never counted."
          }
        }
      },
      "AmbassadorPortalRange": {
        "type": "object",
        "description": "The window actually applied, after defaults.",
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AmbassadorPortalSummary": {
        "type": "object",
        "properties": {
          "totals": {
            "type": "object",
            "properties": {
              "clicks_by_traffic_type": {
                "$ref": "#/components/schemas/ReferralClicksByType"
              },
              "distinct_human_visitors": {
                "type": "integer"
              },
              "driven_gifts": {
                "type": "integer"
              },
              "raised_cents": {
                "type": "integer"
              },
              "tips_cents": {
                "type": "integer"
              },
              "distinct_donors": {
                "type": "integer"
              },
              "refunded_count": {
                "type": "integer"
              },
              "refunded_amount_cents": {
                "type": "integer"
              },
              "human_conversion_rate": {
                "type": "number"
              },
              "currencies": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "funnel": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "time_series": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "clicks_human": {
                  "type": "integer"
                },
                "clicks_bot": {
                  "type": "integer"
                },
                "clicks_unknown": {
                  "type": "integer"
                },
                "driven_gifts": {
                  "type": "integer"
                },
                "raised_cents": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "AmbassadorPortalTraffic": {
        "type": "object",
        "properties": {
          "by_source": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                },
                "human": {
                  "type": "integer"
                },
                "driven_gifts": {
                  "type": "integer"
                }
              }
            }
          },
          "by_referer": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                },
                "human": {
                  "type": "integer"
                }
              }
            }
          },
          "by_medium": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                },
                "human": {
                  "type": "integer"
                }
              }
            }
          },
          "traffic_type": {
            "$ref": "#/components/schemas/ReferralClicksByType"
          },
          "geography": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "country": {
                  "type": "string"
                },
                "countryCode": {
                  "type": "string"
                },
                "sessions": {
                  "type": "integer"
                },
                "donations": {
                  "type": "integer"
                }
              }
            }
          },
          "devices": {
            "type": "object",
            "nullable": true,
            "properties": {
              "devices": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "device": {
                      "type": "string"
                    },
                    "sessions": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number"
                    }
                  }
                }
              },
              "browsers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "browser": {
                      "type": "string"
                    },
                    "sessions": {
                      "type": "integer"
                    },
                    "share": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "ga_note": {
            "type": "string",
            "enum": [
              "ok",
              "unconfigured",
              "no_codes",
              "no_data"
            ]
          }
        }
      },
      "DmcaError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "NOT_FOUND",
              "FORBIDDEN",
              "INVALID_STATE",
              "INVALID_INPUT",
              "NO_TARGET"
            ]
          }
        }
      },
      "AchievementHolder": {
        "type": "object",
        "description": "The public name block. Never the full name or email.",
        "nullable": true,
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "user"
            ]
          },
          "first_name": {
            "type": "string",
            "nullable": true
          },
          "short_name": {
            "type": "string",
            "nullable": true,
            "example": "Vitaliy R."
          },
          "handle": {
            "type": "string",
            "nullable": true,
            "description": "Null when the handle would reveal the email."
          },
          "profile_path": {
            "type": "string"
          },
          "source": {
            "type": "string"
          }
        }
      },
      "AchievementPin": {
        "type": "object",
        "nullable": true,
        "description": "Null only when nothing is earned.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "pinned",
              "automatic"
            ]
          },
          "slug": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "nullable": true,
            "description": "The card code (`/c/{code}` on the site)."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Authentication required",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Permission denied",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionCredentialRequired": {
        "description": "The request was authenticated with an API key, or made while an administrator is viewing the account as its owner. Managing API keys requires the owner's own signed-in session.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "SESSION_CREDENTIAL_REQUIRED",
                    "IMPERSONATION_RESTRICTED_ACTION"
                  ]
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Which bucket applies depends on the endpoint — see **Rate limits** in the API description.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests allowed in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the window resets.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Policy": {
            "description": "The policy in force, e.g. `300;w=60`.",
            "schema": {
              "type": "string"
            }
          },
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "retryAfter": {
                  "type": "integer",
                  "description": "Seconds until the window resets. Present on the shared limiters."
                }
              }
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid or missing request parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/cognito/signup": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Register a new user",
        "operationId": "registerUser",
        "description": "Create a new account via AWS Cognito. The account is confirmed immediately and a verification link is mailed to the address; the email stays unverified until that link is followed (see `GET /cognito/verify-email`), and several write endpoints refuse an unverified account. Gated by the `features.user_registration` flag — when the flag is off the endpoint answers `403`. Rate limited to 10 requests per minute per IP. When the risk engine flags the caller (repeated sign-ups from one IP), a reCAPTCHA token must be sent as `captchaToken`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "user@example.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password",
                    "example": "SecureP@ssw0rd!"
                  },
                  "name": {
                    "type": "string",
                    "description": "Display name. Optional; when it is omitted or blank the account has no name until the person sets one (`name` is then `null`).",
                    "example": "John Doe"
                  },
                  "captchaToken": {
                    "type": "string",
                    "description": "reCAPTCHA v3 token. Required only after a `429` with `requiresCaptcha`."
                  },
                  "ambassadorInviteToken": {
                    "type": "string",
                    "description": "An ambassador invitation token. Redeemed against the address the account is created with; a token that does not match is ignored and never fails the sign-up."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created; verification email sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "userSub": {
                      "type": "string",
                      "description": "The Cognito user id."
                    },
                    "userConfirmed": {
                      "type": "boolean",
                      "description": "False only if the automatic Cognito confirmation failed; the account then needs `POST /cognito/confirm`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing email or password, a disposable or malformed address, or a password that fails the policy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Registration is disabled (`features.user_registration` is off), or the reCAPTCHA token failed (`captchaFailed: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "An account with this email already exists (`code: EMAIL_ALREADY_REGISTERED`). Existing users sign in or reset their password instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the risk engine wants a CAPTCHA first — the body then carries `requiresCaptcha: true`; retry with `captchaToken`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "requiresCaptcha": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/cognito/confirm": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Confirm registration",
        "operationId": "confirmRegistration",
        "description": "Confirm a Cognito account with a verification code. Sign-up already confirms the account itself, so this is needed only when `POST /cognito/signup` answered `userConfirmed: false`. It does not mark the email verified — that is the link sent by sign-up.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "code"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string",
                    "example": "123456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account confirmed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing fields, or an invalid or expired code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cognito/signin": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Sign in",
        "operationId": "signIn",
        "description": "Authenticate with email and password. **Tokens are not returned in the body**: the session is set as httpOnly cookies (`access_token`, `id_token`, `refresh_token`) that the browser sends on every later request. Scripts and server-to-server integrations should use an API key instead (see **Authentication**). When the risk engine flags repeated failures, a reCAPTCHA token must be sent as `captchaToken`. If Cognito asks for a further challenge, the body carries `challengeRequired: true`, `challengeName` and `session` and no cookies are set.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "user@example.com"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  },
                  "captchaToken": {
                    "type": "string",
                    "description": "reCAPTCHA v3 token. Required only after a `429` with `requiresCaptcha`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authentication successful; session cookies set.",
            "headers": {
              "Set-Cookie": {
                "description": "`access_token`, `id_token` and `refresh_token`, all httpOnly.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Signin successful"
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "avatar": {
                          "type": "string",
                          "nullable": true
                        },
                        "role": {
                          "type": "string",
                          "nullable": true
                        },
                        "email_verified": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "profile_slug": {
                          "type": "string",
                          "nullable": true
                        },
                        "deletion_scheduled_for": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the account is scheduled to be deleted, while a deletion is pending; `null` otherwise."
                        }
                      }
                    },
                    "expiresIn": {
                      "type": "integer",
                      "description": "Access-token lifetime in seconds."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Email or password missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The Cognito account is not confirmed, a password reset is required, or the reCAPTCHA token failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or the risk engine wants a CAPTCHA first — the body then carries `requiresCaptcha: true`; retry with `captchaToken`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "requiresCaptcha": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/cognito/refresh": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Refresh tokens",
        "operationId": "refreshTokens",
        "description": "Exchange the `refresh_token` **cookie** for new access and ID token cookies. There is no request body — a refresh token sent in JSON is ignored.",
        "responses": {
          "200": {
            "description": "Tokens refreshed; new `access_token` and `id_token` cookies set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "expiresIn": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No refresh-token cookie, or Cognito rejected it (the session cookies are cleared).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Cognito was unreachable; the cookies are kept so the client can retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cognito/logout": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Sign out",
        "operationId": "signOut",
        "description": "Revoke the session's Cognito tokens (global sign-out) and clear the session cookies. Always answers `200`, even without a session.",
        "responses": {
          "200": {
            "description": "Logged out",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/cognito/me": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Get current user",
        "operationId": "getCurrentUser",
        "description": "The signed-in user's profile. **Cookie session only** — this endpoint does not accept an `Authorization` header or an API key. When the short-lived session has expired but the refresh cookie is still valid, it renews the session transparently and sets new cookies. While FundlyHub support is viewing the account on the user's behalf it returns that user with `is_impersonating: true` and `impersonated_by`.",
        "responses": {
          "200": {
            "description": "Current user info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "avatar": {
                          "type": "string",
                          "nullable": true
                        },
                        "role": {
                          "type": "string",
                          "nullable": true
                        },
                        "email_verified": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "profile_slug": {
                          "type": "string",
                          "nullable": true
                        },
                        "auth_provider": {
                          "type": "string",
                          "description": "How the account signs in, e.g. `email`, `google`, `apple`."
                        },
                        "needs_name_update": {
                          "type": "boolean",
                          "description": "True when the account has no name of its own yet: `name` is `null`, or still an automatic placeholder such as `Apple User 1234`."
                        },
                        "deletion_scheduled_for": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the account is scheduled to be deleted, while a deletion requested with `POST /users/me/deletion-request` is pending; `null` otherwise. Show it with a way to cancel."
                        },
                        "is_impersonating": {
                          "type": "boolean",
                          "description": "Present only while FundlyHub support is viewing the account."
                        },
                        "impersonated_by": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The support agent's profile id. Present only with `is_impersonating`."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/cognito/forgot-password": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Request password reset",
        "operationId": "forgotPassword",
        "description": "Send a password reset code to the user's email. Answers `200` whether or not the address has an account, so it cannot be used to discover accounts.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reset code sent, if the account exists",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Email missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cognito/reset-password": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Reset password",
        "operationId": "resetPassword",
        "description": "Set a new password using the reset code.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "code",
                  "newPassword"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string"
                  },
                  "newPassword": {
                    "type": "string",
                    "format": "password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing fields, an invalid or expired code, or a password that fails the policy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cognito/oauth/{provider}": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Initiate OAuth login",
        "operationId": "initiateOAuth",
        "description": "Redirect to the Cognito Hosted UI for a third-party provider. After the provider signs the user in, `GET /cognito/oauth/callback` sets the session cookies and redirects to `redirect` on the FundlyHub site.\n\nOpen this URL in the browser that will be signed in, by navigating to it rather than fetching it. The response sets a short-lived httpOnly cookie that the callback requires. A sign-in completed in a different browser, after about ten minutes, or after a newer sign-in was started in the same browser is refused, and the browser is sent to `/auth?error=state_mismatch` with no session set; starting again fixes it.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "google",
                "apple"
              ]
            }
          },
          {
            "name": "redirect",
            "in": "query",
            "required": false,
            "description": "The path on the FundlyHub site to send the browser to after sign-in, such as `/campaigns/help-food-bank?tab=updates`. It must start with a single `/`; a full URL on the FundlyHub site is reduced to its path. Any other value, such as a URL on another host or a protocol-relative `//host`, is replaced by `/`. Defaults to `/`.",
            "schema": {
              "type": "string",
              "maxLength": 2048,
              "example": "/campaigns/help-food-bank"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the OAuth provider. Sets the short-lived sign-in cookie that the callback checks."
          },
          "400": {
            "description": "Unsupported provider",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "List fundraisers",
        "operationId": "listFundraisers",
        "description": "A paginated list of public campaigns, newest first. No authentication; the result is the same whoever asks. Only published campaigns (`active` or `ended`) with `visibility: public` are listed — drafts, campaigns awaiting review, `paused`, `rejected`, `unlisted` and `private` campaigns never appear here, whatever is asked for — and deleted campaigns are excluded. Each item is a campaign card (`FundraiserCard`); fetch the detail read for the full campaign. Card text is translated into the reader's language (`lang`, then the language cookie, then `Accept-Language`) where a translation exists.\n\n**Near a point.** With `near=<lat>,<lng>` the list holds only the campaigns whose city is within `radius_mi` miles (default 20) of that point, nearest first and then most raised, and each item carries `distance_mi`. Every other filter still applies, and so do the public-only rules above. A campaign's point is its CITY's centroid, geocoded on the server from the free-text `location`; a campaign whose location has no city-level point yet is not in a `near` list. For the cities to offer a picker, read `GET /fundraisers/cities`.\n\nOnly the parameters below are accepted. Any other query parameter is ignored.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "`active` — live campaigns whose end date has not passed; `closed` — ended campaigns plus live ones past their end date; `ended` — the same as `closed`; `all` — both (the default). Any other value answers `400` with `{ \"error\": \"Invalid status\", \"allowed\": [...] }`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "closed",
                "ended",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category — its id, slug or name all match."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Free-text search over title, summary, story, category and location — the same match `GET /search?scope=campaigns` uses. Matches are ranked first."
          },
          {
            "name": "is_project",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "`true` for projects only, `false` for fundraisers only; omit for both."
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "raised"
              ]
            },
            "description": "`raised` sorts by most raised first. Omit for newest first."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            },
            "description": "Language to translate card text into."
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–100. A larger value is treated as `100`; a missing, zero, negative or non-numeric value as `20`.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Number of campaigns to skip, `0` or more. A negative or non-numeric value is treated as `0`.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "near",
            "in": "query",
            "description": "`<lat>,<lng>` in decimal degrees, e.g. `38.5816,-121.4944`: latitude −90..90, longitude −180..180. Lists only the campaigns within `radius_mi` of the point, nearest first, then most raised (this order replaces `sort` and the search rank), each with `distance_mi`. Anything that is not two numbers in range — including an empty value or the parameter sent twice — answers `400 { \"error\": \"invalid_near\" }`.",
            "schema": {
              "type": "string",
              "pattern": "^\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)\\s*,\\s*[+-]?(\\d+(\\.\\d*)?|\\.\\d+)\\s*$"
            },
            "example": "38.5816,-121.4944"
          },
          {
            "name": "radius_mi",
            "in": "query",
            "description": "Radius in miles around `near`, 1–100, default 20. A larger value is treated as `100`, a smaller one as `1`, and a non-numeric one as `20`. Ignored without `near`.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of campaign cards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FundraiserCard"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer",
                          "description": "The page size applied."
                        },
                        "offset": {
                          "type": "integer",
                          "description": "The offset applied."
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`status` is not one of `active`, `closed`, `ended` or `all` (`{ \"error\": \"Invalid status\", \"allowed\": [...] }`), or `near` is not a point (`{ \"error\": \"invalid_near\" }`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "error",
                        "allowed"
                      ],
                      "properties": {
                        "error": {
                          "type": "string",
                          "example": "Invalid status"
                        },
                        "allowed": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "example": [
                            "active",
                            "closed",
                            "ended",
                            "all"
                          ]
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "error"
                      ],
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "invalid_near"
                          ]
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "invalidStatus": {
                    "value": {
                      "error": "Invalid status",
                      "allowed": [
                        "active",
                        "closed",
                        "ended",
                        "all"
                      ]
                    }
                  },
                  "invalidNear": {
                    "value": {
                      "error": "invalid_near"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Create fundraiser",
        "operationId": "createFundraiser",
        "description": "Create a new fundraising campaign.\nSend `status: \"draft\"` to save a draft, which skips the publish gate. Any other status, **including an omitted one**, runs the publish gate (profile readiness, an image, reasonability and AI review), and a flagged campaign lands `pending` for review. Note that an omitted status that passes the gate is stored as `draft`, not `active`. Send `status: \"active\"` to publish.\nRequires a bearer session and is gated by the `features.fundraiser_creation` flag. Publishing requires a **verified email address**. A caller whose email is not yet verified may still create a **draft** (`status: \"draft\"` sent explicitly) and may hold at most 5 drafts. Past that the answer is `403` with code `UNVERIFIED_DRAFT_LIMIT`. Any other status from an unverified caller answers `403` with code `EMAIL_NOT_VERIFIED`. A failing gate answers `403`, not `401`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FundraiserCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Fundraiser created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Fundraiser"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email not verified and the request is not a draft (`EMAIL_NOT_VERIFIED`), an unverified caller already holds 5 drafts (`UNVERIFIED_DRAFT_LIMIT`), campaign creation disabled, a publish blocker, or the caller is not a member of the organization named in the payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A fundraiser with that slug already exists, or the organization named in `org_id` was approved with conditions this campaign breaks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/cities": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "List cities with active fundraisers",
        "operationId": "listFundraiserCities",
        "description": "Every city that has at least one active public campaign, for a \"near\" city picker: one row per city, with how many active public campaigns it has and a representative point to pass to `GET /fundraisers?near=<lat>,<lng>`. No authentication.\n\nActive means what `GET /fundraisers?status=active` lists: status `active`, end date not passed, `visibility: public`, not deleted. Cities come from a server-side, city-level geocode of each campaign's free-text `location`; campaigns whose location has no city (not yet geocoded, not a place, or a whole state or country) are not counted. `lat`/`lng` is the average of that city's campaigns' points, which are all the city's centroid, to 2 decimals. Sorted by `count`, most first, then by `label`. `label` is `\"City, REGION\"` in the US and `\"City, Country\"` elsewhere. Cached for up to 5 minutes.",
        "responses": {
          "200": {
            "description": "Cities with active public campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "label",
                          "city",
                          "region",
                          "country",
                          "lat",
                          "lng",
                          "count"
                        ],
                        "properties": {
                          "label": {
                            "type": "string",
                            "description": "`\"City, REGION\"` in the US, `\"City, Country\"` elsewhere.",
                            "example": "Sacramento, CA"
                          },
                          "city": {
                            "type": "string",
                            "example": "Sacramento"
                          },
                          "region": {
                            "type": "string",
                            "nullable": true,
                            "description": "State/province code where there is one, else its name.",
                            "example": "CA"
                          },
                          "country": {
                            "type": "string",
                            "nullable": true,
                            "description": "ISO 3166-1 alpha-2.",
                            "example": "US"
                          },
                          "lat": {
                            "type": "number",
                            "format": "double",
                            "example": 38.58
                          },
                          "lng": {
                            "type": "number",
                            "format": "double",
                            "example": -121.49
                          },
                          "count": {
                            "type": "integer",
                            "description": "Active public campaigns in this city.",
                            "example": 12
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "label": "Sacramento, CA",
                      "city": "Sacramento",
                      "region": "CA",
                      "country": "US",
                      "lat": 38.58,
                      "lng": -121.49,
                      "count": 12
                    },
                    {
                      "label": "Kyiv, Ukraine",
                      "city": "Kyiv",
                      "region": "Kyiv",
                      "country": "UA",
                      "lat": 50.45,
                      "lng": 30.52,
                      "count": 3
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get fundraiser",
        "operationId": "getFundraiser",
        "description": "Retrieve a single fundraiser by UUID — the stored row, without the joined owner and raised figures the slug read adds. Authentication is optional and widens what you can see: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted, is returned to its owner and answers `404` to everyone else. An `unlisted` campaign is readable with its link. The owner also gets `trustStatus` and the private fields they entered, such as `beneficiary_contact`; every other caller, signed in or not, gets the campaign without them.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay the stored translation for this language, when one exists.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fundraiser details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Fundraiser"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Update fundraiser",
        "operationId": "updateFundraiser",
        "description": "Update fundraiser details. Only the owner can update. Setting `status` is how a draft is published (`active`) or sent for review (`pending`). Publishing runs the same gate as creation — profile readiness, at least one image, reasonability and AI review — and a flagged campaign lands `pending`. A campaign that has collected money cannot go back to `draft`.\nThe caller's email must be verified, with one exception: an unverified caller may edit a campaign that is currently `draft`, as long as the body sends no `status` or sends `status: \"draft\"`. Moving a draft to any other status, or editing a campaign that is not a draft, answers `403` with code `EMAIL_NOT_VERIFIED`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FundraiserUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fundraiser updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Fundraiser"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (`details` lists the fields), no fields to update, or an attempt to unpublish (`status: draft`) a campaign that has collected money.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner, `EMAIL_NOT_VERIFIED`, or a publish blocker — the body then carries `blockers`, e.g. `fundraiser_image_required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Delete fundraiser",
        "operationId": "deleteFundraiser",
        "description": "Soft-deletes a fundraiser (sets `deleted_at`). Only the owner may delete. A campaign that has collected funds cannot be deleted and answers `400`. Once deleted, `GET /fundraisers/{id}` and `GET /fundraisers/slug/{slug}` answer `404` to everyone but its owner.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fundraiser soft-deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "campaignId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slug": {
                      "type": "string",
                      "nullable": true
                    },
                    "deletedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The campaign has received donations and cannot be deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/slug/{slug}": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get fundraiser by slug",
        "operationId": "getFundraiserBySlug",
        "description": "Retrieve a fundraiser using its URL-friendly slug, with the owner, category name, raised figures and share count joined in. Authentication is optional and widens what you can see, exactly as on `GET /fundraisers/{id}`: unpublished, private and deleted campaigns, the private fields such as `beneficiary_contact`, and `trustStatus` are for the owner only.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay the stored translation for this language, when one exists.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fundraiser details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Fundraiser"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/check-slug/{slug}": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Check slug availability",
        "operationId": "checkSlugAvailability",
        "description": "Check if a fundraiser slug is available. Every campaign counts, including drafts and soft-deleted ones.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "exclude",
            "in": "query",
            "required": false,
            "description": "A campaign id to ignore — pass the campaign being edited so its own slug reads as available.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    },
                    "suggestion": {
                      "type": "string",
                      "description": "An unused variant of the slug (`<slug>-2`, `-3`, …). Only present when `available` is false."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Slug is shorter than 3 characters or contains anything other than lowercase alphanumerics and hyphens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/stats": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get fundraiser statistics",
        "operationId": "getFundraiserStats",
        "description": "Aggregate totals for one campaign. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "title": {
                          "type": "string"
                        },
                        "goal_amount_cents": {
                          "type": "integer",
                          "description": "Goal in cents."
                        },
                        "total_raised_cents": {
                          "type": "integer",
                          "description": "Net raised in cents."
                        },
                        "total_tips_cents": {
                          "type": "integer",
                          "description": "Tips in cents."
                        },
                        "donation_count": {
                          "type": "integer"
                        },
                        "donor_count": {
                          "type": "integer",
                          "description": "Unique donors."
                        },
                        "percentage_funded": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/donations": {
      "post": {
        "tags": [
          "Donations"
        ],
        "summary": "Create donation",
        "operationId": "createDonation",
        "description": "Record a `pending` donation row against a fundraiser. The Stripe fee is computed server-side and the donor is the session's account.\nRequires a bearer session **and a verified email address**, is gated by the `features.donations` flag, and is protected by reCAPTCHA v3 (action `donation`) — send the token as `recaptcha_token` in the body or in the `x-recaptcha-token` header. Because this route already requires a verified email, a Cognito session may omit the token; an API key or impersonation session without one answers `400`. A token that is sent is always verified: below the score threshold answers `403`.\nThis is the bookkeeping half of a gift. Money is moved by `POST /payments/create-intent` + `POST /payments/confirm`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "x-recaptcha-token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "reCAPTCHA v3 token, action `donation`. Alternative to `recaptcha_token` in the body."
          },
          {
            "name": "x-app-attest-key-id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "iOS only. The App Attest key id (standard base64, as `DCAppAttestService.generateKey` returns it) of a key registered with `POST /app-attest/attest`. Send with `x-app-attest-assertion`."
          },
          {
            "name": "x-app-attest-assertion",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "iOS only. base64 of the assertion from `generateAssertion(keyId, clientDataHash: SHA256(raw request body))`. The body must carry a fresh `app_attest_challenge`. A valid assertion replaces the reCAPTCHA token; the headers alone never do. An invalid one answers `403` with `code` `APP_ATTEST_INVALID`, `APP_ATTEST_KEY_UNKNOWN` (attest a new key) or `APP_ATTEST_CHALLENGE_INVALID` (fetch a new challenge), unless the request passes reCAPTCHA some other way."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DonationCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Donation created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Donation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error, or the reCAPTCHA token is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email not verified, donations disabled (`features.donations` is off), reCAPTCHA rejected the request, or an App Attest assertion was invalid (`code` `APP_ATTEST_*`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{fundraiserId}/donations": {
      "get": {
        "tags": [
          "Donations"
        ],
        "summary": "List donations for fundraiser",
        "operationId": "listFundraiserDonations",
        "description": "Public donor wall for one fundraiser: paid donations only, newest first, with a true total count. Donor email and payment identifiers are never returned, and anonymous gifts have their donor name and avatar stripped. Amounts are **net** (donation minus the Stripe fee) so the list agrees with the creator's balance.\nOnly a campaign anyone may open by link (live, ended or paused; not private; not deleted) has a public donor wall. For any other campaign the answer is an empty `data` array with `total: 0`, the same as for an unknown id, unless the caller is signed in as the campaign's owner or an admin.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, clamped to 1–200. A missing, zero, negative or non-numeric value means 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "A missing, negative or non-numeric value means 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of donations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Donation"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/donations/receipt/{receiptId}": {
      "get": {
        "tags": [
          "Donations"
        ],
        "summary": "Get donation by receipt ID",
        "operationId": "getDonationByReceipt",
        "description": "The donor's receipt. Public — the receipt id is the Stripe PaymentIntent (`pi_…`) or invoice (`in_…`) id, a capability token only the donor holds, so treat it as a secret: the response includes the donor's email and card details. Answers for a donation in any payment state, including `pending`.",
        "parameters": [
          {
            "name": "receiptId",
            "in": "path",
            "required": true,
            "description": "The receipt id, or the donation's PaymentIntent id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Receipt details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid",
                          "nullable": true
                        },
                        "amount_cents": {
                          "type": "integer",
                          "description": "Gross donation in cents."
                        },
                        "net_amount_cents": {
                          "type": "integer"
                        },
                        "fee_amount_cents": {
                          "type": "integer"
                        },
                        "tip_amount_cents": {
                          "type": "integer"
                        },
                        "currency": {
                          "type": "string"
                        },
                        "donor_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "donor_email": {
                          "type": "string",
                          "nullable": true
                        },
                        "is_anonymous": {
                          "type": "boolean"
                        },
                        "payment_status": {
                          "type": "string",
                          "enum": [
                            "paid",
                            "refunded",
                            "failed",
                            "pending"
                          ]
                        },
                        "payment_method_type": {
                          "type": "string",
                          "nullable": true
                        },
                        "card_brand": {
                          "type": "string",
                          "nullable": true
                        },
                        "card_last4": {
                          "type": "string",
                          "nullable": true
                        },
                        "receipt_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "campaign_title": {
                          "type": "string",
                          "nullable": true
                        },
                        "campaign_slug": {
                          "type": "string",
                          "nullable": true
                        },
                        "beneficiary_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "fundraiser": {
                          "type": "object",
                          "properties": {
                            "title": {
                              "type": "string",
                              "nullable": true
                            },
                            "slug": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/donations/receipt/email": {
      "post": {
        "tags": [
          "Donations"
        ],
        "summary": "Send donation receipt",
        "operationId": "sendDonationReceipt",
        "description": "Email the receipt for one donation.\n\nThe donation is named by `receipt_id` and nothing else — every figure\nin the email is read from that row, so the caller cannot dictate the\ncontents. `receipt_id` is the Stripe PaymentIntent id, which is not\nguessable.\n\n`recipient_email` is deliberately free-form, because \"send it to my\naccountant\" is the feature. What bounds it instead is a cap of **5\nsends per donation**, after which this returns `429` for that donation\npermanently.\n\nAuthentication is optional; the endpoint is rate limited.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "receipt_id",
                  "recipient_email"
                ],
                "properties": {
                  "receipt_id": {
                    "type": "string",
                    "description": "The donation's receipt id. The donation's `payment_intent_id` is also accepted — the two have been written interchangeably over time.",
                    "example": "pi_3abc123def456"
                  },
                  "recipient_email": {
                    "type": "string",
                    "format": "email",
                    "description": "Lowercased before use. Need not be the original donor's address."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Receipt sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Receipt queued; it will be delivered shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "queued": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`receipt_id` or `recipient_email` missing, or the address is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such receipt, or the donation was never paid — the two answer the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Either the shared rate limit (5 requests per minute per IP), or this donation has already been emailed the maximum of 5 times.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The email provider rejected the message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/categories": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "List categories",
        "operationId": "listCategories",
        "description": "Get all active fundraiser categories, ordered by `display_order`.",
        "responses": {
          "200": {
            "description": "List of categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/categories/stats": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Get all category statistics",
        "operationId": "getAllCategoryStats",
        "description": "Organization count, active-campaign count and funds raised for every active category, in `display_order`. Only public campaigns and approved or verified organizations are counted.",
        "responses": {
          "200": {
            "description": "Statistics for all categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category_name": {
                            "type": "string"
                          },
                          "category_slug": {
                            "type": "string"
                          },
                          "organization_count": {
                            "type": "integer"
                          },
                          "campaign_count": {
                            "type": "integer",
                            "description": "Public campaigns with status `active`."
                          },
                          "total_raised_cents": {
                            "type": "integer",
                            "description": "GROSS raised in cents over paid donations to those campaigns, before the Stripe fee."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/categories/{id}": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Get category",
        "operationId": "getCategory",
        "description": "A single active category, addressed by its numeric id or its slug.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Category id (an integer) or slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Category details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Category"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/categories/{id}/stats": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Get category statistics",
        "operationId": "getCategoryStats",
        "description": "Organization count, active-campaign count and funds raised for one category. Only public campaigns with status `active` and approved or verified organizations are counted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Category id (an integer) or slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Category statistics with fundraiser counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "category_name": {
                          "type": "string"
                        },
                        "organization_count": {
                          "type": "integer"
                        },
                        "fundraiser_count": {
                          "type": "integer"
                        },
                        "total_raised_cents": {
                          "type": "integer",
                          "description": "GROSS raised in cents — the sum of paid donation amounts, before the Stripe fee. Unlike the campaign-level total_raised_cents, this is NOT net."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/organizations": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "List organizations",
        "operationId": "listOrganizations",
        "description": "Public list of approved/verified, non-deleted organizations. Returns their public fields plus a true total count. Rate-limited.",
        "parameters": [
          {
            "name": "verification_status",
            "in": "query",
            "required": false,
            "description": "Only `approved` or `verified` take effect; other values fall back to the default public filter.",
            "schema": {
              "type": "string",
              "enum": [
                "approved",
                "verified"
              ]
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "legal_name": {
                            "type": "string"
                          },
                          "dba_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "country": {
                            "type": "string",
                            "nullable": true
                          },
                          "address": {
                            "type": "string",
                            "nullable": true
                          },
                          "website": {
                            "type": "string",
                            "nullable": true
                          },
                          "logo": {
                            "type": "string",
                            "nullable": true
                          },
                          "banner_image": {
                            "type": "string",
                            "nullable": true
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "mission": {
                            "type": "string",
                            "nullable": true
                          },
                          "categories": {
                            "type": "array",
                            "nullable": true,
                            "items": {
                              "type": "string"
                            }
                          },
                          "social_links": {
                            "type": "object",
                            "nullable": true,
                            "additionalProperties": true
                          },
                          "founded_year": {
                            "type": "integer",
                            "nullable": true
                          },
                          "verification_status": {
                            "type": "string",
                            "enum": [
                              "approved",
                              "verified"
                            ]
                          },
                          "verification_completed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "company",
                              "conglomerate"
                            ]
                          },
                          "parent_organization_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Organizations"
        ],
        "summary": "Create organization",
        "operationId": "createOrganization",
        "description": "Multi-tier onboarding payload.\nRequires a bearer session **and a verified email address** — an unverified session answers `403`. Self-service creation is capped at **5 organizations per hour** per (IP, user).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "legal_name"
                ],
                "properties": {
                  "legal_name": {
                    "type": "string"
                  },
                  "ein": {
                    "type": "string",
                    "pattern": "^\\d{2}-\\d{7}$",
                    "example": "12-3456789"
                  },
                  "country": {
                    "type": "string"
                  },
                  "website": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "categories": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "company",
                      "conglomerate"
                    ],
                    "default": "company"
                  },
                  "parent_organization_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "A conglomerate the new company sits under."
                  },
                  "dbas": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "dba_name"
                      ],
                      "properties": {
                        "dba_name": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "locations": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "address"
                      ],
                      "properties": {
                        "label": {
                          "type": "string"
                        },
                        "address": {
                          "type": "object",
                          "description": "Free-form address fields, each a string (e.g. `line1`, `city`, `postal_code`).",
                          "additionalProperties": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Organization created, pending verification. The caller becomes its `org_owner`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "legal_name": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "company",
                            "conglomerate"
                          ]
                        },
                        "parent_organization_id": {
                          "type": "string",
                          "format": "uuid",
                          "nullable": true
                        },
                        "verification_status": {
                          "type": "string"
                        },
                        "dbas": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "dba_name": {
                                "type": "string"
                              },
                              "is_default": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "locations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "label": {
                                "type": "string",
                                "nullable": true
                              },
                              "address": {
                                "type": "object",
                                "additionalProperties": true
                              },
                              "is_primary": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email address not verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Organization already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/me": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "Get the caller's organizations",
        "operationId": "getMyOrganizations",
        "description": "Organizations the authenticated caller holds an active org-scoped role on. Flat array, no pagination.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Caller's organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "legal_name": {
                            "type": "string"
                          },
                          "dba_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "country": {
                            "type": "string",
                            "nullable": true
                          },
                          "website": {
                            "type": "string",
                            "nullable": true
                          },
                          "logo": {
                            "type": "string",
                            "nullable": true
                          },
                          "description": {
                            "type": "string",
                            "nullable": true
                          },
                          "verification_status": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "parent_organization_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "role_name": {
                            "type": "string"
                          },
                          "role_display_name": {
                            "type": "string"
                          },
                          "role_hierarchy_level": {
                            "type": "integer"
                          },
                          "org_role_title_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "org_role_title_label": {
                            "type": "string",
                            "nullable": true
                          },
                          "org_role_title_category": {
                            "type": "string",
                            "nullable": true
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "is_current": {
                            "type": "boolean"
                          },
                          "affiliation_description": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/organizations/{id}": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "Get organization by ID or slug",
        "operationId": "getOrganization",
        "description": "Public read of a single org by UUID or slug. Anonymous callers only see approved/verified non-deleted orgs; an active member of the organization can also read it in its other states and additionally receives suspension/rejection metadata (`suspension_reason`, `rejected_reason`, `suspended_until`) and the unredacted `contact_email`. Authentication is therefore optional but not inert.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Organization UUID or slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "legal_name": {
                          "type": "string"
                        },
                        "dba_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "slug": {
                          "type": "string",
                          "nullable": true
                        },
                        "country": {
                          "type": "string",
                          "nullable": true
                        },
                        "website": {
                          "type": "string",
                          "nullable": true
                        },
                        "logo": {
                          "type": "string",
                          "nullable": true
                        },
                        "banner_image": {
                          "type": "string",
                          "nullable": true
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "mission": {
                          "type": "string",
                          "nullable": true
                        },
                        "verification_status": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string"
                        },
                        "parent_organization_id": {
                          "type": "string",
                          "format": "uuid",
                          "nullable": true
                        },
                        "address": {
                          "type": "string",
                          "nullable": true
                        },
                        "categories": {
                          "type": "array",
                          "nullable": true,
                          "items": {
                            "type": "string"
                          }
                        },
                        "social_links": {
                          "type": "object",
                          "nullable": true,
                          "additionalProperties": true
                        },
                        "founded_year": {
                          "type": "integer",
                          "nullable": true
                        },
                        "verification_completed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "contact_email": {
                          "type": "string",
                          "nullable": true,
                          "description": "Null unless `contact_email_public` is true or the caller is a member."
                        },
                        "contact_email_public": {
                          "type": "boolean"
                        },
                        "suspension_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Members only."
                        },
                        "rejected_reason": {
                          "type": "string",
                          "nullable": true,
                          "description": "Members only."
                        },
                        "suspended_until": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Members only."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "primary_location": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "label": {
                              "type": "string",
                              "nullable": true
                            },
                            "address": {
                              "type": "object",
                              "additionalProperties": true
                            },
                            "is_publicly_visible": {
                              "type": "boolean"
                            }
                          }
                        },
                        "dbas": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "dba_name": {
                                "type": "string"
                              },
                              "is_default": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "children": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "slug": {
                                "type": "string",
                                "nullable": true
                              },
                              "legal_name": {
                                "type": "string"
                              },
                              "logo": {
                                "type": "string",
                                "nullable": true
                              },
                              "verification_status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/{id}/stats": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "Get organization stats",
        "operationId": "getOrganizationStats",
        "description": "Campaign counts, funds raised, unique donors and followers for an org (UUID or slug). A bare object — not wrapped in `data`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "campaignCount": {
                      "type": "integer"
                    },
                    "totalCampaignCount": {
                      "type": "integer"
                    },
                    "totalFundsRaisedCents": {
                      "type": "integer",
                      "description": "Lifetime GROSS funds raised over paid donations, in integer CENTS (before the Stripe fee). Divide by 100 for dollars.",
                      "example": 45600000
                    },
                    "uniqueDonorCount": {
                      "type": "integer"
                    },
                    "followerCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/{id}/members": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "Get organization public team",
        "operationId": "getOrganizationMembers",
        "description": "Public \"Our team\" list for an org (UUID or slug). Filtered to active, publicly-visible, non-expired memberships whose user profile is not private.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization team members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "user_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "profile_slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "kyc_verified_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "org_role_title_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "role_title_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "The taxonomy label, or the custom title the admin typed."
                          },
                          "role_title_category": {
                            "type": "string",
                            "nullable": true
                          },
                          "role_title_sort_order": {
                            "type": "integer",
                            "nullable": true
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "is_current": {
                            "type": "boolean"
                          },
                          "affiliation_description": {
                            "type": "string",
                            "nullable": true
                          },
                          "rbac_role_name": {
                            "type": "string"
                          },
                          "rbac_hierarchy_level": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/{id}/updates": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "Get organization updates feed",
        "operationId": "getOrganizationUpdates",
        "description": "Public, latest-first updates feed for an org (UUID or slug). Offset pagination, limit clamped 1..50.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Organization updates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "org_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "author_user_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "title": {
                            "type": "string"
                          },
                          "body": {
                            "type": "string"
                          },
                          "cover_image": {
                            "type": "string",
                            "nullable": true
                          },
                          "published_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "author_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "author_profile_slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "author_avatar": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/organizations/{id}/documents/public": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "List public organization documents",
        "operationId": "getOrganizationPublicDocuments",
        "description": "Approved trust documents an organization admin has marked public, for an approved or verified org (UUID or slug). Metadata only — there is no public download.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public documents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "doc_type": {
                            "type": "string"
                          },
                          "original_filename": {
                            "type": "string"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "size_bytes": {
                            "type": "integer"
                          },
                          "reviewed_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/users/{id}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get user profile",
        "operationId": "getUserProfile",
        "description": "Public profile by UUID or profile_slug. Authentication is optional: the caller's own profile may include their email, and a profile marked private is redacted for every other viewer.\nWHAT SURVIVES THE REDACTION (#1696): `id`, `name`, `avatar`, `profile_slug`, `created_at` and the four counters — `campaign_count`, `total_funds_raised`, `follower_count` and `following_count` — with their REAL values. The leaderboard has always published a private person's rank and impact, so a profile reporting zeroes beside a board reporting real figures would be two surfaces disagreeing about one person rather than privacy.\nDropped: `bio`, `location`, `website`, `social_links`, `email`, `phone`, `private_contact_email`, `display_name`, `account_status`, `kyc_verified_at` and `account_kind`. The last three are more than a name — a Team chip and a verification tick say something about the person, which is exactly what a private profile withholds.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "User UUID or profile_slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User profile — a bare object, not wrapped in `data`. The fields marked *own profile only* are present only when the caller is the profile's owner.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string",
                      "nullable": true,
                      "description": "The name the person gave, or `null` when they have not set one. Never derived from their email address; use `display_name` for a label that is always present."
                    },
                    "display_name": {
                      "type": "string",
                      "description": "What to call this person: their name, else `@` and their profile handle, else `FundlyHub member`. Never an email address."
                    },
                    "email": {
                      "type": "string",
                      "description": "*Own profile only.*"
                    },
                    "avatar": {
                      "type": "string",
                      "nullable": true
                    },
                    "bio": {
                      "type": "string",
                      "nullable": true
                    },
                    "location": {
                      "type": "string",
                      "nullable": true
                    },
                    "website": {
                      "type": "string",
                      "nullable": true
                    },
                    "social_links": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "profile_visibility": {
                      "type": "string",
                      "enum": [
                        "public",
                        "private"
                      ]
                    },
                    "profile_slug": {
                      "type": "string"
                    },
                    "account_status": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    },
                    "campaign_count": {
                      "type": "integer"
                    },
                    "total_funds_raised": {
                      "type": "number",
                      "description": "Raised across the person's campaigns, in DOLLARS (a decimal) — the one profile counter not yet in cents."
                    },
                    "follower_count": {
                      "type": "integer"
                    },
                    "following_count": {
                      "type": "integer"
                    },
                    "phone": {
                      "type": "string",
                      "description": "*Own profile only.*"
                    },
                    "private_contact_email": {
                      "type": "string",
                      "description": "*Own profile only.*"
                    },
                    "private_contact_verified": {
                      "type": "boolean",
                      "description": "*Own profile only.*"
                    },
                    "kyc_verified_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "account_kind": {
                      "type": "string",
                      "description": "The badge the profile header shows (e.g. a Team or Ambassador chip), resolved server-side."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Users"
        ],
        "summary": "Update user profile",
        "operationId": "updateProfile",
        "description": "Update your own profile. The fields below are the whole writable set;\nanything else in the body is ignored rather than rejected.\n\n`location` has been accepted since #1654 and was missing from this\nschema; `profile_visibility` is new in #1696.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "profile_slug": {
                    "type": "string"
                  },
                  "social_links": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "location": {
                    "type": "string",
                    "description": "A city and a two-letter state, e.g. `Sacramento, CA`. Digits\nare refused: a ZIP or a street number does not belong on a\npublic profile. An empty string clears it.\n",
                    "example": "Sacramento, CA"
                  },
                  "profile_visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private"
                    ],
                    "description": "Who may see the detail of this profile.\n\nA `private` profile still publishes its avatar, its name and\nits four counters — campaigns, impact, followers, following.\nWhat it withholds is everything descriptive (bio, location,\nwebsite, social links) and everything the person has done:\ncampaigns, achievements, supported causes, organizations,\ncreator tiers and the activity feed. `GET /users/{id}/impact`\nanswers the headline figure only, and the follower and\nfollowing LISTS refuse.\n\n`unlisted` is not accepted here.\n",
                    "example": "private"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/organizations": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a user's organization memberships",
        "operationId": "getUserOrganizations",
        "description": "Public org memberships for a user (UUID or profile_slug). Memberships marked not publicly visible, memberships of deleted organizations, and every membership of a private profile — even to its owner — are left out. 404 only when the user does not exist.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "The organization's public fields plus the membership: the same shape as `GET /organizations/me`.",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "legal_name": {
                            "type": "string"
                          },
                          "dba_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "logo": {
                            "type": "string",
                            "nullable": true
                          },
                          "verification_status": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "role_name": {
                            "type": "string"
                          },
                          "role_display_name": {
                            "type": "string"
                          },
                          "org_role_title_label": {
                            "type": "string",
                            "nullable": true
                          },
                          "start_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "end_date": {
                            "type": "string",
                            "format": "date",
                            "nullable": true
                          },
                          "is_current": {
                            "type": "boolean"
                          },
                          "affiliation_description": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/users/{id}/activity": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a user's public activity feed",
        "operationId": "getProfileActivity",
        "description": "The public activity feed on a profile (#1693): six kinds of public act, ordered newest first. Accepts a UUID or a profile_slug; auth-optional.\n\n`campaign_launched` (the campaign passed approval), `achievement_earned` (a badge, read through the same public projection the profile badge strip uses), `commented`, `donated`, `update_posted` and `endorsed` (declared, or their referral link for that campaign recorded a visit — the definition the Endorsed-by dialog and the profile chip already use).\n\nEach row inherits the visibility of the thing it reports: a refunded or charged-back gift, a hidden or retracted comment, a withdrawn update, a soft-deleted campaign and a revoked award are all simply absent. **Anonymous gifts never appear, for anyone, including the profile's owner.** A campaign that is unlisted, private, paused, draft or deleted is never named. A profile with `show_donations_on_profile` off keeps every row type except `donated`.\n\nAll money is INTEGER CENTS, never dollars. `amount_cents` on a `donated` row is what the giver paid — the donation plus the platform tip — which is the same figure the profile's Impact stat sums, so the two cannot disagree.\n\nPaging is a KEYSET cursor, not an offset: pass the `nextCursor` from the previous page back as `cursor`. A cursor this endpoint did not issue is a 400. `nextCursor` is null on the last page.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A user UUID or a profile_slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "group",
            "in": "query",
            "required": false,
            "description": "Filter chip. `giving` is `donated`; `fundraising` is `campaign_launched` and `update_posted`; `community` is `commented`, `achievement_earned` and `endorsed`. An unrecognised value falls back to `all`. The `summary` block is NOT filtered — it is the strip above the chips and always counts every type.\n",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "giving",
                "fundraising",
                "community"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The opaque `nextCursor` from the previous page. Omit for page 1.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the feed. A private profile answers 200 with an empty `items` array to everybody but its owner, rather than 404 — the page renders and the section is simply empty.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor",
                    "joinedAt",
                    "summary"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "type",
                          "at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "`<type>:<source key>` — stable, usable as a list key and as a deep link. For `achievement_earned` the source key is the badge SLUG, which is how every public surface addresses a badge.\n",
                            "example": "donated:6f1c2a2e-2b0a-4c6f-9a7e-2f0b9c1d4e55"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "donated",
                              "commented",
                              "update_posted",
                              "campaign_launched",
                              "achievement_earned",
                              "endorsed"
                            ]
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "The witnessed timestamp for this row. For `donated` that is when the gift settled, not when the payment form was opened.\n"
                          },
                          "campaign": {
                            "nullable": true,
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "slug": {
                                "type": "string"
                              },
                              "title": {
                                "type": "string"
                              },
                              "cover_image": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "amount_cents": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Integer minor units. Never dollars."
                          },
                          "currency": {
                            "type": "string",
                            "nullable": true
                          },
                          "excerpt": {
                            "type": "string",
                            "nullable": true,
                            "description": "Comment body or update title, trimmed to 280 characters. Null for a comment that is only a GIF."
                          },
                          "gif": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Gif"
                              }
                            ],
                            "nullable": true,
                            "description": "The GIF on a `commented` row (#1963); null on every other row."
                          },
                          "achievement": {
                            "nullable": true,
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "The badge slug."
                              },
                              "title": {
                                "type": "string",
                                "nullable": true
                              },
                              "tagline": {
                                "type": "string",
                                "nullable": true
                              },
                              "tier": {
                                "type": "string",
                                "nullable": true
                              },
                              "badge_art_url": {
                                "type": "string",
                                "nullable": true
                              },
                              "background_color": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    },
                    "joinedAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the account was created — the terminal \"Joined FundlyHub\" row at the end of the feed.\n"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "windowDays": {
                          "type": "integer",
                          "enum": [
                            30
                          ]
                        },
                        "total": {
                          "type": "integer"
                        },
                        "byType": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "The `features.activity_feed` flag is off."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/users/{id}/donation-activity": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a user's public donation activity",
        "operationId": "getUserDonationActivity",
        "description": "\"Causes I support\" on a public profile — the aggregate plus one entry per supported campaign. Accepts a UUID or a profile_slug; 404 only when the user does not exist.\n\nAnonymous gifts are excluded, always. A profile that is private, or that has `show_donations_on_profile` off, answers zeroes. Only settled (`paid`) gifts count, and only to a campaign a reader could actually open — a deleted, draft, private, paused or unlisted campaign is neither listed nor counted (#1694).\n\nEach entry in `recent_supported_causes` is a full campaign card, the same read model `/fundraisers` serves the /causes grid from, so the section renders the card the rest of the product uses. `total_cents` figures are INTEGER CENTS.\n\n`total_donated_cents` is the PUBLIC figure and excludes anonymous gifts. It is the GIVER-FACING value of those gifts — the donation plus the platform tip, which is what the card was charged — so it agrees with the profile activity feed's per-gift figures and with the leaderboard's `given` figure. `/donor/me/summary` counts every gift the owner made and is a different number by design; do not compare them.\n\n`currency` is the ISO 4217 code `total_donated_cents` is denominated in, taken from the donor's most recent gift that named one. Read it: minor units are not always hundredths, so formatting the total without it is a 100x error for a zero-decimal currency, not just the wrong symbol.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Causes per page. The profile rail asks for 6; the dedicated page asks for more.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 48,
              "default": 6
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User donation activity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "total_donated_cents": {
                          "type": "integer",
                          "description": "INTEGER MINOR UNITS. The donation plus the platform tip, summed over every counted gift.\n"
                        },
                        "currency": {
                          "type": "string",
                          "description": "ISO 4217 code for `total_donated_cents`, from the donor's most recent gift that named one; the platform default when no gift does.\n",
                          "example": "USD"
                        },
                        "causes_supported_count": {
                          "type": "integer"
                        },
                        "recent_supported_causes": {
                          "type": "array",
                          "description": "One campaign card per supported campaign, newest gift first. Carries the campaign's public fields plus `profiles`, `owner_name`, `owner_avatar`, `category_name`, `total_raised_cents`, `donor_count` and `gallery` (the card's photos, cover first, at most six — as on `GET /fundraisers`), and the `fundraiser_id` and `last_donation_at` fields.\n",
                          "items": {
                            "type": "object"
                          }
                        },
                        "pagination": {
                          "type": "object",
                          "properties": {
                            "limit": {
                              "type": "integer"
                            },
                            "offset": {
                              "type": "integer"
                            },
                            "total": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/users/{id}/impact": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a user's Impact and leaderboard rank",
        "operationId": "getUserImpact",
        "description": "The figures behind the Impact stat on a public profile, read from the same all-time field `/leaderboard` is served from, so the two surfaces always agree. Accepts a UUID or a profile_slug. All money is integer cents. A user who has not moved money answers zeroes and a null rank rather than 404 — their referral impressions are still reported. A private profile answers its headline `impact` figure only, with the rest of the breakdown zeroed and `rank` null, to everybody but its owner. If the leaderboard field cannot be read the endpoint answers `200` with zeroes rather than failing the profile.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The user's Impact breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "impact": {
                      "type": "object",
                      "properties": {
                        "rank": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Place on the all-time Everyone leaderboard, or null when unranked."
                        },
                        "impact": {
                          "type": "integer",
                          "description": "raised + given + driven, in cents — what the leaderboard ranks on."
                        },
                        "raised": {
                          "type": "integer",
                          "description": "Sum of the person's counted campaign cards, in cents. Own gifts included."
                        },
                        "given": {
                          "type": "integer",
                          "description": "Everything they paid, donation plus tip, to their own campaigns and to others', in cents."
                        },
                        "driven": {
                          "type": "integer",
                          "description": "Value their referral links drove to campaigns they do not organise, in cents."
                        },
                        "impressions": {
                          "type": "integer",
                          "description": "Non-bot visits on their referral links. A count, not money."
                        },
                        "gifts": {
                          "type": "integer",
                          "description": "Settled gifts they made."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/users/{id}/permissions": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a user's permissions",
        "operationId": "getUserPermissions",
        "description": "Role assignments and effective permissions for the user named in the path. Requires a bearer session. Callers may read their own; reading another user's requires the `view_all_users` permission, and any other caller gets `403`. Answers can be several minutes old; for the caller's own, current permissions use `GET /me/capabilities`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User permissions — a bare object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "role_name": {
                            "type": "string"
                          },
                          "context_type": {
                            "type": "string",
                            "description": "`global`, `organization`, …"
                          },
                          "context_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "hierarchy_level": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "roleDefinitions": {
                      "type": "array",
                      "description": "Every role on the platform, with its hierarchy level.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "hierarchy_level": {
                            "type": "integer"
                          },
                          "context_type": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own and the caller lacks `view_all_users`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/preferences": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get user preferences",
        "operationId": "getUserPreferences",
        "description": "Returns the caller's own preferences. Callers may only access their own — any other path id answers `403`.\nAccounts with no stored row get a default object rather than a `404`, so the returned key set differs slightly between the two cases. `suppressed_scopes` is always present: it lists the notification scopes this account's **email address** has been unsubscribed from via an emailed link. Those suppressions are keyed by address, not by account, so they stop mail even while the matching toggle reads true — which is exactly why they are reported separately rather than folded into the toggles.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User preferences",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "user_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "email_notifications": {
                      "type": "boolean"
                    },
                    "push_notifications": {
                      "type": "boolean"
                    },
                    "suppressed_scopes": {
                      "type": "array",
                      "description": "Notification scopes this account's email address has opted out of. Only self-service opt-outs appear here — suppressions FundlyHub applies itself (for example after a bounce) stay in force and are not listed.",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Update user preferences",
        "operationId": "updateUserPreferences",
        "description": "Upserts the caller's own preferences and returns the stored row. Callers may only update their own — any other path id answers `403`. `suppressed_scopes` is read-only and is not accepted here; an address unsubscribed by an emailed link can only be resubscribed through `/unsubscribe/resubscribe`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A partial update — an omitted field keeps its stored value. Unknown fields are ignored.",
                "properties": {
                  "view_mode": {
                    "type": "string",
                    "enum": [
                      "grid",
                      "list"
                    ]
                  },
                  "theme": {
                    "type": "string"
                  },
                  "admin_theme": {
                    "type": "string"
                  },
                  "recent_searches": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "search_suggestions": {
                    "type": "boolean"
                  },
                  "email_notifications": {
                    "type": "boolean"
                  },
                  "push_notifications": {
                    "type": "boolean"
                  },
                  "reduced_motion": {
                    "type": "boolean"
                  },
                  "high_contrast": {
                    "type": "boolean"
                  },
                  "font_size": {
                    "type": "string"
                  },
                  "has_completed_onboarding": {
                    "type": "boolean"
                  },
                  "has_skipped_onboarding": {
                    "type": "boolean"
                  },
                  "last_visited": {
                    "type": "string"
                  },
                  "auto_save": {
                    "type": "boolean"
                  },
                  "default_category": {
                    "type": "string"
                  },
                  "notify_donations": {
                    "type": "boolean"
                  },
                  "notify_comments": {
                    "type": "boolean"
                  },
                  "notify_updates": {
                    "type": "boolean"
                  },
                  "notify_milestones": {
                    "type": "boolean"
                  },
                  "notify_campaign_status": {
                    "type": "boolean"
                  },
                  "notify_followers": {
                    "type": "boolean"
                  },
                  "notify_org_status": {
                    "type": "boolean"
                  },
                  "notify_payouts": {
                    "type": "boolean"
                  },
                  "notify_digest": {
                    "type": "boolean"
                  },
                  "notify_donation_reminders": {
                    "type": "boolean"
                  },
                  "notify_endorsement_requests": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated preferences — the stored row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/avatar": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Upload user avatar",
        "operationId": "uploadAvatar",
        "description": "Upload the caller's own avatar from a base64-encoded image — JPEG, PNG or WebP, at most 5 MB decoded. Callers may only update their own.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fileBase64",
                  "contentType"
                ],
                "properties": {
                  "fileBase64": {
                    "type": "string"
                  },
                  "contentType": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Avatar uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "avatar_url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Delete user avatar",
        "operationId": "deleteAvatar",
        "description": "Delete the caller's own avatar. Callers may only delete their own.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Avatar deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/deactivate": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Self-deactivate account",
        "operationId": "selfDeactivateAccount",
        "description": "Deactivates the caller's own account and clears auth cookies. Callers may only deactivate their own.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account deactivated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path id is not the caller's own profile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Account could not be deactivated: it still owns active campaigns that have collected money. `error` names them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/me/deletion-request": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Request account deletion",
        "operationId": "requestAccountDeletion",
        "description": "Schedules deletion of the caller's account 30 days from now. In the same step every personal campaign of theirs that is active, paused or pending is closed (status `ended`), so it stops taking donations, and every session is signed out: the cookies of this one are cleared, every refresh token is revoked and every API key is revoked. An access token already issued keeps working until it expires (at most an hour).\n\nThe person can sign in again during the 30 days, which is how they reach Cancel; `GET /cognito/me` and the sign-in response then carry `deletion_scheduled_for`. They cannot publish or reopen a campaign meanwhile (publish blocker `account_deletion_pending`).\n\nAfter the 30 days the account is deleted once nothing is owed to the person: money not yet paid out is paid out to their verified payout account first, and deletion waits for it. Donation and payout records are kept, anonymised; everything else is deleted, including the sign-in. Campaigns they created for an organization are the organization's: they keep running, and when the deletion completes they are handed to an owner of that organization, unchanged.\n\nIdempotent: while a request is pending, calling again returns `200` with the same schedule and changes nothing. Requires a signed-in session; refused for an API key and while FundlyHub support is viewing the account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "A deletion was already pending; its schedule, unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountDeletionSchedule"
                }
              }
            }
          },
          "201": {
            "description": "Deletion scheduled; this session is signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountDeletionSchedule"
                },
                "example": {
                  "scheduled_for": "2026-11-06T12:00:00.000Z",
                  "requested_at": "2026-10-07T12:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a signed-in session (`SESSION_CREDENTIAL_REQUIRED`), or FundlyHub support is viewing the account (`IMPERSONATION_RESTRICTED_ACTION`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The caller is the only owner of one or more organizations (`SOLE_ORGANIZATION_OWNER`). Deleting the account would leave each with nobody able to run it; make another member an owner, or close the organization, first. Nothing was scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "organizations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "You are the only owner of an organization. Make someone else an owner, or close the organization, before deleting your account.",
                  "code": "SOLE_ORGANIZATION_OWNER",
                  "organizations": [
                    {
                      "id": "7d1f0c2a-9b3e-4f5a-8c6d-1e2f3a4b5c6d",
                      "name": "Riverside Food Bank"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get the pending account deletion",
        "operationId": "getAccountDeletion",
        "description": "The caller's pending account deletion. `awaiting_payout` is true once the 30 days have passed and deletion is waiting for money owed to the caller to be paid out.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "A deletion is pending.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "requested_at",
                    "scheduled_for",
                    "awaiting_payout"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending"
                      ]
                    },
                    "requested_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "scheduled_for": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "awaiting_payout": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "status": "pending",
                  "requested_at": "2026-10-07T12:00:00.000Z",
                  "scheduled_for": "2026-11-06T12:00:00.000Z",
                  "awaiting_payout": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No deletion is pending (`NO_PENDING_DELETION`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Cancel account deletion",
        "operationId": "cancelAccountDeletion",
        "description": "Cancels the caller's pending account deletion. Allowed until the deletion has completed. Campaigns the request closed go back to the status they had, if they are still closed and not deleted; any other campaign is left as it is. Requires a signed-in session; refused for an API key and while FundlyHub support is viewing the account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "cancelled_at",
                    "restored_campaign_ids"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "cancelled"
                      ]
                    },
                    "cancelled_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "restored_campaign_ids": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "description": "The campaigns reopened to their earlier status."
                    }
                  }
                },
                "example": {
                  "status": "cancelled",
                  "cancelled_at": "2026-10-08T09:30:00.000Z",
                  "restored_campaign_ids": [
                    "5b0c3a8e-2f4d-4c1a-9e7b-0d6f1a2b3c4d"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a signed-in session (`SESSION_CREDENTIAL_REQUIRED`), or FundlyHub support is viewing the account (`IMPERSONATION_RESTRICTED_ACTION`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No deletion is pending (`NO_PENDING_DELETION`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/me/private-contact": {
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Set private contact email",
        "operationId": "setPrivateContact",
        "description": "Sets the caller's private contact email (visible only to the FundlyHub team). Validates format, blocks disposable domains, and triggers a verification email when the address changes — unless it is the caller's own verified sign-in address, which counts as verified at once. Shares the verification-resend limit: 5 requests per minute per user.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Private contact email saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string"
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The address is already another account's contact or sign-in address (`code: CONTACT_EMAIL_TAKEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/users/me/phone": {
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Set phone number",
        "operationId": "setPhone",
        "description": "Sets the caller's phone number (stored only; no SMS verification).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "phone"
                ],
                "properties": {
                  "phone": {
                    "type": "string",
                    "pattern": "^[+\\d\\s\\-().]{7,20}$",
                    "description": "Digits, spaces, `+`, `-`, `(`, `)` and `.`, 7 to 20 characters. Stored trimmed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Phone number saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "phone": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/users/me/publish-readiness": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get publish-readiness checklist",
        "operationId": "getPublishReadiness",
        "description": "Returns the publish-gate checklist for the caller (hard blockers + soft suggestions). When the checklist is unavailable the answer is `ready: true`, `checklist: null` and `migration_pending: true`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Publish-readiness status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ready": {
                      "type": "boolean"
                    },
                    "checklist": {
                      "type": "object",
                      "nullable": true,
                      "additionalProperties": {
                        "type": "boolean"
                      }
                    },
                    "blockers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "migration_pending": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/users/check-slug/{slug}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Check profile slug availability",
        "operationId": "checkProfileSlug",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "exclude",
            "in": "query",
            "required": false,
            "description": "User id to exclude from the uniqueness check.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Slug availability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/leaderboard": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Public leaderboard",
        "operationId": "getLeaderboard",
        "description": "One ranked list on impact = raised + given + driven, in integer cents, over the chosen period.\nRaised is the sum of the person's counted campaign cards — settled gifts to public fundraisers they organize, net of processor fees, their own gifts included;\nGiven is every settled gift they made under their name; Driven is other people's settled gifts through\ntheir share or ambassador link to fundraisers they do not organize. Every dollar counts once.\nAnonymous donors appear as alias rows keyed on an opaque `anonymousKey`; guest and anonymous rows link to their donor page under `/d/{kind}/{key}`.\nWhen the request carries a session, `standing` describes where the caller ranks on the full view.\n`q` searches the ranked board by name without renumbering it: a match keeps the rank it has on the full view.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "30d",
                "7d"
              ],
              "default": "all"
            }
          },
          {
            "name": "role",
            "in": "query",
            "description": "Everyone; people organizing a public fundraiser; ambassador role holders; or donors — everyone with a settled gift under their name, whatever their role, anonymous identities included. Everyone, creators and ambassadors rank on impact; donors rank on given (see `metric`). A filtered view is ranked within itself.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "creators",
                "ambassadors",
                "donors"
              ],
              "default": "all"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Name search over this view. Trimmed and cut to 100 characters.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Language for badge titles and anonymous aliases (en, ru, uk, es). Defaults from Accept-Language.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the ranked view",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rank": {
                            "type": "integer",
                            "description": "Consecutive, 1..N; an equal figure goes to the more recent person"
                          },
                          "tied": {
                            "type": "boolean",
                            "description": "Always false; ranks are consecutive. Kept for compatibility"
                          },
                          "id": {
                            "type": "string",
                            "description": "Profile id, `guest:<key>` for a guest donor, or `anon:<key>` for an anonymous identity"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "individual",
                              "team",
                              "ambassador",
                              "org",
                              "anonymous"
                            ]
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "href": {
                            "type": "string",
                            "nullable": true,
                            "description": "Profile path, or `/d/guest/<key>` / `/d/anon/<key>` for a donor identity; null only for a private profile"
                          },
                          "anonymousKey": {
                            "type": "string",
                            "nullable": true
                          },
                          "isCreator": {
                            "type": "boolean"
                          },
                          "isAmbassador": {
                            "type": "boolean"
                          },
                          "raised": {
                            "type": "integer",
                            "description": "Cents"
                          },
                          "given": {
                            "type": "integer",
                            "description": "Cents the person paid as a donor: donation plus tip"
                          },
                          "driven": {
                            "type": "integer",
                            "description": "Cents"
                          },
                          "impact": {
                            "type": "integer",
                            "description": "Cents; raised + given + driven"
                          },
                          "score": {
                            "type": "integer",
                            "description": "Cents this row is ranked on — impact, or given on the donors view"
                          },
                          "impressions": {
                            "type": "integer"
                          },
                          "gifts": {
                            "type": "integer"
                          },
                          "badges": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "slug": {
                                  "type": "string"
                                },
                                "artKey": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "artUrl": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "tier": {
                                  "type": "string"
                                },
                                "title": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            }
                          },
                          "badgeCount": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "qualified": {
                      "type": "integer",
                      "description": "Everyone on this view, loaded or not"
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "standing": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "rank": {
                          "type": "integer"
                        },
                        "tied": {
                          "type": "boolean"
                        },
                        "score": {
                          "type": "integer",
                          "description": "Cents the caller is ranked on in this view"
                        },
                        "impact": {
                          "type": "integer"
                        },
                        "raised": {
                          "type": "integer"
                        },
                        "given": {
                          "type": "integer"
                        },
                        "driven": {
                          "type": "integer"
                        },
                        "gap": {
                          "type": "integer",
                          "description": "Cents of score to the person one rank up; 0 at the top or when only recency separates them"
                        },
                        "above": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "period": {
                      "type": "string",
                      "enum": [
                        "all",
                        "30d",
                        "7d"
                      ]
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "all",
                        "creators",
                        "ambassadors",
                        "donors"
                      ]
                    },
                    "metric": {
                      "type": "string",
                      "enum": [
                        "impact",
                        "given"
                      ],
                      "description": "What this view ranks on"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "query": {
                      "type": "string",
                      "nullable": true,
                      "description": "The `q` that was applied, after trimming; null when none"
                    },
                    "executionTimeMs": {
                      "type": "integer"
                    },
                    "cached": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/donors/{kind}/{key}": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Public donor page",
        "operationId": "getDonorProfile",
        "description": "The public page behind a guest or anonymous donor row on the leaderboard.\n\nA donor identity is addressed ONLY by an opaque public key of 20 hex characters. The key is\none-way: no email, no identity id and no account reference ever leaves the server, and the\nguest key and the anonymous key of one person cannot be related to each other.\n\n`given` and `gifts` cover every settled gift of the identity — the same figures the\nleaderboard row shows — including gifts to campaigns that are not public. The `campaigns`\nlist is public campaigns only, and `campaignsHidden` counts the gifts the list cannot show.\n\nAn anonymous identity is never named: `name` is null and `anonymousKey` carries the key, from\nwhich the client derives the same localized alias the donor wall shows.\n\nNo authentication: the page is viewer-independent. Money is in integer cents.\n",
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "guest",
                "anon"
              ]
            }
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{20}$"
            },
            "description": "The opaque public key from a leaderboard row or a search result."
          }
        ],
        "responses": {
          "200": {
            "description": "The donor page",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "guest",
                        "anon"
                      ]
                    },
                    "key": {
                      "type": "string",
                      "description": "The key, echoed. Never the identity behind it"
                    },
                    "name": {
                      "type": "string",
                      "nullable": true,
                      "description": "The guest's own name; always null for an anonymous identity"
                    },
                    "anonymousKey": {
                      "type": "string",
                      "nullable": true,
                      "description": "The key again for an anonymous identity, so the client can derive the alias and the avatar tone"
                    },
                    "given": {
                      "type": "integer",
                      "description": "Minor units this person paid: donation plus tip, over every settled gift"
                    },
                    "gifts": {
                      "type": "integer"
                    },
                    "currency": {
                      "type": "string",
                      "nullable": true,
                      "description": "ISO 4217 for every figure on the page, from the most recent gift; null when the rows carried none"
                    },
                    "firstGiftAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "lastGiftAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "rank": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Place on the all-time Everyone leaderboard; null when it could not be computed"
                    },
                    "score": {
                      "type": "integer",
                      "description": "Cents that rank is on"
                    },
                    "href": {
                      "type": "string",
                      "description": "The page's own path"
                    },
                    "campaigns": {
                      "type": "array",
                      "description": "Public campaigns this person gave to, most recent gift first, at most 24.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "coverImage": {
                            "type": "string",
                            "nullable": true
                          },
                          "isProject": {
                            "type": "boolean",
                            "description": "Projects live at /p/<slug>, campaigns at /f/<slug>"
                          },
                          "givenCents": {
                            "type": "integer"
                          },
                          "gifts": {
                            "type": "integer"
                          },
                          "lastGiftAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "campaignsHidden": {
                      "type": "integer",
                      "description": "Settled gifts to campaigns that are not public"
                    },
                    "hasMoreCampaigns": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such donor. Returned for an unknown kind, a malformed key, a key that resolves to nobody and an identity with no settled gift alike — the endpoint does not distinguish them, so it cannot be used to discover which keys are real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/donor/me/summary": {
      "get": {
        "tags": [
          "Donors"
        ],
        "summary": "Get donor summary",
        "operationId": "getDonorSummary",
        "description": "Aggregate giving stats for the authenticated donor, anonymous gifts included. `lifetimeAmountCents` counts paid gifts only (no refunds or failures); the counts include every gift that left `pending`. A bare object.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Donor summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lifetimeAmountCents": {
                      "type": "integer",
                      "description": "Gross donation amount in cents, tips excluded."
                    },
                    "donationCount": {
                      "type": "integer"
                    },
                    "supportedFundraisers": {
                      "type": "integer"
                    },
                    "lastDonationAt": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/donor/me/donations": {
      "get": {
        "tags": [
          "Donors"
        ],
        "summary": "Get donor donation history",
        "operationId": "getDonorDonations",
        "description": "Paginated giving history for the authenticated donor with optional filters.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "paid",
                "refunded",
                "failed",
                "pending"
              ]
            }
          },
          {
            "name": "fundraiserId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Donor donations, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "fundraiser_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "fundraiser_title": {
                            "type": "string",
                            "nullable": true
                          },
                          "fundraiser_slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "amount_cents": {
                            "type": "integer"
                          },
                          "tip_amount_cents": {
                            "type": "integer"
                          },
                          "fee_amount_cents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "payment_status": {
                            "type": "string",
                            "enum": [
                              "paid",
                              "refunded",
                              "failed",
                              "pending"
                            ]
                          },
                          "payment_intent_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_anonymous": {
                            "type": "boolean"
                          },
                          "comment": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "totalPages": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/donor/me/annual-statement": {
      "get": {
        "tags": [
          "Donors"
        ],
        "summary": "Download annual giving statement",
        "operationId": "getDonorAnnualStatement",
        "description": "Generates a year-end giving summary for the authenticated donor as PDF or CSV (binary download). Includes only paid donations in the requested calendar year. Not a tax receipt.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "required": true,
            "description": "Calendar year. Between 2026 and the current year.",
            "schema": {
              "type": "integer",
              "minimum": 2026
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "pdf",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Generated statement file",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/search": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Full-text search",
        "operationId": "fullTextSearch",
        "description": "Search across campaigns, organizations, users and donors. Queries shorter than two characters return an empty result set rather than an error. Authentication is optional and does not change the result set. Donor rows cover guest donors (matched on the name given at checkout — never on an email) and anonymous identities (matched on the localized alias the donor wall shows for that identity), and they link to `/d/{kind}/{key}`.\nTwo limits apply to donor rows specifically. They are returned on the FIRST PAGE ONLY (`offset=0`); the donor ranking is a single highest-given-first list with no stable second page, so a later offset would repeat the same rows rather than continue them. And a guest-donor name match needs three consecutive alphanumeric characters somewhere in the query — `An`, `a_n` and `___` match no guest donor. Anonymous alias matching is unaffected and keeps the two-character minimum. Campaigns, organizations and users page normally and have no such floor.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Search query. Fewer than 2 characters returns an empty result set."
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "campaigns",
                "users",
                "orgs",
                "donors"
              ],
              "default": "all"
            },
            "description": "Limit to a specific resource type."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Pages campaigns. Donor rows are returned only at `offset=0` — see the description above."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            },
            "description": "Language an anonymous donor's alias is matched and cached in. Defaults from Accept-Language."
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          }
        }
      }
    },
    "/suggest": {
      "get": {
        "tags": [
          "Search"
        ],
        "summary": "Autocomplete suggestions",
        "operationId": "autocompleteSuggestions",
        "description": "Typeahead suggestions drawn from the titles of active, public campaigns: a prefix of the original title or of any translated title matches, and the suggestion is the title in the reader's language (`lang`, the language cookie, then `Accept-Language`). Queries shorter than two characters return an empty list.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "executionTimeMs": {
                      "type": "integer"
                    },
                    "cached": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/payouts/earnings": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get user earnings",
        "operationId": "getUserEarnings",
        "description": "Returns the authenticated creator's earnings summary in integer CENTS (#1499) — every field is suffixed `_cents` and must be divided by 100 before display. The target is always derived from the authenticated principal — any `?userId` query param is ignored (#1072). `total_cents` and `fees_cents` are lifetime aggregates from the canonical ledger. For creators with an active Stripe Connect account, `available_cents`, `pending_cents` and `withdrawn_cents` come from Stripe directly (`pending_cents` additionally includes platform-held net funds that haven't transferred yet). Creators without a Connect account see `available_cents` and `withdrawn_cents` as 0 and `pending_cents` equal to `total_cents` (everything sits on FundlyHub's platform balance awaiting onboarding). Rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Earnings summary (all values are integer cents)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_cents": {
                      "type": "integer",
                      "description": "Lifetime gross raised (canonical ledger figure), in cents."
                    },
                    "pending_cents": {
                      "type": "integer",
                      "description": "Funds not yet available for withdrawal, including platform-held net funds, in cents."
                    },
                    "available_cents": {
                      "type": "integer",
                      "description": "Balance available to withdraw now (from Stripe), in cents."
                    },
                    "withdrawn_cents": {
                      "type": "integer",
                      "description": "Funds already paid out toward the creator's bank (Stripe payouts in paid/in_transit), in cents."
                    },
                    "ready_to_pay_out_cents": {
                      "type": "integer",
                      "description": "The part of `pending_cents` that has settled at Stripe and is eligible for release now, in cents. 0 for creators without a connected account."
                    },
                    "fees_cents": {
                      "type": "integer",
                      "description": "Lifetime platform + processing fees, in cents."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/payouts/earnings/pending-breakdown": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get pending earnings breakdown",
        "operationId": "getPendingBreakdown",
        "description": "Per-donation attribution of the creator's not-yet-available money, lazy-loaded by the Earnings tab's Pending-tile dialog (#1131). The target is always the authenticated principal. Each donation is classified into one of three states: `destination_charge_pending` (Stripe holds it for this creator, card settlement pending), `platform_held_unsettled` (platform balance, awaiting settlement), or `platform_held_settled` (platform balance, eligible for release now). Rows already transferred to the creator's Stripe balance, settled destination charges, and rows under review are excluded. Rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pending breakdown (all amounts in cents)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pending_breakdown": {
                      "type": "object",
                      "properties": {
                        "has_payout_account": {
                          "type": "boolean",
                          "description": "Whether the creator has a Stripe Connect account on file. False means everything below is sitting on the platform balance awaiting onboarding."
                        },
                        "total_cents": {
                          "type": "integer"
                        },
                        "destination_charge_cents": {
                          "type": "integer"
                        },
                        "platform_held_unsettled_cents": {
                          "type": "integer"
                        },
                        "platform_held_settled_cents": {
                          "type": "integer"
                        },
                        "donations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "donation_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "amount_cents": {
                                "type": "integer"
                              },
                              "fundraiser_title": {
                                "type": "string",
                                "nullable": true
                              },
                              "fundraiser_slug": {
                                "type": "string",
                                "nullable": true
                              },
                              "state": {
                                "type": "string",
                                "enum": [
                                  "destination_charge_pending",
                                  "platform_held_unsettled",
                                  "platform_held_settled"
                                ]
                              },
                              "available_on": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "donated_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/accounts": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List Stripe connected accounts",
        "operationId": "getStripeAccounts",
        "description": "Returns the authenticated user's Stripe Connect accounts. Refreshes each account's enabled/onboarding flags from the Stripe API on read (transient downgrades are suppressed unless Stripe reports a concrete requirement). Returns an empty array if the user has no connected account. Rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Connected accounts (array; empty if none)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "stripe_account_id": {
                        "type": "string"
                      },
                      "charges_enabled": {
                        "type": "boolean"
                      },
                      "payouts_enabled": {
                        "type": "boolean"
                      },
                      "details_submitted": {
                        "type": "boolean"
                      },
                      "onboarding_complete": {
                        "type": "boolean"
                      },
                      "country": {
                        "type": "string"
                      },
                      "default_currency": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "requirements": {
                        "type": "object",
                        "nullable": true,
                        "description": "Stripe account requirements (currently_due / past_due / disabled_reason)."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/accounts": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Start or resume Stripe Connect onboarding",
        "operationId": "createConnectedAccount",
        "description": "Creates the caller's Stripe Connect account if they do not have one, then returns a **fresh onboarding link** in either case. Send the creator to `onboardingUrl`; the link is single-use and short-lived, so call this again rather than caching it.\nIf the stored account no longer exists at Stripe, a new account is created transparently. Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "businessType": {
                    "type": "string",
                    "default": "individual",
                    "description": "Passed through to Stripe when the account is created."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account id plus a fresh onboarding link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accountId": {
                      "type": "string",
                      "example": "acct_1AbCdEfGhIjKlMnO"
                    },
                    "onboardingUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "status": {
                      "type": "string",
                      "description": "Always `pending` — read the real state from `GET /stripe/accounts`."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/accounts/{accountId}/status": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get connected-account status",
        "operationId": "getConnectedAccountStatus",
        "description": "Reads one connected account straight from Stripe and refreshes the cached flags on FundlyHub's side. The account is addressed by its Stripe id, which you get from `GET /stripe/accounts`.\n**Ownership:** `accountId` must be a connected account the caller owns: their own account, or an organization's account where the caller holds `org.read_payouts` in that organization. Any other id, including one that does not exist, returns `404`; the response does not distinguish the two.\nThis is a pure read: it never moves money. Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "accountId",
            "in": "path",
            "required": true,
            "description": "Stripe connected-account id.",
            "schema": {
              "type": "string",
              "example": "acct_1AbCdEfGhIjKlMnO"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "complete",
                        "pending"
                      ],
                      "description": "`complete` only when charges and payouts are both enabled."
                    },
                    "chargesEnabled": {
                      "type": "boolean"
                    },
                    "payoutsEnabled": {
                      "type": "boolean"
                    },
                    "detailsSubmitted": {
                      "type": "boolean"
                    },
                    "requirements": {
                      "type": "object",
                      "nullable": true,
                      "description": "Stripe's requirements object (currently_due / past_due / disabled_reason)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No connected account with this id is owned by the caller (it does not exist, or it belongs to someone else).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/sessions": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Create an embedded-components account session",
        "operationId": "createAccountSession",
        "description": "Mints a Stripe Account Session client secret for the embedded Connect components (`account_onboarding`, `payouts`, `payments`). Omit `accountId` to use the caller's most recent connected account.\n**Ownership:** an explicit `accountId` must be a connected account the caller owns: their own account, or an organization's account where the caller holds `org.manage_payouts` in that organization. Any other id, including one that does not exist, returns `404` and no session is created.\nRequires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "accountId": {
                    "type": "string",
                    "description": "Defaults to the caller's most recently created connected account. When set, it must be an account the caller owns (see Ownership above)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account session",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "clientSecret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`accountId` was omitted and the caller has no connected account, or `accountId` names an account the caller does not own (or one that does not exist).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/transfers": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List transfers to the creator's Stripe balance",
        "operationId": "listConnectTransfers",
        "description": "Platform → creator transfers recorded for the authenticated caller, newest first. These are movements onto the creator's Stripe balance; payouts to their bank are a separate list (`GET /stripe/connect/payouts`). Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transfer rows (bare array)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "stripe_transfer_id": {
                        "type": "string"
                      },
                      "donation_id": {
                        "type": "string",
                        "format": "uuid",
                        "nullable": true
                      },
                      "fundraiser_id": {
                        "type": "string",
                        "format": "uuid",
                        "nullable": true
                      },
                      "amount_cents": {
                        "type": "integer"
                      },
                      "application_fee_cents": {
                        "type": "integer"
                      },
                      "currency": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "pending",
                          "paid",
                          "failed",
                          "reversed"
                        ]
                      },
                      "failure_reason": {
                        "type": "string",
                        "nullable": true
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "arrived_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/payouts": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List payouts to the creator's bank",
        "operationId": "listConnectPayouts",
        "description": "Payouts from the creator's Stripe balance to their bank account. Read live from Stripe so the list matches what the creator sees in their bank; if Stripe is unreachable the endpoint falls back to FundlyHub's own records, which only cover payouts this API initiated or that a webhook recorded.\n`arrival_date` and `created` are **Unix timestamps in seconds**, not ISO strings. Stripe paginates by cursor, so `offset` only applies to the database fallback. Requires a bearer session; rate limited to 100 requests per minute per IP.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Only honoured by the database fallback path.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payout rows (bare array)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "example": "po_1AbCdEfGhIjKlMnO"
                      },
                      "amount": {
                        "type": "integer",
                        "description": "Amount in cents."
                      },
                      "currency": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string",
                        "description": "Stripe payout status (`paid`, `pending`, `in_transit`, `failed`, `canceled`)."
                      },
                      "arrival_date": {
                        "type": "integer",
                        "nullable": true,
                        "description": "Unix timestamp (seconds)."
                      },
                      "created": {
                        "type": "integer",
                        "description": "Unix timestamp (seconds)."
                      },
                      "destination": {
                        "type": "string",
                        "nullable": true,
                        "description": "Stripe external-account id. Always null on the database fallback path."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Withdraw available balance to the bank",
        "operationId": "createConnectPayout",
        "description": "Initiates a standard payout from the caller's connected Stripe balance to their bank account. Omit `amountCents` to withdraw the full available balance.\nThe minimum payout is **$1.00 (100 cents)** and the amount may not exceed the available balance. The caller must have a payout-enabled connected account. Gated by the `features.payouts` flag; requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amountCents": {
                    "type": "integer",
                    "minimum": 100,
                    "description": "Defaults to the entire available balance."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payout initiated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payoutId": {
                      "type": "string",
                      "example": "po_1AbCdEfGhIjKlMnO"
                    },
                    "amount": {
                      "type": "integer",
                      "description": "Amount in cents."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "arrivalDate": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No payout-enabled account, zero available balance, amount below the $1.00 minimum, amount above the available balance, or Stripe rejected the payout (its message and `code` are returned).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Payouts are disabled (`features.payouts` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/payouts/reconcile": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Refresh payouts and withdrawals from Stripe",
        "operationId": "reconcileConnectPayouts",
        "description": "Re-reads every payout of the caller's connected Stripe account and repairs FundlyHub's own records of them: the payout rows, the link between each payout and the donations it carried, and the payout entries in the financial ledger. These are what a campaign's Updates feed (`GET /projects/{fundraiserId}/updates`) lists as withdrawals, so a payout Stripe made on its automatic schedule shows up there without waiting for a webhook. Moves no money. Never a dry run.\nOmit `accountId` to refresh the caller's own account. An `acct_…` id of an organization account the caller can manage payouts for is also accepted; any other id answers 404, like an unknown one.\nRequires a bearer session; rate limited to 100 requests per minute per account, and to one refresh per account per minute (429 with `Retry-After`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "accountId": {
                    "type": "string",
                    "example": "acct_1AbCdEfGhIjKlMnO",
                    "description": "Defaults to the caller's own connected account."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refresh result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accountId": {
                      "type": "string",
                      "example": "acct_1AbCdEfGhIjKlMnO"
                    },
                    "payoutsFound": {
                      "type": "integer",
                      "description": "Payouts Stripe lists for the account."
                    },
                    "payoutRowsInserted": {
                      "type": "integer",
                      "description": "Payouts FundlyHub had no record of until now."
                    },
                    "payoutRowsUpdated": {
                      "type": "integer",
                      "description": "Payouts whose recorded status or arrival date changed."
                    },
                    "donationsStamped": {
                      "type": "integer",
                      "description": "Donations newly linked to the payout that carried them."
                    },
                    "donationsCleared": {
                      "type": "integer",
                      "description": "Donations unlinked from a failed or canceled payout."
                    },
                    "balanceTransactionsBackfilled": {
                      "type": "integer",
                      "description": "Donations whose Stripe balance transaction was traced so they could be linked."
                    },
                    "ledgerRowsWritten": {
                      "type": "integer",
                      "description": "Payout entries written to the financial ledger."
                    },
                    "errors": {
                      "type": "integer",
                      "description": "Steps that failed and will be retried by the next refresh."
                    },
                    "payouts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "po_1AbCdEfGhIjKlMnO"
                          },
                          "status": {
                            "type": "string",
                            "description": "Stripe payout status (`paid`, `pending`, `in_transit`, `failed`, `canceled`)."
                          },
                          "amount": {
                            "type": "integer",
                            "description": "Amount in cents."
                          },
                          "currency": {
                            "type": "string"
                          },
                          "row": {
                            "type": "string",
                            "enum": [
                              "inserted",
                              "updated",
                              "unchanged"
                            ]
                          },
                          "donationsStamped": {
                            "type": "integer"
                          },
                          "ledgerRowsWritten": {
                            "type": "integer"
                          },
                          "ok": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller has no connected account, or `accountId` is not one they manage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, or this account was refreshed less than a minute ago.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/stripe/connect/payout-settings": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get payout methods and schedule",
        "operationId": "getPayoutSettings",
        "description": "The bank accounts and debit cards attached to the caller's connected account, plus the payout schedule Stripe currently applies. Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Payout methods and schedule",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payoutMethods": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "bank_account",
                              "card"
                            ]
                          },
                          "bankName": {
                            "type": "string",
                            "nullable": true
                          },
                          "last4": {
                            "type": "string"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "country": {
                            "type": "string"
                          },
                          "routingNumber": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string"
                          },
                          "defaultForCurrency": {
                            "type": "boolean"
                          },
                          "brand": {
                            "type": "string",
                            "nullable": true,
                            "description": "Card brand. Only present on `card` entries."
                          }
                        }
                      }
                    },
                    "schedule": {
                      "$ref": "#/components/schemas/PayoutSchedule"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller has no connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/payout-schedule": {
      "put": {
        "tags": [
          "Payouts"
        ],
        "summary": "Update the payout schedule",
        "operationId": "updatePayoutSchedule",
        "description": "Sets how often Stripe pays the creator out. `weekly` requires `weeklyAnchor` (a weekday name), `monthly` requires `monthlyAnchor` (1–31); `manual` and `daily` take neither. Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "interval"
                ],
                "properties": {
                  "interval": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "daily",
                      "weekly",
                      "monthly"
                    ]
                  },
                  "weeklyAnchor": {
                    "type": "string",
                    "example": "monday",
                    "description": "Required when `interval` is `weekly`."
                  },
                  "monthlyAnchor": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 31,
                    "description": "Required when `interval` is `monthly`."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The schedule Stripe now applies",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutSchedule"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `interval`, a missing anchor, or Stripe rejected the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller has no connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stripe/connect/express-login": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Get a Stripe dashboard link",
        "operationId": "getExpressLoginLink",
        "description": "Returns a URL into Stripe for the caller's connected account — a single-use Express login link deep-linked to payouts for Express accounts, or the Connect dashboard URL for other account types. Do not cache it. Requires a bearer session; rate limited to 100 requests per minute per account.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Stripe refused to mint a login link for this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller has no connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{fundraiserId}/milestones": {
      "get": {
        "tags": [
          "Milestones"
        ],
        "summary": "List milestones",
        "operationId": "listMilestones",
        "description": "Get all milestones for a fundraiser, earliest due date first. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay the stored translation of title and description for this language, when one exists.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of milestones",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "milestones": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Milestone"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Milestones"
        ],
        "summary": "Create milestone",
        "operationId": "createMilestone",
        "description": "Add a milestone to a campaign. Only the campaign owner may post one, the caller's email must be verified, and the endpoint is gated by the `features.milestones` flag.\n**The request body is camelCase** (`targetAmountCents`, `dueDate`) while the response row is snake_case (`target_amount_cents`, `due_date`). Both are in CENTS; `target_amount_cents` is also accepted in the body. The dollars-era `targetAmount` is not, and is ignored.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "targetAmountCents": {
                    "type": "number",
                    "nullable": true,
                    "description": "Optional. When present it must be greater than 0."
                  },
                  "dueDate": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Milestone created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "400": {
            "description": "Title missing, or `targetAmountCents` is not a positive integer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the campaign owner, email not verified, or milestones are disabled (`features.milestones` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/milestones/{id}": {
      "get": {
        "tags": [
          "Milestones"
        ],
        "summary": "Get milestone",
        "operationId": "getMilestone",
        "description": "A single milestone by id. Public when its campaign is: a milestone of a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but the campaign's owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay the stored translation of title and description for this language, when one exists.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Milestone details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Milestones"
        ],
        "summary": "Update milestone",
        "operationId": "updateMilestone",
        "description": "Partial update of a milestone. Only the owner of the parent campaign may update it, and the caller's email must be verified. Send only the fields you are changing; an empty body answers `400`.\n**The request body is camelCase** (`targetAmountCents`, `dueDate`, `completedAt`) while the response row is snake_case. Setting `completedAt` is what marks a milestone complete.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true
                  },
                  "targetAmountCents": {
                    "type": "number",
                    "nullable": true
                  },
                  "dueDate": {
                    "type": "string",
                    "format": "date",
                    "nullable": true
                  },
                  "completedAt": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Milestone updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Milestone"
                }
              }
            }
          },
          "400": {
            "description": "No fields to update, empty title, or non-positive `targetAmountCents`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner of the parent campaign, or email not verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Milestones"
        ],
        "summary": "Delete milestone",
        "operationId": "deleteMilestone",
        "description": "Deletes a milestone. Only the owner of the parent campaign may delete it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner of the parent campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/payments/create-intent": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create payment intent",
        "operationId": "createPaymentIntent",
        "description": "Create a Stripe PaymentIntent for a donation. This is the first half of\nthe donation flow; confirm it with `POST /payments/confirm` once Stripe\nreports the payment succeeded.\n\n**Every field is snake_case, and every amount is in integer cents.**\n`amount_cents` is the donation itself; `tip_amount_cents` is the\noptional tip to FundlyHub. The charge Stripe sees is\n`amount_cents + tip_amount_cents`, and it must clear Stripe's $0.50\n(50 cent) minimum.\n\n`amount_cents` was called `amount`. The value was always cents — only\nthe name has changed, so that it says so.\n\n**`fundraiser_id` decides where the money goes.** Omit it and the\nPaymentIntent is created against the platform account with no campaign\nattribution — the donation is not credited to any campaign and no\ntransfer to a creator is scheduled. It is optional in the sense that\nthe request succeeds without it, not in the sense that it is safe to\nleave out.\n\nThe campaign must be `active` and not deleted; anything else is\nrejected (`404` if the id is unknown, `409` if the campaign exists but\ncannot accept money).\n\nAuthentication is optional — a signed-in donor gets the donation linked\nto their account, an anonymous one does not. reCAPTCHA is enforced\n(see below), except that a signed-in session whose email is verified\nmay omit the token. While donations are switched off platform-wide the\nrequest is refused with `403` and no PaymentIntent is created.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "x-recaptcha-token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "reCAPTCHA v3 token, action `payment`. Required unless supplied as `recaptcha_token` in the body, or the caller is a signed-in session with a verified email. Missing (guest or unverified) → `400`; failing the score threshold → `403`, whoever sent it."
          },
          {
            "name": "x-app-attest-key-id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "iOS only. The App Attest key id (standard base64, as `DCAppAttestService.generateKey` returns it) of a key registered with `POST /app-attest/attest`. Send with `x-app-attest-assertion`."
          },
          {
            "name": "x-app-attest-assertion",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "iOS only. base64 of the assertion from `generateAssertion(keyId, clientDataHash: SHA256(raw request body))`. The body must carry a fresh `app_attest_challenge`. A valid assertion replaces the reCAPTCHA token; the headers alone never do. An invalid one answers `403` with `code` `APP_ATTEST_INVALID`, `APP_ATTEST_KEY_UNKNOWN` (attest a new key) or `APP_ATTEST_CHALLENGE_INVALID` (fetch a new challenge), unless the request passes reCAPTCHA some other way."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount_cents"
                ],
                "properties": {
                  "amount_cents": {
                    "type": "integer",
                    "description": "Donation amount in integer cents. Must be a whole number — a fractional value is rejected with `400`. Renamed from `amount`; the value was always cents, the name now says so.",
                    "example": 5000
                  },
                  "currency": {
                    "type": "string",
                    "default": "usd",
                    "example": "usd"
                  },
                  "fundraiser_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The campaign to credit. See the note above — omitting this silently sends the money to the platform balance."
                  },
                  "tip_amount_cents": {
                    "type": "integer",
                    "default": 0,
                    "description": "Optional tip to FundlyHub, in integer cents.",
                    "example": 500
                  },
                  "tip_percent": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "How the tip was expressed, when it was chosen as a percentage rather than typed as an amount. Recorded only so an abandoned-donation reminder can restore the widget the way the donor left it; it does not affect the charge. Anything that is not a whole number in 0–100 is stored as null and read as \"typed\"."
                  },
                  "donor_email": {
                    "type": "string",
                    "format": "email"
                  },
                  "donor_name": {
                    "type": "string"
                  },
                  "is_anonymous": {
                    "type": "boolean",
                    "default": false
                  },
                  "donor_timezone": {
                    "type": "string",
                    "description": "IANA zone, e.g. `America/New_York`. Used to time reminder emails.",
                    "example": "America/New_York"
                  },
                  "donor_locale": {
                    "type": "string",
                    "description": "BCP-47 tag used to localise reminder emails.",
                    "example": "en"
                  },
                  "recaptcha_token": {
                    "type": "string",
                    "description": "Alternative to the `x-recaptcha-token` header."
                  },
                  "app_attest_challenge": {
                    "type": "string",
                    "description": "iOS App Attest only: a challenge from `POST /app-attest/challenge`, single use. Required when the App Attest headers are sent."
                  }
                }
              },
              "examples": {
                "campaignDonation": {
                  "summary": "$50 donation with a $5 tip",
                  "value": {
                    "amount_cents": 5000,
                    "currency": "usd",
                    "fundraiser_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tip_amount_cents": 500,
                    "tip_percent": 10,
                    "donor_email": "donor@example.com",
                    "donor_name": "Alex Rivera",
                    "is_anonymous": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PaymentIntent created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "client_secret": {
                      "type": "string",
                      "description": "Pass to Stripe.js to collect the payment."
                    },
                    "payment_intent_id": {
                      "type": "string",
                      "description": "Hand this back to `POST /payments/confirm`.",
                      "example": "pi_3abc123def456"
                    },
                    "amount_cents": {
                      "type": "integer",
                      "description": "Total charged, in cents (`amount_cents` + `tip_amount_cents`).",
                      "example": 5500
                    },
                    "stripe_fee_cents": {
                      "type": "integer",
                      "description": "Processing fee in cents (2.9% + 30¢).",
                      "example": 190
                    },
                    "net_amount_cents": {
                      "type": "integer",
                      "description": "What reaches the creator, in cents.",
                      "example": 5310
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Amount is not whole cents, is below Stripe's 50-cent minimum, or the fee would consume the whole donation; or the reCAPTCHA token is missing on a request that is not from a verified, signed-in session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Donations are switched off (`features.donations` is off; the body carries `feature_key`), reCAPTCHA verification failed, or an App Attest assertion was invalid (`code` `APP_ATTEST_*`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`fundraiser_id` does not match any campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The campaign exists but cannot accept money — it is draft, paused, ended, pending or rejected review, or deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/app-attest/challenge": {
      "post": {
        "tags": [
          "App Attest"
        ],
        "summary": "Issue an App Attest challenge",
        "operationId": "createAppAttestChallenge",
        "description": "A random, single-use challenge for the iOS app, valid for\n`expires_in_seconds` (300). Fetch one before attesting a key and one\nbefore every donation request that carries an App Attest assertion.\n\nPublic (guests call it). Rate limited to 30 a minute per address.\nAnswers `503` with `code: APP_ATTEST_DISABLED` while App Attest is not\nconfigured on the server.\n",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "A fresh challenge (`Cache-Control: no-store`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "challenge",
                    "expires_in_seconds"
                  ],
                  "properties": {
                    "challenge": {
                      "type": "string",
                      "description": "base64url (no padding) of 32 random bytes — 43 characters. Use it verbatim.",
                      "example": "q7uE3fP9yK2sV1xW4zA6bC8dE0fG2hJ4kL6mN8pR0tU"
                    },
                    "expires_in_seconds": {
                      "type": "integer",
                      "example": 300
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "App Attest is not enabled (`code: APP_ATTEST_DISABLED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/app-attest/attest": {
      "post": {
        "tags": [
          "App Attest"
        ],
        "summary": "Register an App Attest key",
        "operationId": "attestAppAttestKey",
        "description": "Register a device key with its attestation. The server verifies the\ncertificate chain to Apple's App Attestation Root CA, that the\nattestation was made for `challenge`\n(`clientDataHash = SHA256(UTF-8 bytes of challenge)`), the App ID, the\nenvironment, a zero counter and that `key_id` is the key's hash, then\nstores the public key. Attest once per key; attesting a key that is\nalready registered changes nothing.\n\nPublic (guests call it). Rate limited to 10 a minute per address.\n",
        "security": [
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "key_id",
                  "attestation",
                  "challenge"
                ],
                "properties": {
                  "key_id": {
                    "type": "string",
                    "description": "The key id from `DCAppAttestService.generateKey` (standard base64)."
                  },
                  "attestation": {
                    "type": "string",
                    "description": "base64 of the attestation object from `attestKey`."
                  },
                  "challenge": {
                    "type": "string",
                    "description": "An unused challenge from `POST /app-attest/challenge`, exactly as received."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Key verified and registered."
          },
          "400": {
            "description": "Invalid input or attestation (`code: APP_ATTEST_INVALID`, `reason` names the failed check), or the challenge is unknown, expired or used (`code: APP_ATTEST_CHALLENGE_INVALID`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "App Attest is not enabled (`code: APP_ATTEST_DISABLED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/payments/confirm": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Confirm payment",
        "operationId": "confirmPayment",
        "description": "Confirm a PaymentIntent that Stripe has reported as `succeeded`, and\nsettle the donation record.\n\nThe field is `payment_intent_id` (snake_case) — the value returned as\n`payment_intent_id` by `POST /payments/create-intent`.\n\nSafe to retry: a PaymentIntent whose donation is already `paid` is\nreturned as-is rather than double-counted. Stripe webhooks reconcile the\nsame donation independently, so a client that never reaches this call\nstill ends up with a settled donation — calling it just makes the\nreceipt available immediately.\n",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "payment_intent_id"
                ],
                "properties": {
                  "payment_intent_id": {
                    "type": "string",
                    "example": "pi_3abc123def456"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment confirmed; the settled donation is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Donation"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "campaign_title": {
                              "type": "string"
                            },
                            "campaign_slug": {
                              "type": "string"
                            },
                            "card_brand": {
                              "type": "string",
                              "nullable": true
                            },
                            "card_last4": {
                              "type": "string",
                              "nullable": true
                            },
                            "payment_method_type": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`payment_intent_id` missing, or the PaymentIntent has not succeeded yet (its Stripe status is echoed in the message).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ai/enhance-text": {
      "post": {
        "tags": [
          "AI"
        ],
        "summary": "Enhance text",
        "operationId": "enhanceText",
        "description": "Rewrite or generate one campaign field with an LLM. Requires a bearer session and is gated by the `features.ai_text_enhancement` flag. Throughput is capped per user (10 requests per minute by default).\n`context.field` selects the system prompt and must be one of `summary`, `story` or `milestone`; any other value falls back to the `summary` prompt. `text` is capped at the server's maximum length.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action",
                  "text",
                  "context"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "generate",
                      "refine",
                      "expand",
                      "shorten"
                    ],
                    "description": "An unrecognised action answers `400`."
                  },
                  "text": {
                    "type": "string",
                    "description": "The current text, or the starting idea when `action` is `generate`."
                  },
                  "context": {
                    "type": "object",
                    "required": [
                      "field"
                    ],
                    "description": "Campaign context folded into the prompt. `field` is required; the rest are optional hints.",
                    "properties": {
                      "field": {
                        "type": "string",
                        "enum": [
                          "summary",
                          "story",
                          "milestone"
                        ]
                      },
                      "title": {
                        "type": "string"
                      },
                      "category": {
                        "type": "string"
                      },
                      "goalAmount": {
                        "type": "number"
                      },
                      "beneficiaryName": {
                        "type": "string"
                      },
                      "milestoneTitle": {
                        "type": "string"
                      },
                      "milestoneAmount": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enhanced text",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enhancedText": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `action`, `text` or `context.field`; unknown action; or text too long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "AI text enhancement is disabled (`features.ai_text_enhancement` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The AI provider is not configured or is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ai/campaign-chat": {
      "post": {
        "tags": [
          "AI"
        ],
        "summary": "Campaign AI chat",
        "operationId": "campaignAiChat",
        "description": "Conversational assistant for the campaign-creation wizard. Requires a\nbearer session; throughput is capped per user (10 requests per minute\nby default).\n\n**The response is a Server-Sent Events stream** (`text/event-stream`),\nnot a JSON document. Each event carries one JSON object:\n\n- `{\"type\":\"token\",\"content\":\"…\"}` — one streamed text token\n- `{\"type\":\"campaign_data\",\"data\":{…}}` — campaign fields the model extracted\n- `{\"type\":\"done\"}` — stream complete\n- `{\"type\":\"error\",\"message\":\"…\"}` — the stream failed mid-flight\n\nOnly the most recent 30 messages of `messages` are sent to the model.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "messages"
                ],
                "properties": {
                  "messages": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "role",
                        "content"
                      ],
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "user",
                            "assistant"
                          ]
                        },
                        "content": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "extractedData": {
                    "type": "object",
                    "description": "Fields the user has already filled in, so the assistant does not ask for them again.",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Server-Sent Events stream of tokens and extracted campaign data.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`messages` missing or empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The AI provider is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ai/detect-category": {
      "post": {
        "tags": [
          "AI"
        ],
        "summary": "Detect category",
        "operationId": "detectCategory",
        "description": "Classifies a campaign into one of the platform's categories. Requires a bearer session; throughput is capped per user (10 requests per minute by default).\nSend any combination of `title`, `summary` and `description` — they are joined, truncated to 500 characters and classified. At least one must be present and the joined text must be 3 characters or longer.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Detected category",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categoryName": {
                      "type": "string",
                      "description": "The matched category name. If the model answers with something outside the catalogue, its raw answer is returned instead — match it against `GET /categories` before relying on it."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No usable text supplied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The AI provider is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/capabilities": {
      "get": {
        "tags": [
          "RBAC"
        ],
        "summary": "Get current user capabilities",
        "operationId": "getCurrentCapabilities",
        "description": "Scope-aware permissions and roles for the authenticated user. Requires a bearer session and nothing else — this is the endpoint a client should use to decide what to show. The answer always includes the caller's global roles; with an `organization` or `fundraiser` scope it adds the roles held in that scope. A bare object, not wrapped in `data`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "scopeType",
            "in": "query",
            "required": false,
            "description": "Defaults to `global`. `scopeId` is required for the other two.",
            "schema": {
              "type": "string",
              "enum": [
                "global",
                "organization",
                "fundraiser"
              ],
              "default": "global"
            }
          },
          {
            "name": "scopeId",
            "in": "query",
            "required": false,
            "description": "The organization or fundraiser id. Required unless `scopeType` is `global`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User capabilities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "permissions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "scope": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string"
                        },
                        "id": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "fetchedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When this answer should be considered stale and fetched again."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/stats": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get platform statistics",
        "operationId": "getPlatformStats",
        "description": "Public endpoint returning aggregate platform stats. A bare object, not wrapped in `data`.",
        "responses": {
          "200": {
            "description": "Platform statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalRaisedCents": {
                      "type": "integer",
                      "description": "Platform-wide GROSS funds raised over paid donations, in integer CENTS, tips excluded.",
                      "example": 1234560000
                    },
                    "activeCampaigns": {
                      "type": "integer"
                    },
                    "totalSupporters": {
                      "type": "integer",
                      "description": "Distinct signed-in donors with a paid gift. Guest and anonymous-only donors are not counted."
                    },
                    "totalCreators": {
                      "type": "integer",
                      "description": "Distinct owners of any non-deleted campaign, in any status."
                    },
                    "totalGoalCents": {
                      "type": "integer",
                      "description": "Sum of the goals of every non-deleted campaign, in any status, in integer CENTS.",
                      "example": 5000000000
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/trust/status": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get trust center status",
        "operationId": "getTrustStatus",
        "description": "Live security and compliance status for the Trust Center. Public; cached for up to 15 minutes.",
        "responses": {
          "200": {
            "description": "Trust status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "overallStatus": {
                          "type": "string",
                          "enum": [
                            "operational",
                            "degraded",
                            "outage"
                          ]
                        },
                        "lastChecked": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "controls": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              },
                              "icon": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "active",
                                  "degraded",
                                  "inactive"
                                ]
                              },
                              "lastChecked": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "details": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The status could not be assembled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/location/zip/{zipCode}": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "ZIP code lookup",
        "operationId": "lookupZipCode",
        "description": "Look up city and state from a 5-digit US ZIP code. Public; answers are cached.",
        "parameters": [
          {
            "name": "zipCode",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "95630"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Location data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "zipCode": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "stateAbbreviation": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The ZIP code is not exactly 5 digits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/notifications": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "List notifications",
        "operationId": "getNotifications",
        "description": "Returns the authenticated user's notifications, newest first, together with the unread count. The count always excludes archived rows, even when `archived=true` is requested.\nThere is no offset pagination here — raise `limit` (max 100) to see further back.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "archived",
            "in": "query",
            "required": false,
            "description": "Pass \"true\" to return archived notifications instead of the active ones. Any other value returns the active ones.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Notifications payload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notifications": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Notification"
                      }
                    },
                    "unreadCount": {
                      "type": "integer",
                      "description": "Unread, non-archived notifications for this user."
                    },
                    "updatesUnreadCount": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Unread campaign updates for this user — the same number as `unread_count` on `GET /me/campaign-updates`. The Activity badge is `unreadCount + updatesUnreadCount`, from this one request (`limit=1` keeps it cheap to poll). `null` when that count could not be computed this time: keep the previous value rather than showing zero."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "delete": {
        "tags": [
          "Notifications"
        ],
        "summary": "Delete notifications",
        "operationId": "deleteNotifications",
        "description": "Permanently deletes the given notifications for the authenticated user.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/notifications/mark-read": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark notifications as read",
        "operationId": "markNotificationsRead",
        "description": "Marks the given notification ids as read for the authenticated user.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/notifications/read-all": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark all notifications as read",
        "operationId": "markAllNotificationsRead",
        "description": "Marks all of the authenticated user's notifications as read.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Update result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/notifications/{id}/read": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark one notification as read",
        "operationId": "markOneNotificationRead",
        "description": "Marks a single notification as read for the authenticated user.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Update result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/notifications/archive": {
      "patch": {
        "tags": [
          "Notifications"
        ],
        "summary": "Archive notifications",
        "operationId": "archiveNotifications",
        "description": "Archives the given notification ids for the authenticated user.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Archive result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "archived": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/campaign-updates": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "List my campaign updates",
        "operationId": "getMyCampaignUpdates",
        "description": "Updates posted on campaigns the authenticated user is connected to, newest first, each with its read state, plus the unread count. Read state is kept on the server, so it is the same on every device.\nA campaign is included when the user follows its holder — the person running a personal campaign, or the organization an org campaign belongs to — and the campaign is public and active or ended; or when the user has a paid gift to it and it is active, ended or paused and not private (an unlisted campaign a donor holds the link to is included). The user's own campaigns and own updates are never included, nor deleted campaigns or withdrawn updates.\nAn update posted before the user started following or first gave is returned with `is_read: true` and never counts as unread.\nPaged by an opaque keyset cursor: pass `next_cursor` back as `cursor` until it is `null`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` of the previous page. A cursor this API did not mint is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay translations into this locale, as `GET /projects/{fundraiserId}/updates` does.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CampaignUpdateFeedItem"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true
                    },
                    "unread_count": {
                      "type": "integer",
                      "description": "Unread updates across the whole feed, not just this page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/campaign-updates/read-all": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark all my campaign updates as read",
        "operationId": "markAllMyCampaignUpdatesRead",
        "description": "Marks every unread update in the user's feed read. With `until`, only updates posted at or before that instant (inclusive to the millisecond), so clearing the list a client is showing does not also clear an update that arrived after it was fetched.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Typically the `created_at` of the newest update the client is showing."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update result and the new unread count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "integer",
                      "description": "How many updates this call marked read."
                    },
                    "unread_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/me/campaign-updates/{updateId}/read": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Mark one campaign update as read",
        "operationId": "markMyCampaignUpdateRead",
        "description": "Marks one update read for the authenticated user. Idempotent: an update already read answers `updated: 0` and keeps its first `read_at`. Any existing, non-withdrawn update may be marked, whether or not it is in the user's feed, so a client can mark an update read wherever it was opened. There is no \"mark unread\", as for notifications.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Update result and the new unread count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "integer",
                      "enum": [
                        0,
                        1
                      ]
                    },
                    "unread_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such update, or it was withdrawn",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscriptions/status": {
      "get": {
        "tags": [
          "Social"
        ],
        "summary": "Check follow status",
        "operationId": "getSubscriptionStatus",
        "description": "Returns whether `followerId` currently follows `followingId`. A signed-in caller may ask about any pair — follow edges are public.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "followerId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "followingId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "followingType",
            "in": "query",
            "required": false,
            "description": "Defaults to \"user\".",
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "organization"
              ],
              "default": "user"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Follow status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "isFollowing": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/subscriptions": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "Follow a user or organization",
        "operationId": "createSubscription",
        "description": "Creates a follow edge from the authenticated caller to following_id. The follower is always the authenticated user and cannot be supplied by the client. Gated by the `features.user_follow_user` flag.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "following_id"
                ],
                "properties": {
                  "following_id": {
                    "type": "string"
                  },
                  "following_type": {
                    "type": "string",
                    "enum": [
                      "user",
                      "organization"
                    ],
                    "default": "user"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Follow created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing following_id, invalid following_type, or cannot follow yourself",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Following is disabled (`features.user_follow_user` is off — the one flag gates organization follows too).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No user or organization with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/subscriptions/{followerId}/{followingId}/{followingType}": {
      "delete": {
        "tags": [
          "Social"
        ],
        "summary": "Unfollow a user or organization",
        "operationId": "deleteSubscription",
        "description": "Removes a follow edge. The path followerId must match the authenticated caller.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "followerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "followingId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "followingType",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "organization"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Follow removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/users/{id}/followers": {
      "get": {
        "tags": [
          "Social"
        ],
        "summary": "List a user's followers",
        "operationId": "getFollowers",
        "description": "Returns the users that follow the given user. Emails are never exposed.\nA PRIVATE PROFILE ANSWERS AN EMPTY ARRAY to anyone but its owner (#1696). This list publishes other people's names, avatars, handles and counts, one row per follow, so a private profile's social graph is not enumerable through it. The COUNTS remain public — they ride on the profile payload, which carries real figures for a private profile. 200 with `[]`, not 404: the person exists, their graph is closed.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Follower list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "avatar": {
                        "type": "string",
                        "nullable": true
                      },
                      "email": {
                        "type": "string",
                        "nullable": true,
                        "description": "Always null."
                      },
                      "role": {
                        "type": "string"
                      },
                      "profile_slug": {
                        "type": "string",
                        "nullable": true
                      },
                      "follower_count": {
                        "type": "integer"
                      },
                      "campaign_count": {
                        "type": "integer"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/users/{id}/following": {
      "get": {
        "tags": [
          "Social"
        ],
        "summary": "List who a user is following",
        "operationId": "getFollowing",
        "description": "Returns the entities the given user follows. Emails are never exposed.\nA PRIVATE PROFILE ANSWERS AN EMPTY ARRAY to anyone but its owner (#1696). This list publishes other people's names, avatars, handles and counts, one row per follow, so a private profile's social graph is not enumerable through it. The COUNTS remain public — they ride on the profile payload, which carries real figures for a private profile. 200 with `[]`, not 404: the person exists, their graph is closed.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Following list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "avatar": {
                        "type": "string",
                        "nullable": true
                      },
                      "email": {
                        "type": "string",
                        "nullable": true,
                        "description": "Always null."
                      },
                      "role": {
                        "type": "string"
                      },
                      "profile_slug": {
                        "type": "string",
                        "nullable": true
                      },
                      "follower_count": {
                        "type": "integer"
                      },
                      "campaign_count": {
                        "type": "integer"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{fundraiserId}/comments": {
      "get": {
        "tags": [
          "Comments"
        ],
        "summary": "List comments for a fundraiser",
        "operationId": "getCommentsByFundraiser",
        "description": "Returns paginated comments for a fundraiser, newest first. Hidden and retracted comments are left out. A comment written as a donor's note from their receipt carries its gift (`donation_amount_cents`, `donation_currency`); on an anonymous gift the author fields are null and `is_anonymous` is true. Every row carries `gif`, a GIF object or null; a GIF comment's `content` may be empty.\nAuthentication is optional. Every row carries `like_count`; for a signed-in caller `liked_by_me` says whether they like it, and for a guest it is always `false`.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "fundraiser_id": {
                            "type": "string"
                          },
                          "content": {
                            "type": "string",
                            "description": "May be empty when `gif` is set."
                          },
                          "gif": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Gif"
                              }
                            ],
                            "nullable": true,
                            "description": "The GIF on this comment, or null (#1963)."
                          },
                          "parent_comment_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "author_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "author_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "author_avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_anonymous": {
                            "type": "boolean"
                          },
                          "donation_amount_cents": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The gift behind a donor note, net of the Stripe fee, in cents. Null on an ordinary comment."
                          },
                          "donation_currency": {
                            "type": "string",
                            "nullable": true
                          },
                          "like_count": {
                            "type": "integer",
                            "minimum": 0,
                            "description": "Accounts that like this comment (#1833)."
                          },
                          "liked_by_me": {
                            "type": "boolean",
                            "description": "Whether the caller likes it; always false for a guest (#1833)."
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Comments"
        ],
        "summary": "Create a comment",
        "operationId": "createComment",
        "description": "Creates a comment (or reply) on a fundraiser. The author is derived from the authenticated user. Content must be 2000 characters or less, and is required unless `gif_id` is set.\n`gif_id` attaches a GIF (#1963): a GIPHY id from `GET /gifs/trending` or `GET /gifs/search`. The server looks it up on GIPHY and stores the GIF object it builds itself; clients never send URLs. Only `g` and `pg` GIFs are accepted. A request with `gif_id` also counts against the GIF rate limit (60 a minute per account).\nRequires a verified email address, and is gated by the `features.comments` flag — either gate failing answers `403`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Required unless `gif_id` is set; may then be empty or whitespace."
                  },
                  "parent_comment_id": {
                    "type": "string",
                    "nullable": true
                  },
                  "gif_id": {
                    "type": "string",
                    "nullable": true,
                    "pattern": "^[A-Za-z0-9]{6,40}$",
                    "description": "A GIPHY GIF id (#1963). Replies take one too."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Comment created — the stored row (including `gif`, a GIF object or null) plus `author_name` and `author_avatar`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid fundraiser ID, missing/oversized content, malformed `gif_id` (`Invalid gif_id`), or parent comment not in this fundraiser",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email not verified, or comments are disabled (`features.comments` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`{ \"error\": \"gif_not_found\" }` — GIPHY has no such GIF; `{ \"error\": \"gif_not_allowed\" }` — it is rated above `pg`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`{ \"error\": \"gifs_upstream\" }` — GIPHY could not be reached while resolving `gif_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`{ \"error\": \"gifs_unavailable\" }` — `gif_id` was sent while GIF comments are off (`features.comment_gifs`) or not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gifs/trending": {
      "get": {
        "tags": [
          "Comments"
        ],
        "summary": "Trending GIFs for the comment GIF picker",
        "operationId": "getTrendingGifs",
        "description": "GIPHY's trending GIFs, rated `pg` or lower, as GIF objects. Cached on the server for a few minutes. Page with `offset` = the previous answer's `next_offset`.\nAuthentication is optional. Rate limited at 60 requests a minute per account (per IP for a guest), on top of the public limit.\nAnswers `503 { \"error\": \"gifs_unavailable\" }` while GIF comments are off (`features.comment_gifs`) or GIPHY is not configured. Clients should hide their GIF button when they get it.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 24,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0,
              "maximum": 4999
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of GIFs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GifPage"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`{ \"error\": \"gifs_upstream\" }` — GIPHY failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`{ \"error\": \"gifs_unavailable\" }` — GIF comments are off or not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gifs/search": {
      "get": {
        "tags": [
          "Comments"
        ],
        "summary": "Search GIFs for the comment GIF picker",
        "operationId": "searchGifs",
        "description": "GIPHY search, rated `pg` or lower, as GIF objects. Cached on the server by `q`, `offset`, `limit` and `lang`. Same limits and `503` behaviour as `GET /gifs/trending`.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The search term. Whitespace is collapsed and it is cut to 50 characters.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 24,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0,
              "maximum": 4999
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "The search language. Anything else is treated as `en`.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of GIFs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GifPage"
                }
              }
            }
          },
          "400": {
            "description": "`{ \"error\": \"invalid_query\" }` — `q` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`{ \"error\": \"gifs_upstream\" }` — GIPHY failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`{ \"error\": \"gifs_unavailable\" }` — GIF comments are off or not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comments/{id}": {
      "delete": {
        "tags": [
          "Comments"
        ],
        "summary": "Delete a comment",
        "operationId": "deleteComment",
        "description": "Deletes a comment. Only the comment's author may delete it. A donor's note tied to a gift is retracted (hidden, kept for the record) rather than deleted.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comment deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Comment not found or not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/comments/{commentId}/like": {
      "put": {
        "tags": [
          "Comments"
        ],
        "summary": "Like a comment",
        "operationId": "likeComment",
        "description": "Likes a comment or a reply as the authenticated user. Idempotent: liking a comment you already like changes nothing and answers the same state. No verified email is required. Gated by the `features.comments` flag and the per-account rate limit.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liked; the comment's count after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikeState"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Comments are disabled (`features.comments` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such comment, or it is hidden, retracted, or on a campaign that is not publicly readable — one answer for all of them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Comments"
        ],
        "summary": "Unlike a comment",
        "operationId": "unlikeComment",
        "description": "Removes the authenticated user's like from a comment or a reply. Idempotent: unliking a comment you do not like changes nothing.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Not liked; the comment's count after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikeState"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Comments are disabled (`features.comments` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such comment, or it is hidden, retracted, or on a campaign that is not publicly readable — one answer for all of them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/projects/{fundraiserId}/updates": {
      "get": {
        "tags": [
          "Updates"
        ],
        "summary": "Get the project updates feed",
        "operationId": "getProjectUpdates",
        "description": "Returns a unified feed of the fundraiser's updates and its withdrawals, newest first — a bare array. A withdrawal row is this campaign's share of a payout that carried its donations (never the payout's whole amount); paid, in-transit and pending payouts are listed. Withdrawn updates are left out. An unreadable feed answers `200` with an empty array rather than an error.\nReadable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner.\nAuthentication is optional. Every `update` item carries `like_count`; for a signed-in caller `liked_by_me` says whether they like it, and for a guest it is always `false`. Withdrawal items carry neither.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay update translations into this language, when they exist.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Update feed (items are type \"update\" or \"withdrawal\")",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "update",
                          "withdrawal"
                        ]
                      },
                      "id": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "title": {
                        "type": "string",
                        "nullable": true,
                        "description": "Updates only."
                      },
                      "body": {
                        "type": "string",
                        "description": "Updates only."
                      },
                      "author": {
                        "type": "object",
                        "description": "Updates only.",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      },
                      "amount_cents": {
                        "type": "integer",
                        "description": "Withdrawals only — this campaign's share, in cents."
                      },
                      "currency": {
                        "type": "string",
                        "description": "Withdrawals only."
                      },
                      "status": {
                        "type": "string",
                        "description": "`active` on an update; `paid`, `in_transit` or `pending` on a withdrawal."
                      },
                      "arrival_date": {
                        "type": "string",
                        "nullable": true,
                        "description": "Withdrawals only."
                      },
                      "like_count": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Accounts that like this update; update items only (#1833)."
                      },
                      "liked_by_me": {
                        "type": "boolean",
                        "description": "Whether the caller likes it; always false for a guest; update items only (#1833)."
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Updates"
        ],
        "summary": "Post a project update",
        "operationId": "createProjectUpdate",
        "description": "Creates a project update on a fundraiser. Only the fundraiser owner may post, the caller's email must be verified, and the endpoint is gated by the `features.project_updates` flag. The `body` field is required (at most 20,000 characters; `title` at most 200). The update's language is detected and machine translations are queued.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 20000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Update created — a bare object, not wrapped in `data`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "fundraiser_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "author_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    },
                    "body": {
                      "type": "string"
                    },
                    "source_language": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "update"
                      ]
                    },
                    "author": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "avatar": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Update body is missing, or `title` is not a string",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the fundraiser owner, email not verified, or updates are disabled (`features.project_updates` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "`title` or `body` is over its length limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{fundraiserId}/updates/{updateId}": {
      "patch": {
        "tags": [
          "Updates"
        ],
        "summary": "Edit a project update",
        "operationId": "editProjectUpdate",
        "description": "Changes an update's title and/or body. Campaign owner only; the caller's email must be verified, and the endpoint is gated by the `features.project_updates` flag. Send at least one of `title` and `body`; `title: null` (or blank) removes the title.\nWhen the words change, the update's language is detected again, its machine translations are dropped and re-queued, and hand-made translations are flagged as outdated. A withdrawn update answers `404`. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "Must not be blank when sent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The edited update (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "fundraiser_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "author_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    },
                    "body": {
                      "type": "string"
                    },
                    "source_language": {
                      "type": "string",
                      "nullable": true
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "update"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither field sent, a blank `body`, or a non-string `title`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the campaign owner, email not verified, or `features.project_updates` is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Campaign or update not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`title` over 200 or `body` over 20,000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Updates"
        ],
        "summary": "Delete a project update",
        "operationId": "deleteProjectUpdate",
        "description": "Soft-deletes a project update. Only the fundraiser owner may delete it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Update deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "deletedId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not authorized to delete updates from this fundraiser",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Fundraiser or update not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{fundraiserId}/updates/{updateId}/like": {
      "put": {
        "tags": [
          "Updates"
        ],
        "summary": "Like a project update",
        "operationId": "likeProjectUpdate",
        "description": "Likes a campaign update as the authenticated user. Idempotent: liking an update you already like changes nothing and answers the same state. No verified email is required; the per-account rate limit applies.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Liked; the update's count after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikeState"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such update under this campaign, or it was deleted, or the campaign is not publicly readable — one answer for all of them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Updates"
        ],
        "summary": "Unlike a project update",
        "operationId": "unlikeProjectUpdate",
        "description": "Removes the authenticated user's like from a campaign update. Idempotent: unliking an update you do not like changes nothing.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Not liked; the update's count after the write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikeState"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such update under this campaign, or it was deleted, or the campaign is not publicly readable — one answer for all of them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/projects/{fundraiserId}/milestones": {
      "get": {
        "tags": [
          "Updates"
        ],
        "summary": "List project milestones",
        "operationId": "getProjectMilestones",
        "description": "Returns all milestones for a fundraiser, ordered by due date then creation date — a bare array, unlike `GET /fundraisers/{fundraiserId}/milestones`, which wraps the same rows in `milestones`. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Overlay the stored translation of title and description for this language, when one exists.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Milestone list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Milestone"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/projects/{fundraiserId}/stats": {
      "get": {
        "tags": [
          "Updates"
        ],
        "summary": "Get project funding stats",
        "operationId": "getProjectStats",
        "description": "Returns allocation, disbursement, and milestone-count stats for a project-type fundraiser. Non-project fundraisers return zeroed stats. **Not available yet for project fundraisers:** the endpoint currently answers `500` for them. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "totalAllocated": {
                      "type": "number"
                    },
                    "totalDisbursed": {
                      "type": "number"
                    },
                    "unallocated": {
                      "type": "number"
                    },
                    "milestones": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        }
                      },
                      "additionalProperties": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "Project funding stats are not available yet (every project fundraiser, today).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/shares": {
      "post": {
        "tags": [
          "Shares"
        ],
        "summary": "Track a share event",
        "operationId": "trackShareEvent",
        "description": "Records a social-share event for a fundraiser or profile. Authentication is optional — the sharing user is recorded when a session is present, otherwise the row is anonymous.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "entity_type",
                  "entity_id",
                  "platform"
                ],
                "properties": {
                  "entity_type": {
                    "type": "string",
                    "enum": [
                      "fundraiser",
                      "profile"
                    ]
                  },
                  "entity_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "copy_link",
                      "facebook",
                      "story",
                      "whatsapp",
                      "instagram",
                      "text",
                      "messenger",
                      "linkedin",
                      "email",
                      "x",
                      "native",
                      "twitter",
                      "sms"
                    ],
                    "description": "The share button's id. `twitter` and `sms` are accepted as the old names of `x` and `text`, and counted under them."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Share tracked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid share payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-role-titles": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "List organization role titles",
        "operationId": "listOrgRoleTitles",
        "description": "Public reference list of non-deprecated position titles (the LinkedIn-style \"position\" picker on the org-admin Members page), ordered by `sort_order` then `label`. These titles are display-only and carry no permissions — they are distinct from the RBAC roles `org_owner` / `org_admin` / `org_viewer`. Pass an `id` from this list as `org_role_title_id` when adding a member.",
        "responses": {
          "200": {
            "description": "Role titles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgRoleTitle"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/overview": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Get organization dashboard totals",
        "operationId": "getOrgAdminOverview",
        "description": "Headline numbers for the org-admin dashboard. For a conglomerate the figures roll up the organization **and all of its child organizations**; for a company they cover the organization alone. `totalRaisedCents` sums every `paid` donation on those organizations' fundraisers, in integer cents. `uniqueDonors` counts distinct signed-in donors only — guest donations do not add to it.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Dashboard totals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "activeFundraisers": {
                          "type": "integer",
                          "description": "Non-deleted fundraisers with status `active`."
                        },
                        "totalFundraisers": {
                          "type": "integer",
                          "description": "All non-deleted fundraisers."
                        },
                        "totalRaisedCents": {
                          "type": "integer",
                          "description": "Lifetime paid donations, in integer CENTS.",
                          "example": 4560000
                        },
                        "uniqueDonors": {
                          "type": "integer"
                        },
                        "kind": {
                          "type": "string",
                          "enum": [
                            "company",
                            "conglomerate"
                          ]
                        },
                        "childrenCount": {
                          "type": "integer",
                          "description": "Number of child organizations (always 0 for a company)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/fundraisers": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List the organization's fundraisers",
        "operationId": "listOrgAdminFundraisers",
        "description": "Non-deleted fundraisers owned by this organization (not its children), newest first, with offset pagination. Every status is included — drafts, pending review, ended — unless `status` narrows it. Private contact fields are never returned.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only fundraisers with this status. Send one of the listed values.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "pending",
                "rejected",
                "active",
                "paused",
                "ended"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated fundraisers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminFundraiser"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/OrgAdminPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/donations": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List donations to the organization's fundraisers",
        "operationId": "listOrgAdminDonations",
        "description": "Donations to this organization's non-deleted fundraisers, newest first, with offset pagination. Every payment status is returned unless `status` narrows it.\n\nDonor privacy: `donor_display_name` is `Anonymous donor` when the gift was marked anonymous, the donor's profile name for a signed-in donor, and the checkout name for a guest. Donor email addresses are never returned.\n\n**Requires permission:** `org.read_donations`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only donations with this payment status. Send one of the listed values.",
            "schema": {
              "type": "string",
              "enum": [
                "paid",
                "pending",
                "failed",
                "refunded"
              ]
            }
          },
          {
            "name": "fundraiser_id",
            "in": "query",
            "required": false,
            "description": "Only donations to this fundraiser. Must be a UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated donations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminDonation"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/OrgAdminPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/donors": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List the organization's donors",
        "operationId": "listOrgAdminDonors",
        "description": "Distinct donors who made a `paid` donation to any of this organization's non-deleted fundraisers, with their lifetime totals here, largest first, with offset pagination. All anonymous gifts are collapsed into a single `Anonymous donor` row (`donor_key: anonymous`) so they cannot be told apart by amount. Signed-in donors are keyed `user:<profile id>`; guest donors are keyed `guest:<24 hex characters>`. Treat `donor_key` as an opaque identifier: it is stable across pages and requests, but carries no contact details and cannot be turned back into one.\n\n**Requires permission:** `org.read_donors`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1..100. Defaults to 20.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated donor roll-up",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminDonor"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/OrgAdminPagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/users/search": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Look up an account by email before adding it as a member",
        "operationId": "searchOrgAdminPlatformUsers",
        "description": "Confirms which FundlyHub account uses an email address, for the Members page's \"Add member\" form. `q` must be a **complete** email address; it is matched case-insensitively against account login emails only, and at most one result comes back, with the address masked. Anything that is not a complete email address (a name, a fragment, a phone number) answers `200` with an empty list. This is not a directory search.\n\nAvailable only to organizations that are approved or verified; any other organization gets `403` with `code: ORG_NOT_APPROVED`.\n\nRate limited to 30 requests per minute per user, in one bucket shared with `POST /org-admin/{slug}/members`.\n\nNote the bare `{ \"results\": [ … ] }` envelope rather than `data`.\n\n**Requires permission:** `org.manage_members`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "A complete email address (surrounding whitespace is trimmed). Matched case-insensitively against account login emails.",
            "schema": {
              "type": "string",
              "format": "email",
              "maxLength": 254
            },
            "example": "sam@example.org"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching account, or an empty list when no account uses that email or `q` is not a complete email address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "maxItems": 1,
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminUserSearchResult"
                      }
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "id": "3f1c2a9e-5b7d-4e1a-9c2b-8d6f0a1b2c3d",
                      "type": "user",
                      "name": "Sam Rivera",
                      "avatar": null,
                      "masked_email": "s***@example.org"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description, or the organization is not approved or verified yet (`code: ORG_NOT_APPROVED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missingPermission": {
                    "summary": "Missing permission",
                    "value": {
                      "error": "Forbidden",
                      "message": "Requires permission: org.manage_members"
                    }
                  },
                  "orgNotApproved": {
                    "summary": "Organization not approved yet",
                    "value": {
                      "error": "Member lookup is available once the organization has been approved.",
                      "code": "ORG_NOT_APPROVED"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 member lookups and adds by this user in the last minute (`Too many member lookups. Please wait a minute and try again.`), or a broader rate limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/members": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List organization members",
        "operationId": "listOrgAdminMembers",
        "description": "Everyone holding an active, unexpired org-scoped role on this organization, highest role first, then by name. Includes each member's email address, which is why this sits behind the management permission rather than `org.read`.\n\n**Requires permission:** `org.manage_members`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Add a member",
        "operationId": "addOrgAdminMember",
        "description": "Adds an **existing** FundlyHub user, found by email (case-insensitive), to the organization with the given role. The membership is active immediately; no invitation is sent and the user does not have to accept. `org_owner` cannot be granted here — add the user first, then promote them with `PATCH /org-admin/{slug}/members/{userId}`.\n\nAn optional display title can be set with **either** `org_role_title_id` (an `id` from `GET /org-role-titles`) **or** `custom_role_title`, not both.\n\nAvailable only to organizations that are approved or verified; any other organization gets `403` with `code: ORG_NOT_APPROVED`.\n\nRate limited to 30 requests per minute per user, in one bucket shared with `GET /org-admin/{slug}/users/search`.\n\n**Requires permission:** `org.manage_members`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "role"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address of an existing FundlyHub account."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "org_admin",
                      "org_viewer"
                    ]
                  },
                  "org_role_title_id": {
                    "type": "string",
                    "description": "An `id` from `GET /org-role-titles`.",
                    "example": "executive_director"
                  },
                  "custom_role_title": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Free-form title, used when nothing in the taxonomy fits."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Member added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "user_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "user_name": {
                          "type": "string",
                          "nullable": true
                        },
                        "role_name": {
                          "type": "string",
                          "enum": [
                            "org_admin",
                            "org_viewer"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `email`; `role` not `org_admin` / `org_viewer`; `custom_role_title` over 100 characters; or both title fields set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description, or the organization is not approved or verified yet (`code: ORG_NOT_APPROVED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missingPermission": {
                    "summary": "Missing permission",
                    "value": {
                      "error": "Forbidden",
                      "message": "Requires permission: org.manage_members"
                    }
                  },
                  "orgNotApproved": {
                    "summary": "Organization not approved yet",
                    "value": {
                      "error": "Members can be added once the organization has been approved.",
                      "code": "ORG_NOT_APPROVED"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or no FundlyHub user has that email (`No FundlyHub user with that email. Ask them to sign up first.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The user already holds a role on this organization. Use `PATCH` to change it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 member lookups and adds by this user in the last minute (`Too many member lookups. Please wait a minute and try again.`), or a broader rate limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/members/{userId}": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Change a member's role",
        "operationId": "updateOrgAdminMemberRole",
        "description": "Moves a member to a different org role. Setting the role they already hold is an idempotent no-op that still answers `200`.\n\nRoles rank `org_owner` > `org_admin` > `org_viewer`. Rules enforced, each a `403` when broken:\n\n  - only an `org_owner` may promote anyone to `org_owner`, or change the role of another owner;\n  - nobody may change the role of a member whose role is equal to or above their own (an\n    `org_admin` manages `org_viewer`s, not other admins);\n  - nobody may grant a role above their own.\n\nLowering your own role (stepping down) is always allowed. The organization's last remaining owner cannot be demoted (`400`) — promote another member to owner first.\n\n**Requires permission:** `org.manage_members`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's profile id. A non-UUID answers `404 Member not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "org_owner",
                      "org_admin",
                      "org_viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Role updated (or already held)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "user_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "role_name": {
                          "type": "string",
                          "enum": [
                            "org_owner",
                            "org_admin",
                            "org_viewer"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid `role`, or the change would demote the only owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks `org.manage_members`; holds no role on this organization; is not an owner and is promoting someone to owner or changing an owner's role; is changing the role of a member ranked equal to or above them; or is granting a role above their own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the user is not a member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Remove a member",
        "operationId": "removeOrgAdminMember",
        "description": "Deactivates every org-scoped role the user holds on this organization. Only an `org_owner` may remove an owner, and nobody may remove a member whose role is equal to or above their own (`403`). Removing yourself (leaving) is always allowed. The organization's last remaining owner cannot be removed (`400`) — promote another member to owner first.\n\n**Requires permission:** `org.manage_members`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's profile id. A non-UUID answers `404 Member not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Member removed"
          },
          "400": {
            "description": "The user is the only owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks `org.manage_members`; holds no role on this organization; or is removing an owner without being one, or a member ranked equal to or above them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Only an organization owner can remove an owner."
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the user is not a member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/settings": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Update organization settings",
        "operationId": "updateOrgAdminSettings",
        "description": "Partial update of the organization's profile. Only the fields below are writable; others are ignored. String fields accept a string or `null` and are trimmed. At least one writable field must be present.\n\n`logo` and `banner_image` take a URL as-is; to upload an image use `POST /org-admin/{slug}/avatar` or `/banner` instead.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "legal_name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 255,
                    "description": "Cannot be set to an empty string."
                  },
                  "website": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1024
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 4000
                  },
                  "country": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 64
                  },
                  "logo": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1024
                  },
                  "banner_image": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1024
                  },
                  "mission": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 4000
                  },
                  "contact_email": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 320,
                    "description": "Must look like an email address when non-empty."
                  },
                  "contact_email_public": {
                    "type": "boolean",
                    "description": "Whether `contact_email` is shown on the public profile. Defaults to false."
                  },
                  "founded_year": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 1800,
                    "description": "Between 1800 and the current year."
                  },
                  "social_links": {
                    "type": "object",
                    "nullable": true,
                    "description": "Replaces the whole set. Keys are limited to the listed platforms; a `null` or empty value drops that platform, and `{}` or `null` clears them all.",
                    "properties": {
                      "twitter": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "linkedin": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "facebook": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "instagram": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "youtube": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "tiktok": {
                        "type": "string",
                        "maxLength": 500
                      }
                    },
                    "additionalProperties": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminSettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field has the wrong type or is too long, `legal_name` is empty, `contact_email` is malformed, `founded_year` is out of range, an unknown `social_links` key was sent, or no writable field was present.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/avatar": {
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Upload organization logo",
        "operationId": "uploadOrgAdminAvatar",
        "description": "Uploads a new logo as base64 JSON. The image is centre-cropped to 256×256, re-encoded as WebP, stored, and written to the organization's `logo` straight away — no separate settings save is needed. Any previous logo file is deleted. Maximum 5 MB decoded.\n\nNote the bare response body (no `data` envelope).\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrgAdminImageUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logo uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "avatar_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`fileBase64` or `contentType` missing, the content type is not allowed, the file is over 5 MB, or it is not a valid image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Remove organization logo",
        "operationId": "removeOrgAdminAvatar",
        "description": "Deletes the stored logo files and clears the organization's `logo`.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "204": {
            "description": "Logo removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/updates": {
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Post an organization update",
        "operationId": "createOrgAdminUpdate",
        "description": "Publishes a post to the organization's public updates feed (`GET /organizations/{id}/updates`). The caller is recorded as the author.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "body"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20000
                  },
                  "cover_image": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 2048,
                    "description": "Image URL. An empty string is stored as null."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Update posted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminUpdate"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`title` or `body` missing, empty, too long or not a string; or `cover_image` invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/updates/{updateId}": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Edit an organization update",
        "operationId": "updateOrgAdminUpdate",
        "description": "Partial edit of one of this organization's updates. Send at least one of `title`, `body`, `cover_image`.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20000
                  },
                  "cover_image": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 2048
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Update edited",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminUpdate"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is invalid, or no editable field was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the update does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Delete an organization update",
        "operationId": "deleteOrgAdminUpdate",
        "description": "Permanently deletes one of this organization's updates. A malformed `updateId` answers `500` rather than `404`.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Update deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the update does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/banner": {
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Upload organization banner",
        "operationId": "uploadOrgAdminBanner",
        "description": "Uploads the public-profile banner as base64 JSON. The image is centre-cropped to 1500×500 (3:1), re-encoded as WebP, stored, and written to the organization's `banner_image` straight away. Any previous banner file is deleted. Maximum 8 MB decoded.\n\nNote the bare response body (no `data` envelope).\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrgAdminImageUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Banner uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "banner_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`fileBase64` or `contentType` missing, the content type is not allowed, the file is over 8 MB, or it is not a valid image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Remove organization banner",
        "operationId": "removeOrgAdminBanner",
        "description": "Deletes the stored banner files and clears the organization's `banner_image`.\n\n**Requires permission:** `org.update_settings`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "204": {
            "description": "Banner removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/dbas": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List DBAs",
        "operationId": "listOrgAdminDbas",
        "description": "The organization's \"doing business as\" names, default first, then alphabetical.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "DBAs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminDba"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Add a DBA",
        "operationId": "addOrgAdminDba",
        "description": "Adds a DBA name. With `is_default: true` it becomes the default and the previous default is cleared in the same transaction.\n\n**Requires permission:** `org.manage_dbas`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "dba_name"
                ],
                "properties": {
                  "dba_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "is_default": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "DBA added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminDba"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`dba_name` missing, blank or over 255 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "409": {
            "description": "The organization already has a DBA with that name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/dbas/{dbaId}": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Update a DBA",
        "operationId": "updateOrgAdminDba",
        "description": "Renames a DBA and/or changes whether it is the default. Setting `is_default: true` clears the previous default in the same transaction.\n\n**Requires permission:** `org.manage_dbas`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "dbaId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 DBA not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dba_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "is_default": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "DBA updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminDba"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`dba_name` blank or too long, or no valid field was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the DBA does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The organization already has a DBA with that name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Delete a DBA",
        "operationId": "deleteOrgAdminDba",
        "description": "Permanently deletes a DBA. Deleting the default leaves the organization with no default DBA.\n\n**Requires permission:** `org.manage_dbas`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "dbaId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 DBA not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "DBA deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the DBA does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/locations": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List office locations",
        "operationId": "listOrgAdminLocations",
        "description": "The organization's physical locations, primary first, then oldest first. Includes locations not shown publicly.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Locations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminLocation"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Add an office location",
        "operationId": "addOrgAdminLocation",
        "description": "Adds a location. With `is_primary: true` it becomes the primary location and the previous primary is cleared in the same transaction. Locations are hidden from the public profile unless `is_publicly_visible` is `true`.\n\n**Requires permission:** `org.manage_locations`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "address"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 120,
                    "example": "Headquarters"
                  },
                  "address": {
                    "$ref": "#/components/schemas/OrgAdminAddress"
                  },
                  "is_primary": {
                    "type": "boolean",
                    "default": false
                  },
                  "is_publicly_visible": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Location added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminLocation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`label` not a string or over 120 characters; `address` missing, not an object, over 16 fields, or a field that is not a string or is over 512 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/locations/{locationId}": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Update an office location",
        "operationId": "updateOrgAdminLocation",
        "description": "Partial update of a location. `address`, when sent, replaces the whole address object. Setting `is_primary: true` clears the previous primary in the same transaction.\n\n**Requires permission:** `org.manage_locations`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 Location not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 120
                  },
                  "address": {
                    "$ref": "#/components/schemas/OrgAdminAddress"
                  },
                  "is_primary": {
                    "type": "boolean"
                  },
                  "is_publicly_visible": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Location updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminLocation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is invalid, or no valid field was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the location does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Delete an office location",
        "operationId": "deleteOrgAdminLocation",
        "description": "Permanently deletes a location. Deleting the primary leaves the organization with no primary location.\n\n**Requires permission:** `org.manage_locations`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 Location not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Location deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the location does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/payouts/connect": {
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Start or resume Stripe Connect onboarding",
        "operationId": "createOrgAdminPayoutsConnect",
        "description": "Creates the organization's Stripe Express account (business type `company`) if it has none, or reuses the existing one, and returns a fresh single-use onboarding link. Safe to call repeatedly — each call returns a new link. If the stored account no longer exists on Stripe a new one is created in its place.\n\nThe organization must have `verification_status: verified`, and the `features.org_level_stripe_connect` flag must be on (`503` otherwise).\n\n**Requires permission:** `org.manage_payouts` (organization owners only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "201": {
            "description": "Onboarding link created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "accountId": {
                          "type": "string",
                          "example": "acct_1Nv0FGQ9RKHgCVdK"
                        },
                        "onboardingUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Stripe-hosted onboarding page. Short-lived; redirect the user straight away."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "existing"
                          ],
                          "description": "`pending` when this call created the account, `existing` when it reused one."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The organization is not verified yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Org-level Stripe Connect is turned off (`features.org_level_stripe_connect`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/payouts/status": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Get Stripe Connect status",
        "operationId": "getOrgAdminPayoutsStatus",
        "description": "Whether the organization can take charges and receive payouts. Reads the account live from Stripe and refreshes the stored flags. If Stripe cannot be reached, the last stored flags are returned instead and the Stripe-only fields (`defaultCurrency`, `country`, `businessType`, `requirementsCurrentlyDue`) are absent. With no account yet the body is `{ \"data\": { \"connected\": false } }`.\n\n**Requires permission:** `org.read_payouts`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Connect status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminPayoutsStatus"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Org-level Stripe Connect is turned off (`features.org_level_stripe_connect`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/children": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List child organizations",
        "operationId": "listOrgAdminChildren",
        "description": "Organizations whose parent is this one, newest first. Always empty for a `company`; only a `conglomerate` has children.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Child organizations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminChildOrganization"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Create a child organization",
        "operationId": "createOrgAdminChild",
        "description": "Creates a new `company` under this conglomerate. Validation is the same as `POST /organizations`, except that the new organization is always a `company` (`kind` is ignored once it passes validation) and sending `parent_organization_id` in the body is rejected — the parent is always the `{slug}` organization. The caller becomes `org_owner` of the child. The child starts with `verification_status: pending`; the first DBA becomes its default and the first location its primary.\n\nRequires a **verified email address**.\n\n**Requires permission:** `org.manage_children` (organization owners only)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "legal_name"
                ],
                "properties": {
                  "legal_name": {
                    "type": "string",
                    "maxLength": 255
                  },
                  "ein": {
                    "type": "string",
                    "pattern": "^\\d{2}-\\d{7}$",
                    "example": "12-3456789"
                  },
                  "country": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "website": {
                    "type": "string",
                    "maxLength": 1024
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 4000
                  },
                  "categories": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    }
                  },
                  "dbas": {
                    "type": "array",
                    "maxItems": 10,
                    "description": "DBA names must be unique (case-insensitive).",
                    "items": {
                      "type": "object",
                      "properties": {
                        "dba_name": {
                          "type": "string",
                          "maxLength": 255
                        }
                      }
                    }
                  },
                  "locations": {
                    "type": "array",
                    "maxItems": 25,
                    "items": {
                      "type": "object",
                      "required": [
                        "address"
                      ],
                      "properties": {
                        "label": {
                          "type": "string",
                          "maxLength": 120
                        },
                        "address": {
                          "$ref": "#/components/schemas/OrgAdminAddress"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Child organization created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrgAdminCreatedChild"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, `parent_organization_id` was sent, or this organization is not a conglomerate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email address not verified, or the caller lacks `org.manage_children`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "409": {
            "description": "Organization already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/documents": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "List verification documents",
        "operationId": "listOrgAdminDocuments",
        "description": "The organization's verification documents in every state except `superseded`, newest first, including review outcome and whether each is shown on the public profile.\n\nFinancial documents — `w9`, `voided_check` and `other` — are listed only when the caller also holds `org.upload_documents`; otherwise they are left out of the list. `ein_letter` and `501c3_determination` are listed with `org.read` alone.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "responses": {
          "200": {
            "description": "Documents",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OrgAdminDocument"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Upload a verification document",
        "operationId": "uploadOrgAdminDocument",
        "description": "Uploads a document for platform review as base64 JSON. It is stored privately and enters the review queue as `pending`. Uploading a `doc_type` the organization already has pending or approved marks the older one `superseded`. Approval of both `ein_letter` and `501c3_determination` is what verifies the organization.\n\nSize limit: 10 MB decoded by default, but the JSON body itself is capped at 10 MB, so in practice a file must stay under about 7.5 MB to fit once base64-encoded — larger bodies get `413` before the handler runs.\n\n**Requires permission:** `org.upload_documents`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "doc_type",
                  "content_type",
                  "file_base64"
                ],
                "properties": {
                  "doc_type": {
                    "type": "string",
                    "enum": [
                      "ein_letter",
                      "501c3_determination",
                      "w9",
                      "voided_check",
                      "other"
                    ]
                  },
                  "content_type": {
                    "type": "string",
                    "enum": [
                      "application/pdf",
                      "image/png",
                      "image/jpeg"
                    ]
                  },
                  "original_filename": {
                    "type": "string",
                    "description": "Shown back in listings and used as the download filename."
                  },
                  "file_base64": {
                    "type": "string",
                    "format": "byte",
                    "description": "The file, base64-encoded (no `data:` prefix)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document uploaded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "org_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "doc_type": {
                          "type": "string"
                        },
                        "original_filename": {
                          "type": "string",
                          "nullable": true
                        },
                        "content_type": {
                          "type": "string"
                        },
                        "size_bytes": {
                          "type": "integer"
                        },
                        "verification_status": {
                          "type": "string",
                          "example": "pending"
                        },
                        "uploaded_by_user_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing, `doc_type` or `content_type` is not allowed, the file is empty or too large, or `original_filename` is not a string.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Organization not found"
                }
              }
            }
          },
          "413": {
            "description": "The JSON body is over the 10 MB request limit."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/documents/{docId}/download": {
      "get": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Download a verification document",
        "operationId": "downloadOrgAdminDocument",
        "description": "Streams the stored file with its original `Content-Type` and, when a filename was recorded, `Content-Disposition: attachment`. A document belonging to a different organization answers `404`.\n\nA `w9`, `voided_check` or `other` document can be downloaded only by a caller who also holds `org.upload_documents`; anyone else gets `404`, as if it did not exist.\n\n**Requires permission:** `org.read`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "docId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 Document not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"<original filename>\"` when one was recorded.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, the document does not belong to it, or it is a `w9`, `voided_check` or `other` document and the caller lacks `org.upload_documents`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/documents/{docId}": {
      "delete": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Delete a pending verification document",
        "operationId": "deleteOrgAdminDocument",
        "description": "Withdraws a document that is still `pending`. Approved and rejected documents are kept for the audit trail and cannot be deleted — they answer `404` like a missing document.\n\n**Requires permission:** `org.upload_documents`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "docId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 Document not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Document deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the document does not exist, belongs to another organization, or is no longer pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/org-admin/{slug}/documents/{docId}/public-visibility": {
      "patch": {
        "tags": [
          "Organization Admin"
        ],
        "summary": "Show or hide a document on the public profile",
        "operationId": "setOrgAdminDocumentPublicVisibility",
        "description": "Sets the document's public-visibility flag. The public profile (`GET /organizations/{id}/documents/public`) lists a document only when it is both flagged public **and** approved, so the flag can be set ahead of review.\n\n**Requires permission:** `org.upload_documents`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The organization's slug (not its UUID).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,254}$"
            },
            "example": "local-food-bank-a1b2c3"
          },
          {
            "name": "docId",
            "in": "path",
            "required": true,
            "description": "A non-UUID answers `404 Document not found`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "is_publicly_visible"
                ],
                "properties": {
                  "is_publicly_visible": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Visibility updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "is_publicly_visible": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`is_publicly_visible` is not a boolean.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller lacks the org-scoped permission named in the description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Forbidden",
                  "message": "Requires permission: org.read"
                }
              }
            }
          },
          "404": {
            "description": "No organization has that slug, or the document does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/locations": {
      "get": {
        "tags": [
          "Organizations"
        ],
        "summary": "List campaign locations by US state",
        "operationId": "listCampaignLocationStates",
        "description": "US states that currently have active campaigns, with a campaign count per state, sorted by state name. The state is parsed out of each campaign's free-text `location` (a two-letter code or full state name); campaigns whose location cannot be matched to a state are left out. Cached for 5 minutes. Drives the location filter on campaign browse.",
        "responses": {
          "200": {
            "description": "States with active campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "example": "CA"
                          },
                          "name": {
                            "type": "string",
                            "example": "California"
                          },
                          "count": {
                            "type": "integer",
                            "description": "Active campaigns in this state."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/organizations/{id}/report": {
      "post": {
        "tags": [
          "Organizations"
        ],
        "summary": "Report an organization",
        "operationId": "reportOrganization",
        "description": "Flags an organization for review by FundlyHub's trust team. A signed-in caller with a **verified email address** can report; no relationship to the organization is needed. Each user holds at most one report per organization — reporting again overwrites the earlier category and details and puts the report back to `pending`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Organization UUID (a slug is not accepted and answers `404`).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "category"
                ],
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "fraud",
                      "impersonation",
                      "spam",
                      "mission_misalignment",
                      "sexual_content",
                      "hate",
                      "other"
                    ]
                  },
                  "details": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 4000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Report recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`category` missing or not in the list, or `details` not a string or over 4000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email address not verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No live organization has that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/translations": {
      "get": {
        "tags": [
          "Translations"
        ],
        "summary": "List a campaign's translations",
        "operationId": "listFundraiserTranslations",
        "description": "The Translations tab's data for one campaign: its source text and language, one row per other supported locale (fields `null` and `source: null` where nothing has been translated yet), the row in the campaign's own language if one exists (`original_row`), and every milestone with its rows.\nOnly the campaign's owner may read it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Source text and per-locale translations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "source": {
                          "type": "object",
                          "properties": {
                            "language": {
                              "type": "string",
                              "enum": [
                                "en",
                                "ru",
                                "uk",
                                "es"
                              ]
                            },
                            "title": {
                              "type": "string"
                            },
                            "summary": {
                              "type": "string",
                              "nullable": true
                            },
                            "story_html": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "translations": {
                          "type": "array",
                          "description": "One entry per supported locale other than the source language.",
                          "items": {
                            "$ref": "#/components/schemas/TranslationFundraiserRow"
                          }
                        },
                        "original_row": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/TranslationFundraiserRow"
                            }
                          ],
                          "nullable": true,
                          "description": "The row in the campaign's own language, if any: a field written in another language machine-translated into it, or a person's version. Readers of that language are served it."
                        },
                        "milestones": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TranslationMilestoneEntry"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/translations/{lang}": {
      "put": {
        "tags": [
          "Translations"
        ],
        "summary": "Save a hand-edited campaign translation",
        "operationId": "upsertFundraiserTranslation",
        "description": "Creates or overwrites the campaign's translation in `lang` with a hand-edited version and marks it `source: \"human\"`, so automatic re-translation leaves it alone. Campaign owner only. Audit-logged.\n`lang` may not be the campaign's stored source language; that text is edited on the campaign itself.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "summary": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 1000
                  },
                  "story_html": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 100000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TranslationFundraiserSavedRow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, `lang` is the campaign's source language, or `title` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "A field is over its limit (title 200, summary 1000, story 100,000 characters). A non-string `summary` or `story_html` also lands here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Translations"
        ],
        "summary": "Discard a hand-edited version in the campaign's own language",
        "operationId": "discardFundraiserSourceLanguageTranslation",
        "description": "Deletes the hand-edited (`source: \"human\"`) rows in the campaign's OWN language, both the campaign's row and its milestones' rows. Such a version exists when someone edited that language while the campaign was labelled with another one; readers of that language are served it instead of the original until it is discarded. Machine rows for fields written in another language are rebuilt in the background afterwards.\nOnly works for `lang` equal to the campaign's stored source language; a translation into another language is replaced with regenerate instead. Campaign owner only. The discarded text is written to the audit log.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What was discarded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "language": {
                          "type": "string",
                          "enum": [
                            "en",
                            "ru",
                            "uk",
                            "es"
                          ]
                        },
                        "discarded": {
                          "type": "object",
                          "properties": {
                            "campaign": {
                              "type": "integer",
                              "description": "Campaign rows deleted (0 or 1)."
                            },
                            "milestones": {
                              "type": "integer",
                              "description": "Milestone rows deleted."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, or `lang` is not the campaign's source language.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign not found, or no hand-edited version exists in this language.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/translations/{lang}/regenerate": {
      "post": {
        "tags": [
          "Translations"
        ],
        "summary": "Regenerate a campaign translation",
        "operationId": "regenerateFundraiserTranslation",
        "description": "Re-runs machine translation of the campaign into `lang` and stores it as `source: \"machine\"`. The locale's milestone rows are refreshed the same way. Campaign owner only. Audit-logged.\nA hand-edited row is not overwritten unless `force=1` (or `true`) is passed; without it the call answers `409`. It also answers `409` when the campaign text or the row changed while the translation was being generated.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "`1` or `true` overwrites a hand-edited row.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The regenerated row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TranslationFundraiserSavedRow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, or `lang` is the campaign's source language.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The row is hand-edited and `force` was not set, or the campaign or the row changed while the translation was generated (reload and retry).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The translation provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No translation provider is configured. The body carries a `code`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranslationMtError"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/milestones/{milestoneId}/translations/{lang}": {
      "put": {
        "tags": [
          "Translations",
          "Milestones"
        ],
        "summary": "Save a hand-edited milestone translation",
        "operationId": "upsertMilestoneTranslation",
        "description": "Creates or overwrites one milestone's translation in `lang` as `source: \"human\"`. The milestone must belong to the campaign in the path. Campaign owner only. Audit-logged. `lang` may not be the campaign's source language.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "milestoneId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Trimmed before saving."
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "milestone_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "language": {
                          "type": "string",
                          "enum": [
                            "en",
                            "ru",
                            "uk",
                            "es"
                          ]
                        },
                        "title": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "human"
                          ]
                        },
                        "translated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, `lang` is the campaign's source language, or `title` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign not found, or the milestone does not belong to it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`title` over 200 or `description` over 2000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/updates/translations": {
      "get": {
        "tags": [
          "Translations",
          "Updates"
        ],
        "summary": "List translations of a campaign's updates",
        "operationId": "listProjectUpdateTranslations",
        "description": "Every live (not withdrawn) update of the campaign, newest first, with its original text and language and one row per other supported locale. Campaign owner only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updates with their translations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "updates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TranslationProjectUpdateEntry"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/updates/{updateId}/translations": {
      "get": {
        "tags": [
          "Translations",
          "Updates"
        ],
        "summary": "Get one update's translations",
        "operationId": "getProjectUpdateTranslations",
        "description": "One update of the campaign: its original and its rows in the other three locales. Campaign owner only. A withdrawn update, or one belonging to another campaign, answers `404`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The update and its translations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "type": "object",
                          "properties": {
                            "fundraiser_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "update_id": {
                              "type": "string",
                              "format": "uuid"
                            }
                          }
                        },
                        {
                          "$ref": "#/components/schemas/TranslationProjectUpdateEntry"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign or update not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/updates/{updateId}/translations/{lang}": {
      "put": {
        "tags": [
          "Translations",
          "Updates"
        ],
        "summary": "Save a hand-edited update translation",
        "operationId": "upsertProjectUpdateTranslation",
        "description": "Creates or overwrites an update's translation in `lang` as `source: \"human\"`, so no automatic pass overwrites it. Campaign owner only. Audit-logged. Refused in the update's own language. When the update has a title, the translation must carry one too.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200,
                    "description": "Required when the update itself has a title. Trimmed; blank is stored as null."
                  },
                  "content": {
                    "type": "string",
                    "maxLength": 20000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TranslationUpdateSavedRow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, `lang` is the update's own language, `content` missing or blank, or `title` missing although the update has one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign or update not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`content` over 20,000 or `title` over 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/updates/{updateId}/translations/{lang}/regenerate": {
      "post": {
        "tags": [
          "Translations",
          "Updates"
        ],
        "summary": "Regenerate an update translation",
        "operationId": "regenerateProjectUpdateTranslation",
        "description": "Re-runs machine translation of one update into `lang` and stores it as `source: \"machine\"`. A hand-edited row is only replaced with `force=1`. Campaign owner only. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "`1` or `true` overwrites a hand-edited row.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The regenerated row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TranslationUpdateSavedRow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, or `lang` is the update's own language.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign or update not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The row is hand-edited and `force` was not set, or the update or the row changed while the translation was generated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The translation provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No translation provider is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TranslationMtError"
                }
              }
            }
          }
        }
      }
    },
    "/updates/{updateId}/translations": {
      "get": {
        "tags": [
          "Translations"
        ],
        "summary": "List a legacy update's translations",
        "operationId": "listLegacyUpdateTranslations",
        "deprecated": true,
        "description": "**Legacy.** Campaign updates are translated through `GET /fundraisers/{id}/updates/{updateId}/translations`; use that instead.\nReturns the update's source text and one row per other locale. The caller must own the parent campaign.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Source and translations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "update_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "source": {
                          "type": "object",
                          "properties": {
                            "language": {
                              "type": "string",
                              "enum": [
                                "en",
                                "ru",
                                "uk",
                                "es"
                              ]
                            },
                            "title": {
                              "type": "string",
                              "nullable": true
                            },
                            "content": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "translations": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TranslationLegacyUpdateRow"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/updates/{updateId}/translations/{lang}": {
      "put": {
        "tags": [
          "Translations"
        ],
        "summary": "Save a legacy update translation",
        "operationId": "upsertLegacyUpdateTranslation",
        "deprecated": true,
        "description": "**Legacy** (see `GET /updates/{updateId}/translations`). Saves a hand-edited translation as `source: \"human\"`. Owner of the parent campaign only. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200
                  },
                  "content": {
                    "type": "string",
                    "maxLength": 100000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TranslationUpdateSavedRow"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported `lang`, `lang` is the update's source language, or `content` missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "description": "`content` over 100,000 or `title` over 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ai/generate-update": {
      "post": {
        "tags": [
          "AI"
        ],
        "summary": "Generate a project update",
        "operationId": "generateProjectUpdateText",
        "description": "Writes or rewrites a campaign update's text with an LLM. Requires a bearer session and is gated by the `features.ai_update_generation` flag. Throughput is capped per user inside the handler (10 requests per minute by default).\n`context.fundraiserTitle` is required. Up to three `context.previousUpdates` are folded into the prompt for `generate` and `improve`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "action",
                  "text",
                  "context"
                ],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "generate",
                      "improve",
                      "expand",
                      "shorten"
                    ],
                    "description": "An unrecognised action answers `400`."
                  },
                  "text": {
                    "type": "string",
                    "description": "The current update, or the idea to write from when `action` is `generate`. Capped at the server's maximum length (10,000 characters by default)."
                  },
                  "context": {
                    "type": "object",
                    "required": [
                      "fundraiserTitle"
                    ],
                    "properties": {
                      "fundraiserTitle": {
                        "type": "string"
                      },
                      "fundraiserId": {
                        "type": "string",
                        "description": "Logged only."
                      },
                      "milestoneTitle": {
                        "type": "string"
                      },
                      "previousUpdates": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated text",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enhancedText": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `action`, `text` or `context.fundraiserTitle`; unknown action; or text too long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "AI update generation is disabled (`features.ai_update_generation` is off).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The model call failed. The body carries `details` with the provider's message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The AI provider is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ai/scrape-campaign-url": {
      "post": {
        "tags": [
          "AI"
        ],
        "summary": "Import a campaign from a URL",
        "operationId": "scrapeCampaignUrl",
        "description": "Reads a campaign page on another platform and extracts its fields with an LLM, for the \"import from link\" step of the campaign builder. Requires a bearer session; throughput is capped per user inside the handler (10 requests per minute by default).\nThe response carries a one-time `scrape_token`, valid for 30 minutes; pass it on `POST /fundraisers` when creating the imported campaign. When the LLM is not configured, only the page title is returned, with a `note` and no token.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "An `http` or `https` URL."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Extracted campaign fields",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "The fields the model could find; any may be absent. Free-form model output; validate it before use.",
                      "properties": {
                        "title": {
                          "type": "string"
                        },
                        "summary": {
                          "type": "string"
                        },
                        "story": {
                          "type": "string"
                        },
                        "goalAmount": {
                          "type": "number",
                          "description": "Dollars."
                        },
                        "categoryName": {
                          "type": "string"
                        },
                        "beneficiaryName": {
                          "type": "string"
                        },
                        "location": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "personal",
                            "charity"
                          ]
                        },
                        "isProject": {
                          "type": "boolean"
                        },
                        "coverImage": {
                          "type": "string"
                        },
                        "galleryImages": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "platform": {
                      "type": "string",
                      "nullable": true,
                      "description": "The source platform, when recognised."
                    },
                    "scrape_token": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "note": {
                      "type": "string",
                      "description": "Present only when the LLM is not configured."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`url` is missing or not a string.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "The page could not be scraped: invalid or non-HTTP URL, scraping not configured, too little content, a timeout, or the scraper's own rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The extraction failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/campaigns/{id}/stats": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get campaign stats (broken)",
        "operationId": "getCampaignStatsLegacy",
        "deprecated": true,
        "description": "**Not available: currently answers `500` for every campaign.** Use `GET /fundraisers/{id}/stats` instead.\nIntended as a public stat-tile read: raised and goal (cents), donor, update and view counts, dates and days left.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Campaign stats (bare object; not currently reachable)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "title": {
                      "type": "string"
                    },
                    "raised": {
                      "type": "string",
                      "description": "Cents, as a numeric string."
                    },
                    "goal": {
                      "type": "string",
                      "description": "Cents, as a numeric string."
                    },
                    "donor_count": {
                      "type": "string"
                    },
                    "update_count": {
                      "type": "string"
                    },
                    "view_count": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "days_left": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "Always, today.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/slug/{slug}/og-image": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Campaign link-preview image",
        "operationId": "getFundraiserOgImage",
        "description": "A 1200×630 PNG Open Graph card for the campaign: title, cover, organizer, raised and goal amounts and donor count. Public; served to social crawlers.\nOnly a campaign anyone may open by link (live, ended or paused; not private; not deleted) has an image. Any other campaign is a `404`, the same as an unknown slug, for every caller.\nThe language comes from `?lang=`, else the locale cookie, else `Accept-Language`, else the original text. Without `?lang=` the response varies on `Accept-Language, Cookie`. Cached for 30 seconds.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language to render in. Without it the locale cookie, then `Accept-Language`, decide.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Rendering failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/slug/{slug}/share-poster": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Campaign square share poster",
        "operationId": "getFundraiserSharePoster",
        "description": "A 1080×1080 PNG for feed posts (Instagram and similar). Same data, language rules, caching and `404` for a campaign that is not readable by link as the link-preview image.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language to render in. Without it the locale cookie, then `Accept-Language`, decide.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Rendering failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/slug/{slug}/share-story": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Campaign story card",
        "operationId": "getFundraiserShareStory",
        "description": "A 1080×1920 PNG for Instagram and Facebook Stories. Same data, language rules, caching and `404` for a campaign that is not readable by link as the link-preview image.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language to render in. Without it the locale cookie, then `Accept-Language`, decide.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Rendering failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/slug/{slug}/share-kit.zip": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Campaign share kit",
        "operationId": "getFundraiserShareKit",
        "description": "The link-preview image, the square poster and the story card in one zip download (`<slug>-link.png`, `<slug>-post.png`, `<slug>-story.png`), as linked from the ambassador endorsement-request email. Same language rules, 30-second caching and `404` for a campaign that is not readable by link as the single images. All three must render, or the call answers `404`.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language to render in. Without it the locale cookie, then `Accept-Language`, decide.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Zip archive, sent as an attachment named `<slug>-share-kit.zip`",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Rendering or packaging failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/share-impact": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Get a campaign's share impact",
        "operationId": "getFundraiserShareImpact",
        "description": "What sharing has done for this campaign, all-time: human visits that arrived through a shared link, how many came through endorsers' links, and the paid, non-self-referred gifts traced back to a share. Public. A campaign nobody has shared returns zeros. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share impact (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "impressions": {
                      "type": "integer",
                      "description": "Human visits through a shared link."
                    },
                    "viaEndorsers": {
                      "type": "integer",
                      "description": "The part of `impressions` that came through endorsers' links."
                    },
                    "gifts": {
                      "type": "integer"
                    },
                    "donors": {
                      "type": "integer"
                    },
                    "value": {
                      "type": "number",
                      "description": "Donation plus tip of those gifts, in DOLLARS (major units)."
                    },
                    "currencies": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Currencies present among those gifts; more than one means `value` is a mixed total."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/share-channels": {
      "get": {
        "tags": [
          "Shares"
        ],
        "summary": "Get a campaign's shares per channel",
        "operationId": "getFundraiserShareChannels",
        "description": "Human clicks through shared links, and the gifts they led to, per share channel, all-time. Every channel is present, with zeros when none. Public. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner. Authentication is optional.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-channel stats (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "$ref": "#/components/schemas/ShareChannelStat"
                    },
                    "channels": {
                      "type": "object",
                      "description": "One entry per channel: `copy_link`, `facebook`, `story`, `whatsapp`, `instagram`, `text`, `messenger`, `linkedin`, `email`, `x`, `native`.",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/ShareChannelStat"
                      }
                    },
                    "unattributed": {
                      "$ref": "#/components/schemas/ShareChannelStat"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/assess": {
      "post": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Run a pre-publish trust assessment",
        "operationId": "assessFundraiser",
        "description": "Runs the full trust assessment the platform applies at publish time, without publishing: profile and campaign blockers, suggestions, a trust score, an AI analysis of the story, and the moderation decision it would lead to. No request body. Campaign owner only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Assessment (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserTrustAssessment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/activity": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get a campaign's lifecycle timeline",
        "operationId": "getFundraiserActivity",
        "description": "Lifecycle events (created, submitted for review, approved, rejected, updated, update posted, goal reached, deleted), newest first, with who acted. Campaign owner only; works for soft-deleted campaigns too.\nActions taken by FundlyHub staff appear with `role: \"admin\"` and null id, name and email.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timeline (unwrapped)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "activity": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FundraiserActivityEntry"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "The timeline could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/report": {
      "post": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Report a campaign",
        "operationId": "reportFundraiser",
        "description": "Flags an active campaign for moderator review. One report per user per campaign: reporting again replaces the earlier reason and details. Requires a verified email address.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reason"
                ],
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 5,
                    "description": "Trimmed; at least 5 characters."
                  },
                  "details": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Report recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`reason` shorter than 5 characters, or the campaign is not `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Email address not verified (`code: EMAIL_NOT_VERIFIED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/contact": {
      "post": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Contact the organizer",
        "operationId": "contactFundraiserOrganizer",
        "description": "Sends a message to the campaign's organizer by email. The organizer receives it with the sender's `email` as the reply address and answers by replying, so the organizer's own address is never revealed unless they reply. Authentication is optional: anyone who can read the campaign may write, with the same visibility rule as `GET /fundraisers/{id}` (a campaign hidden from the caller answers `404`). Limited to five messages an hour per client address and, when signed in, per account. A message with more than three links is refused, and the name may contain none.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "message"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254,
                    "description": "Where the organizer's reply goes. Trimmed."
                  },
                  "message": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "description": "Plain text, trimmed; 1 to 2000 characters, at most three links."
                  },
                  "name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 100,
                    "description": "How the sender signs the message; the email address is shown when it is omitted. Control characters are removed."
                  }
                }
              },
              "example": {
                "email": "jane@example.com",
                "name": "Jane",
                "message": "Hi! Is there a way to help other than donating?"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The message was accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. `details` lists each problem with its field `path` (`email`, `message` or `name`), a readable `message`, and a `code` — `too_many_links` and `link_in_name` for the link rules, otherwise the validator's own code (e.g. `invalid_string`, `too_small`, `too_big`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Validation error"
                    },
                    "details": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "path": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "message": {
                            "type": "string"
                          },
                          "code": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/donor-highlight": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get the featured donor for a campaign",
        "operationId": "getFundraiserDonorHighlight",
        "description": "Feeds the campaign page's \"{name} and N others have donated\" row: one featured donor and up to three faces, chosen from the latest 100 paid, non-anonymous gifts. Authentication is optional and only changes WHO is featured: a signed-in viewer sees people they follow, or who follow them, first. Only public campaigns that are active, paused or ended return donors; anything else returns empty.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Featured donor (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "featured": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/DonorFace"
                        }
                      ],
                      "nullable": true
                    },
                    "avatars": {
                      "type": "array",
                      "maxItems": 3,
                      "items": {
                        "$ref": "#/components/schemas/DonorFace"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/related": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get related campaigns",
        "operationId": "getRelatedFundraisers",
        "description": "Four lists of other people's live, public campaigns for the discovery rail at the bottom of a campaign page: same category (`similar`), same US state (`nearby`), 70–99% funded (`almost`) and newest (`recent`). An empty list means that tab is not shown. Public.\nTitles and summaries are in the reader's language (`?lang=`, then the locale cookie, then `Accept-Language`) when a translation exists; when a language is resolved, each card also carries `translation_source` and, where the text was replaced, `original_title` / `original_summary`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Cards per list, clamped to 1–12.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "default": 12
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The four lists (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "similar": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RelatedCampaign"
                      }
                    },
                    "nearby": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RelatedCampaign"
                      }
                    },
                    "almost": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RelatedCampaign"
                      }
                    },
                    "recent": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RelatedCampaign"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{fundraiserId}/top-donors": {
      "get": {
        "tags": [
          "Donations"
        ],
        "summary": "Get a campaign's top donors",
        "operationId": "getFundraiserTopDonors",
        "description": "The campaign's biggest donors, one row per person across all their paid gifts. Anonymous donors keep their total but not their name; they are identified by an opaque `anonymous_key`. When any donor gave in more than one currency, `meta.mixed_currency` is true and the totals should not be shown. Public.\nOnly a campaign anyone may open by link (live, ended or paused; not private; not deleted) has a public ranking. For any other campaign the answer is an empty `data` array, the same as for an unknown id, unless the caller is signed in as the campaign's owner or an admin.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows to return, clamped to 1–25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top donors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "The donor's key; for anonymous donors the same value as `anonymous_key`."
                          },
                          "anonymous_key": {
                            "type": "string",
                            "nullable": true
                          },
                          "is_anonymous": {
                            "type": "boolean"
                          },
                          "donor_name": {
                            "type": "string",
                            "nullable": true
                          },
                          "donor_avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "total_cents": {
                            "type": "integer"
                          },
                          "donation_count": {
                            "type": "integer"
                          },
                          "last_donation_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "currency": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "mixed_currency": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/outcome-report": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get a campaign's outcome report",
        "operationId": "getFundraiserOutcomeReport",
        "description": "The creator's published account of what the money did. Returns `{ \"report\": null }` (not `404`) when there is no report, when it is still a draft, and when FundlyHub has hidden it; the three cases are deliberately indistinguishable. Authentication is accepted and does not change the report; owners read their draft from `/mine`. The campaign itself is readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The published report, or null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/OutcomeReport"
                        }
                      ],
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Save or publish the outcome report",
        "operationId": "saveFundraiserOutcomeReport",
        "description": "Creates or replaces the campaign's single outcome report, and with `publish: true` publishes it in the same call. Publishing is one-way: a published report stays published on later saves, and `publishedAt` keeps its first value. Only the campaign's owner may write it. The body is sanitised as rich text; the title is plain text. Shares the media-upload rate limit (30 per 15 minutes). Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 20000,
                    "description": "Rich text (HTML). Required after trimming."
                  },
                  "attachments": {
                    "type": "array",
                    "maxItems": 12,
                    "description": "Photos uploaded through `POST /storage/upload`; every URL must be one of the platform's own upload URLs. Duplicates are rejected.",
                    "items": {
                      "$ref": "#/components/schemas/OutcomeReportAttachment"
                    }
                  },
                  "invoice_count": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Self-reported. A numeric string is accepted; omitted means 0."
                  },
                  "publish": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The saved report",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/OutcomeReport"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`body` missing or too long, `title` too long, invalid attachments, or invalid `invoice_count`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller does not own the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/outcome-report/mine": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get my campaign's outcome report, draft included",
        "operationId": "getMyFundraiserOutcomeReport",
        "description": "The owner's view of the outcome report: a draft is returned, and a hidden report is returned with `hiddenAt` set. `{ \"report\": null }` when none has been written. Campaign owner only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The report, or null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/OutcomeReport"
                        }
                      ],
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller does not own the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/endorsements": {
      "get": {
        "tags": [
          "Endorsements"
        ],
        "summary": "List a campaign's endorsers",
        "operationId": "listFundraiserEndorsers",
        "description": "Who stands behind a campaign: everyone who endorsed it, plus everyone whose share link for it recorded a visit, minus the organizer and profiles set to private. Ordered by impressions (human visits through their link). Authentication is optional; with a session, `viewerHasEndorsed` says whether the caller is on the list. A deleted campaign returns an empty list.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Endorsers (unwrapped)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "endorsers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CampaignEndorser"
                      }
                    },
                    "viewerHasEndorsed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/endorse": {
      "post": {
        "tags": [
          "Endorsements"
        ],
        "summary": "Endorse a campaign",
        "operationId": "endorseFundraiser",
        "description": "Publicly vouches for a campaign as the signed-in user, with an optional note. Idempotent: endorsing again updates the note and answers `200` with `created: false`; a new endorsement answers `201` and notifies the campaign owner.\nOnly public campaigns that are `active`, `paused` or `ended` can be endorsed, and not your own.\n**Requires permission:** `endorse_campaigns`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 280,
                    "description": "Trimmed; blank is stored as null."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing endorsement updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EndorsementResult"
                }
              }
            }
          },
          "201": {
            "description": "Endorsement created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EndorsementResult"
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID, or the note is over 280 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign not found (`code: not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EndorsementError"
                }
              }
            }
          },
          "409": {
            "description": "The campaign is not public or not in an endorsable status (`code: not_endorsable`), or it is your own (`code: self_endorsement`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EndorsementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Endorsements"
        ],
        "summary": "Withdraw an endorsement",
        "operationId": "unendorseFundraiser",
        "description": "Revokes the caller's live endorsement of the campaign and notifies the owner.\n**Requires permission:** `endorse_campaigns`",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "The caller has no live endorsement of this campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/fundraisers/{id}/endorsement-requests": {
      "post": {
        "tags": [
          "Endorsements"
        ],
        "summary": "Ask ambassadors to promote a campaign",
        "operationId": "createEndorsementRequests",
        "description": "Asks up to 12 ambassadors at once to promote the caller's campaign. Campaign owner only; no special permission is needed. Requires a verified email address.\nIds that are not current ambassadors (and the caller's own id) are dropped and listed in `rejected`. When the campaign is public and `active`, the ambassadors are emailed now (`status: \"sent\"`); otherwise the requests are queued and sent when the campaign is approved (`status: \"queued\"`). Asking the same ambassador again refreshes the request rather than duplicating it.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ambassadorIds"
                ],
                "properties": {
                  "ambassadorIds": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 12,
                    "description": "Profile ids, deduplicated before the cap is applied. Any malformed id rejects the whole call.",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Requests recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EndorsementRequest"
                      }
                    },
                    "notified": {
                      "type": "integer",
                      "description": "Emails sent by this call. Can exceed this call's rows when earlier queued requests were drained too."
                    },
                    "queued": {
                      "type": "integer",
                      "description": "This call's requests still waiting."
                    },
                    "rejected": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      }
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "sent",
                        "queued"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid campaign id, `ambassadorIds` not an array of UUIDs, empty, over 12, or none of them an available ambassador (that body also carries `rejected`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller does not own the campaign, or their email is not verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Endorsements"
        ],
        "summary": "List a campaign's endorsement requests",
        "operationId": "listEndorsementRequests",
        "description": "Who the owner has asked to promote the campaign, newest first, and whether each has been emailed yet. Campaign owner only.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign's UUID; anything else answers `400`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Requests (unwrapped)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EndorsementRequest"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The caller does not own the campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/images/proxy": {
      "get": {
        "tags": [
          "Images"
        ],
        "summary": "Proxy an allowlisted image",
        "operationId": "proxyImage",
        "description": "Streams an image from the platform's CDN or `images.unsplash.com`, for local development. Public. The upstream body and `Content-Type` are passed through with a 24-hour cache header; the upstream status code is not, so an upstream error page also arrives as `200`.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The upstream image",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`url` missing or not a valid URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The host is not on the allowlist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The upstream fetch failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/features": {
      "get": {
        "tags": [
          "Images"
        ],
        "summary": "Which image features are available",
        "operationId": "getImageFeatures",
        "description": "Whether stock-photo search and AI image generation are configured on this deployment. Public.",
        "responses": {
          "200": {
            "description": "Feature availability",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stockPhotos": {
                      "type": "boolean"
                    },
                    "aiGeneration": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/images/search": {
      "get": {
        "tags": [
          "Images"
        ],
        "summary": "Search stock photos",
        "operationId": "searchStockPhotos",
        "description": "Searches Unsplash for landscape photos. Requires a bearer session. When a photo is chosen, call `POST /images/track-download` with its `downloadLocation`, as Unsplash's terms require.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockPhoto"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`q` is missing or blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "Unsplash is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "An Unsplash error, answered with Unsplash's own status code (`403` means its rate limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/generate": {
      "post": {
        "tags": [
          "Images"
        ],
        "summary": "Generate a cover image with AI",
        "operationId": "generateAiCoverImage",
        "description": "Generates a landscape campaign cover from a prompt (the first 500 characters are used, wrapped in a fixed style prompt). Requires a bearer session.\nWith the default `gpt-image-*` model the image is stored on the platform CDN immediately and `persisted` is true; that path is also gated by `features.image_uploads` and answers `403` when the flag is off. With a `dall-e-*` model the URL is OpenAI's temporary one (`persisted: false`) and should be copied with `POST /images/save-from-url`.\nLimited to 10 generations an hour and 30 a day per user; past either limit the answer is `429` with `Retry-After`. Errors from the image provider are not passed through: a prompt the provider refuses answers `400`, a temporarily unavailable provider `503`, and any other provider failure `502`, each with a generic message.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated image",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "revisedPrompt": {
                      "type": "string"
                    },
                    "persisted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`prompt` missing or blank, or the prompt could not be used to generate an image (try a different description).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Image uploads are disabled (`features.image_uploads`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "No image came back, or storing it failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Image generation failed. The message is generic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "AI image generation is not configured, or is busy right now; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/fit-square": {
      "post": {
        "tags": [
          "Images"
        ],
        "summary": "Make a photo square with AI",
        "operationId": "fitImageToSquare",
        "description": "Re-renders a photo as a square without cutting anyone out of it: the people and the setting are kept and the rest of the frame is composed to fill the square. A photo that is already square is rendered again, so calling twice gives a second rendering. Requires a bearer session.\nThe square comes back as base64 and nothing is stored. Upload it through the normal image upload to use it as a cover.\nShares the limits of `POST /images/generate`: 10 an hour and 30 a day per user; past either limit the answer is `429` with `Retry-After`. Errors from the image provider are not passed through: a photo the provider refuses answers `400`, a temporarily unavailable provider `503`, and any other provider failure `502`, each with a generic message.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "imageBase64"
                ],
                "properties": {
                  "imageBase64": {
                    "type": "string",
                    "format": "byte",
                    "description": "The photo, base64-encoded. JPEG, PNG or WebP, at most 6 MB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The square photo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imageBase64": {
                      "type": "string",
                      "format": "byte",
                      "description": "A 1024×1024 image, base64-encoded."
                    },
                    "contentType": {
                      "type": "string",
                      "example": "image/png"
                    },
                    "changed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`imageBase64` missing, not a JPEG, PNG or WebP, larger than 6 MB or unreadable, or the photo could not be fitted (try another one).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Fitting the photo failed unexpectedly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Photo fitting failed, or no image came back. The message is generic.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "AI photo fitting is not configured, or is busy right now; try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/track-download": {
      "post": {
        "tags": [
          "Images"
        ],
        "summary": "Record a stock-photo selection",
        "operationId": "trackStockPhotoDownload",
        "description": "Tells Unsplash a photo was chosen, as its API guidelines require. Pass the `downloadLocation` from `GET /images/search`. Never fails the caller's action: a failed ping answers `200` with `tracked: false`. Requires a bearer session.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "downloadLocation"
                ],
                "properties": {
                  "downloadLocation": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tracking outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tracked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`downloadLocation` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "Unsplash is not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/save-from-url": {
      "post": {
        "tags": [
          "Images"
        ],
        "summary": "Copy a generated image to the CDN",
        "operationId": "saveImageFromUrl",
        "description": "Downloads an AI-generated image from OpenAI's image storage and stores it on the platform CDN, returning the permanent URL. A URL already on the platform CDN is returned unchanged. Requires a bearer session and the `features.image_uploads` flag.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "bucket": {
                    "type": "string",
                    "default": "fundraiser-images",
                    "description": "Upload folder the copy is stored under."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored image",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "fileName": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`url` missing, or not from a trusted image host.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Image uploads are disabled (`features.image_uploads`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Storing failed, or `url` could not be parsed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The source image could not be downloaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/media": {
      "get": {
        "tags": [
          "Media"
        ],
        "summary": "List a campaign's media",
        "operationId": "listFundraiserMedia",
        "description": "The campaign's photos and videos in gallery order. Every reader sees the public list (DMCA-taken-down items removed); the owner also sees processing and taken-down items. Answers `403` for everyone while `features.fundraiser_video` is off. Readable exactly when `GET /fundraisers/{id}` is: a campaign that is not `active`, `paused` or `ended`, is `private`, or has been deleted answers `404` to everyone but its owner.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Media items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FundraiserMedia"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The `features.fundraiser_video` flag is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "Query failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Media"
        ],
        "summary": "Add media to a campaign",
        "operationId": "createFundraiserMedia",
        "description": "Adds one item, by `kind`:\n- `image` — a URL from `POST /storage/upload` (other hosts are\n  rejected). The first image becomes the cover unless `isCover` says\n  otherwise. At most 10 images per campaign.\n- `video_link` — a YouTube, Vimeo, Wistia, Loom, Instagram, Facebook\n  or TikTok URL. At most 3 videos per campaign.\n- `video_upload` — reserves a direct upload to the video host and\n  returns its one-time `uploadURL`. Counts toward the 3 videos.\n\nCampaign owner only. Behind `features.fundraiser_video` and the media-upload rate limit (30 per 15 minutes). Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "image",
                      "video_link",
                      "video_upload"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Required for `image` and `video_link`."
                  },
                  "position": {
                    "type": "integer",
                    "description": "Defaults to the end of the gallery."
                  },
                  "isCover": {
                    "type": "boolean",
                    "description": "`image` only."
                  },
                  "title": {
                    "type": "string",
                    "description": "`video_upload` only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/FundraiserMedia"
                    },
                    "uploadURL": {
                      "type": "string",
                      "format": "uri",
                      "description": "`video_upload` only: where the client uploads the file."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown `kind`, missing `url`, an image URL not from platform storage, or an unsupported video provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner, or `features.fundraiser_video` is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The image or video cap is reached (`code: CAP_EXCEEDED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The video host failed (`code: UPSTREAM_FAILED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "503": {
            "description": "Video uploads are not configured (`code: STREAM_NOT_CONFIGURED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/media/preview": {
      "post": {
        "tags": [
          "Media"
        ],
        "summary": "Preview a video link",
        "operationId": "previewFundraiserMediaLink",
        "description": "Parses a video URL and returns what the gallery would embed, for the builder's preview card. Nothing is saved. Thumbnail and title come from the provider's oEmbed and may be null. Campaign owner only; behind `features.fundraiser_video`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parsed link",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string"
                    },
                    "providerVideoId": {
                      "type": "string"
                    },
                    "embedUrl": {
                      "type": "string"
                    },
                    "aspectRatio": {
                      "type": "string",
                      "enum": [
                        "16:9",
                        "9:16",
                        "4:5"
                      ]
                    },
                    "thumbnailUrl": {
                      "type": "string",
                      "nullable": true
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`url` missing, or not a supported provider (`code: UNSUPPORTED_PROVIDER`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/media/reorder": {
      "patch": {
        "tags": [
          "Media"
        ],
        "summary": "Reorder a campaign's media",
        "operationId": "reorderFundraiserMedia",
        "description": "Sets the gallery order to the order of `ids`. Every id must belong to this campaign. Campaign owner only; behind `features.fundraiser_video`. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reordered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`ids` is not an array of strings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not the owner, the flag is off, or an id belongs to another campaign (`code: FORBIDDEN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/fundraisers/{id}/media/{mid}/cover": {
      "patch": {
        "tags": [
          "Media"
        ],
        "summary": "Set the cover image",
        "operationId": "setFundraiserMediaCover",
        "description": "Makes one image the campaign's cover (clearing the previous one). Only images can be the cover. Campaign owner only; behind `features.fundraiser_video`. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new cover",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "$ref": "#/components/schemas/FundraiserMedia"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The item is not an image (`code: INVALID_IMAGE_URL`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign or media item not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          }
        }
      }
    },
    "/fundraisers/{id}/media/{mid}": {
      "delete": {
        "tags": [
          "Media"
        ],
        "summary": "Remove a media item",
        "operationId": "deleteFundraiserMedia",
        "description": "Removes one item from the gallery. Campaign owner only; behind `features.fundraiser_video`. Audit-logged.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Campaign or media item not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FundraiserMediaError"
                }
              }
            }
          }
        }
      }
    },
    "/achievements": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get the badge catalogue",
        "operationId": "getAchievementCatalogue",
        "description": "Every badge on the platform with its copy, tier ladder, holder counts and rarity, plus header totals, series and the grid's slots, in the reader's language (`?lang=`, then `Accept-Language`, then the `i18next` cookie). The payload is the same for every reader; a token is accepted and ignored. Cached per language.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catalogue (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AchievementView"
                      }
                    },
                    "totals": {
                      "type": "object",
                      "properties": {
                        "achievements": {
                          "type": "integer"
                        },
                        "earned_total": {
                          "type": "integer"
                        },
                        "collectors": {
                          "type": "integer"
                        },
                        "rarest": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "slug": {
                              "type": "string"
                            },
                            "title": {
                              "type": "string",
                              "nullable": true
                            },
                            "key": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "holders": {
                              "type": "integer"
                            }
                          }
                        },
                        "computed_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    },
                    "language": {
                      "type": "string"
                    },
                    "vocabulary": {
                      "$ref": "#/components/schemas/AchievementVocabulary"
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true,
                        "description": "A numbered series: `id`, `number`, `status`, `listed`, `size`, `name`, `subtitle`, `totals`, `window` (its issuance window), labels and blurbs, and `numbered` (its capped badges)."
                      }
                    },
                    "slots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "series_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "series_number": {
                            "type": "integer"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "card",
                              "reserved",
                              "retired"
                            ]
                          },
                          "slug": {
                            "type": "string"
                          },
                          "track": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "default_series_id": {
                      "type": "string",
                      "format": "uuid",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/art-sheets/{id}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get an achievement art sheet image",
        "operationId": "getAchievementArtSheetImage",
        "description": "The image of one badge art sheet, served immutably (one year) under its version token `?v=`. A request without the current token is redirected (`302`, uncached) to the current versioned URL. Public.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "description": "Anything else is the full sheet.",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "small",
                "cutout",
                "cutout_small"
              ],
              "default": "full"
            }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "description": "Version token. Cut-out sizes append `-c`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image (its stored content type)",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Redirect to the current versioned URL."
          },
          "404": {
            "description": "No such sheet, or no cut-out for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/art-images/{id}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get an achievement library image",
        "operationId": "getAchievementArtImageFile",
        "description": "One image from the badge art library, served immutably under its version token `?v=`; a request without the current token is redirected (`302`) to it. Public.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "small"
              ],
              "default": "full"
            }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image (its stored content type)",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "302": {
            "description": "Redirect to the current versioned URL."
          },
          "404": {
            "description": "No such image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/vocabulary": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get the achievement vocabulary",
        "operationId": "getAchievementVocabulary",
        "description": "The labels and colours every badge card is drawn with (tiers, tracks, rarity frames and so on), in the reader's language. Public.",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Vocabulary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "vocabulary": {
                      "$ref": "#/components/schemas/AchievementVocabulary"
                    },
                    "language": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/feed": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get the \"just earned\" feed",
        "operationId": "getAchievementFeed",
        "description": "The most recent cards issued or upgraded, platform-wide or for one badge (`species`). The size, maximum age and suggested refresh interval come from platform settings. An unknown, hidden or private badge answers an empty list. Public; sent with `Cache-Control: no-store`.",
        "parameters": [
          {
            "name": "species",
            "in": "query",
            "required": false,
            "description": "A badge slug. Repeating the parameter answers an empty list.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AchievementFeedRow"
                      }
                    },
                    "refresh_seconds": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/{slug}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get one badge",
        "operationId": "getAchievementCard",
        "description": "One badge with its copy, ladder and rarity. Authentication is optional: a signed-in reader also gets their own standing (earned, tier, progress). With `holder_type` and `holder_id`, the badge is answered for that holder instead (a profile's or organization's badge); both must be given together.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "holder_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "user",
                "organization"
              ]
            }
          },
          {
            "name": "holder_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The badge",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AchievementView"
                    },
                    "vocabulary": {
                      "$ref": "#/components/schemas/AchievementVocabulary"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Half a holder scope, or an invalid `holder_type` / `holder_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "404": {
            "description": "No such badge, or not visible to this reader.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/achievements/{slug}/share/{asset}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get a badge share picture",
        "operationId": "getAchievementShareAsset",
        "description": "A PNG of the badge for sharing: `og` 1200×630, `post` 1080×1080, `story` and `reel` 1080×1920. A trailing `.png` on `asset` is accepted. `?tier=` draws it at that tier when the tier exists. Drafts and private badges answer `404`. Public; cached for 5 minutes.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "og",
                "post",
                "story",
                "reel",
                "og.png",
                "post.png",
                "story.png",
                "reel.png"
              ]
            }
          },
          {
            "name": "tier",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset or badge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The renderer is busy (`code: RENDER_BUSY`); retry after the `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          }
        }
      }
    },
    "/cards/{code}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get an earned card",
        "operationId": "getAchievementCardByCode",
        "description": "One earned card by its short code, as the card's public page shows it: the badge, holder, tier, dates, serial, evidence, ladder, rarity, the verify URL and share links. Authentication is optional; the holder's own session adds an `owner` block. Sent `private, no-store`.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "The card's short code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Reader language; falls back to `Accept-Language`, then the `i18next` cookie, then English.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The card",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AchievementEarnedCard"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No readable card with this code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "410": {
            "description": "The card was withdrawn by FundlyHub (`code: WITHDRAWN`, with `data.code`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cards/{code}/share/{asset}": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get an earned card's share picture",
        "operationId": "getAchievementCardShareAsset",
        "description": "A PNG of one earned card: `og` 1200×630, `post` 1080×1080, `story` 1080×1920 (a trailing `.png` is accepted). Served only when the card is readable by a stranger. When the card's own picture is not rendered yet, the badge's generic picture is served with `X-Card-Render: pending` and `no-store`. Supports `If-None-Match` (`304`). Public; a per-IP limit (120 per minute by default) applies in addition to the public browsing limit.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "The card's short code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "og",
                "post",
                "story",
                "og.png",
                "post.png",
                "story.png"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image. `Content-Language` names the language it was drawn in.",
            "headers": {
              "X-Card-Render": {
                "description": "`pending` when a placeholder was served.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "304": {
            "description": "Not modified."
          },
          "404": {
            "description": "Unknown asset, or no readable card with this code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "410": {
            "description": "The card was withdrawn (`code: WITHDRAWN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "The renderer is busy (`code: RENDER_BUSY`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          }
        }
      }
    },
    "/cards/{code}/qr.svg": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get an earned card's verify QR code",
        "operationId": "getAchievementCardQr",
        "description": "An SVG QR code pointing at the card's verify URL. Only for cards readable by a stranger. Public; same per-IP limit as the share pictures. Cached for 60 seconds.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "The card's short code.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SVG image",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No readable card with this code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "410": {
            "description": "The card was withdrawn (`code: WITHDRAWN`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/storage/upload": {
      "post": {
        "tags": [
          "Storage"
        ],
        "summary": "Upload an image",
        "operationId": "uploadStorageFile",
        "description": "Stores a base64-encoded raster image on the platform CDN and returns its public URL, which the media gallery, outcome reports and the campaign form accept. Requires a bearer session. Only raster image types are accepted (no SVG or HTML), into one of four folders. Omit `path` and the server stores the file under a new unique path beginning with the caller's user id, returned as `path`. A supplied `path` must begin with the caller's own user id, profile id or Cognito sub, the same rule `POST /storage/delete` applies. An upload never replaces an existing file. The JSON body limit (10 MB) caps the file size.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bucket",
                  "fileBase64",
                  "contentType"
                ],
                "properties": {
                  "bucket": {
                    "$ref": "#/components/schemas/StorageBucket"
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "Optional. Relative path of letters, digits, `.`, `_`, `-` and single `/` separators; no `..`, leading slash or backslash. The first segment must be the caller's own id, followed by at least one more segment. Omit it to let the server choose a unique path.",
                    "example": "3f6c1b9e-7d2a-4c8e-9b1f-2a4d6e8f0c13/cover-1717171717.jpg"
                  },
                  "fileBase64": {
                    "type": "string",
                    "format": "byte"
                  },
                  "contentType": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png",
                      "image/webp",
                      "image/gif",
                      "image/heic",
                      "image/heif"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "path": {
                      "type": "string",
                      "description": "Where the file was stored; pass it to `POST /storage/delete`."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is missing, or the bucket, content type or path is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The supplied `path` does not begin with the caller's own id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A file already exists at the supplied `path`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The storage write failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/storage/delete": {
      "post": {
        "tags": [
          "Storage"
        ],
        "summary": "Delete an uploaded image",
        "operationId": "deleteStorageFile",
        "description": "Deletes one uploaded file. The path's first segment must be the caller's own user id, profile id or Cognito sub; files uploaded under any other prefix cannot be deleted through this endpoint. Requires a bearer session.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bucket",
                  "path"
                ],
                "properties": {
                  "bucket": {
                    "$ref": "#/components/schemas/StorageBucket"
                  },
                  "path": {
                    "type": "string",
                    "maxLength": 256
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is missing, or the bucket or path is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The path does not start with the caller's id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The storage delete failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys": {
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Create an API key",
        "operationId": "createApiKey",
        "description": "Mints a new API key for the authenticated user. The full key is returned **once**, in `api_key`, and cannot be retrieved again. `key.prefix` (the first 12 characters) is what later listings show.\n\nRequires a signed-in session (the session cookies, or a Cognito JWT in the `Authorization` header). A request authenticated with an API key, or made while an administrator is viewing the account as its owner, answers `403`.\n\nEvery new key expires: one year after creation unless `expires_at` asks for sooner. A key stops working early if it is revoked, if its owner's account is suspended, banned or deactivated, or if an administrator signs the owner out of every device (which revokes all of the owner's keys).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "A label for the key. Trimmed; must not be blank.",
                    "example": "CI deploy bot"
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When the key stops working, as an ISO 8601 timestamp. Must be in the future and no more than 365 days from now. Omit or `null` for the default, 365 days from now. A key that never expires cannot be created."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "api_key": {
                      "type": "string",
                      "description": "The full secret key. Shown only in this response.",
                      "example": "fh_live_3f9c2a1b0e7d4c5a8b6f1e2d3c4b5a69788766554433221100ffeeddccbbaa99"
                    },
                    "key": {
                      "$ref": "#/components/schemas/ApiKey"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name` missing, blank, or longer than 100 characters; or `expires_at` not a date-time, not in the future, or more than 365 days from now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/SessionCredentialRequired"
          },
          "500": {
            "description": "Key could not be created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "List my API keys",
        "operationId": "listApiKeys",
        "description": "The authenticated user's keys, newest first. Never includes the secret. Revoked keys are excluded unless `include_revoked=true`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "include_revoked",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Keys could not be listed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys/{id}": {
      "delete": {
        "tags": [
          "API Keys"
        ],
        "summary": "Revoke an API key",
        "operationId": "revokeApiKey",
        "description": "Revokes one of the caller's own keys. Takes effect immediately. A key that belongs to someone else, does not exist, or is already revoked answers `404`.\n\nRequires a signed-in session: a request authenticated with an API key, or made while an administrator is viewing the account as its owner, answers `403`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/SessionCredentialRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "Revocation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cognito/change-password": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Change password",
        "operationId": "changePassword",
        "description": "Changes the password of an email/password account, given the current one.\n\n**Cookie session only.** A request authenticated with an `Authorization: Bearer` header or an API key answers `401 Not authenticated`. Google and Apple accounts have no password to change.\n\nRate limited at 10 requests/minute per IP (authentication bucket).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "currentPassword",
                  "newPassword"
                ],
                "properties": {
                  "currentPassword": {
                    "type": "string",
                    "format": "password"
                  },
                  "newPassword": {
                    "type": "string",
                    "format": "password",
                    "minLength": 8,
                    "description": "Must also satisfy the Cognito pool policy (upper, lower, digit and symbol)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is missing, the new password is shorter than 8 characters or fails the pool policy, or the current password is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Either the authentication rate limit, or Cognito's own `LimitExceededException`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Cognito rejected the change for another reason",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cognito/resend-verification": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Resend the email-verification link",
        "operationId": "resendVerificationEmail",
        "description": "Sends a fresh verification link to the account registered under `email` (matched case-insensitively against the **login** address, not the private contact address — for that use `POST /users/me/private-contact/resend`).\n\nAlways answers `200` with the same message, whether or not the address has an account and even when sending fails, so it cannot be used to discover accounts.\n\nNo authentication. Rate limited at 5 requests/minute, keyed by user when a session is present and by IP otherwise.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted (sent if the address is known)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "If this email exists, a verification link has been sent."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`email` missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cognito/verify-email": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Verify an email address (link target)",
        "operationId": "verifyEmailLink",
        "description": "The URL in verification emails. Consumes the token and **always redirects** to the frontend with the outcome in a query parameter; it never returns JSON.\n\n`?verification=` is one of: `success`; `invalid` (no token); `failed` (unknown, used or expired token); `contact_taken` (the link proved a private contact address another account already holds); `error` (unexpected failure).\n\nWhen the link proved the account's **login** address, the Cognito `email_verified` attribute is updated too, and any pending ambassador invitation for that address is settled.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to `{FRONTEND_URL}/?verification=<outcome>`",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/cognito/oauth/callback": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "OAuth callback (Google / Apple)",
        "operationId": "handleOAuthCallback",
        "description": "The Cognito Hosted UI's `redirect_uri` after a Google or Apple sign-in, started at `GET /cognito/oauth/{provider}`. Not called by API clients directly.\n\nExchanges `code` for tokens, provisions or links the profile, sets the `access_token`, `id_token` and `refresh_token` httpOnly cookies, and redirects to the URL carried in `state` — which is the `redirect` query parameter given when the flow was started, or the frontend root.\n\nEvery failure is also a redirect, to `{FRONTEND_URL}/auth?error=…`: `unverified_account_exists` (a provider identity linked to a native account whose address was never verified), `missing_code`, `token_exchange_failed`, `invalid_token`, `oauth_error`, or the provider's own `error` with a `message`.",
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "required": false,
            "description": "Authorization code from Cognito.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Where to send the browser after a successful sign-in.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "error_description",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect — to `state` on success (with session cookies set), or to `/auth?error=…` on the frontend.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "Set-Cookie": {
                "description": "`access_token`, `id_token`, `refresh_token` (httpOnly) on success.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/badges": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a profile's trust badges",
        "operationId": "getUserBadges",
        "description": "The trust badges (e.g. identity verified, brand ambassador) on a profile, newest first. `id` is a UUID or a profile slug.\n\nA **private** profile answers `{ \"badges\": [] }` to everyone but its owner — the same answer as a profile with no badges, so the response does not confirm that the profile is private.\n\nAuthentication is optional; it only matters for the owner.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Profile UUID or profile slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The badges (bare object, not the `data` envelope)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "badges": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "badgeType": {
                            "type": "string"
                          },
                          "metadata": {
                            "type": "object",
                            "nullable": true,
                            "additionalProperties": true,
                            "description": "Free-form, per badge type."
                          },
                          "awardedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/campaigns": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List a profile's campaigns",
        "operationId": "getUserCampaigns",
        "description": "The campaigns shown on a profile: those the person runs, and those they have endorsed (`is_endorsed: true`), newest activity first. `id` is a UUID or a profile slug.\n\nA visitor sees public campaigns in `active` or `ended` status. The profile's owner, identified by the **session** (there is no query flag for it), additionally sees their own `draft`, `pending` and `paused` campaigns and their unlisted and private ones. A private profile returns `{ \"data\": [] }` to everyone but its owner.\n\nTitles and summaries are returned in the reader's language when a translation exists (`?lang=`, then the language cookie, then `Accept-Language`).\n\nRate limited at 300 requests/minute per IP.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Profile UUID or profile slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 60
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The profile's campaign cards",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProfileCampaignCard"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/og-image": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Get a profile's share image",
        "operationId": "getUserOgImage",
        "description": "The 1200×630 PNG a shared profile link unfurls into: name, avatar, account kind, location, join date and campaign count. Intended for social crawlers. `id` is a UUID or a profile slug. The name is the profile's `display_name`: the name, else `@handle`, else `FundlyHub member`, never an email address.\n\nA private profile, like a missing one, is a `404`. Cached for five minutes, server-side and via `Cache-Control`.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Profile UUID or profile slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300, s-maxage=300"
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Image generation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/me/private-contact/resend": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Resend the private-contact verification email",
        "operationId": "resendPrivateContactVerification",
        "description": "Sends a new verification link to the caller's private contact email (set with `PUT /users/me/private-contact`). Links last 24 hours; a verified private contact email is required before publishing a campaign. If the address is already verified, nothing is sent and the response says so.\n\nRate limited at 5 requests/minute per user, a bucket shared with `PUT /users/me/private-contact`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sent, or already verified",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "alreadyVerified": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No private contact email on file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The email could not be sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/recalculate-counts": {
      "post": {
        "tags": [
          "Social"
        ],
        "summary": "Recalculate follower counts",
        "operationId": "recalculateFollowCounts",
        "description": "Recounts the caller's followers and followings, stores `follower_count` / `following_count` on the profile, and returns them. `id` must be the caller's own profile UUID; any other profile answers `403`. The counts are also refreshed whenever a follow is created or removed, so this is only a manual resync. Requires a bearer session.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The recounted figures",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "follower_count": {
                          "type": "integer"
                        },
                        "following_count": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Recount failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/marketplace/creators": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List top creators",
        "operationId": "listMarketplaceCreators",
        "description": "Public profiles ranked by money raised, then followers. Cached server- side; `cached` says whether this answer came from the cache.\n\n**`fundsRaised` is in DOLLARS** (a decimal), unlike the rest of the API, which uses integer cents.\n\nNo authentication.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The creators (bare object with `data`, not paginated)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string",
                            "description": "`Anonymous User` when the profile has no name."
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "location": {
                            "type": "string",
                            "description": "Empty string when unset."
                          },
                          "bio": {
                            "type": "string",
                            "description": "Empty string when unset."
                          },
                          "profileType": {
                            "type": "string",
                            "enum": [
                              "individual",
                              "organization"
                            ],
                            "description": "`organization` when the person belongs to any organization."
                          },
                          "fundsRaised": {
                            "type": "number",
                            "description": "Money raised across their campaigns, in DOLLARS."
                          },
                          "campaignsCompleted": {
                            "type": "integer"
                          },
                          "followersCount": {
                            "type": "integer"
                          },
                          "sharesCount": {
                            "type": "integer"
                          },
                          "badges": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Badge types, newest first."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "executionTimeMs": {
                      "type": "integer"
                    },
                    "cached": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/system-settings/features": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get feature flags",
        "operationId": "getPublicFeatureFlags",
        "description": "The `features.*` flags, read-only, for the signed-in caller — so the client can hide what the server will refuse. Flags that only concern FundlyHub's own operations are omitted. Each value is reduced to the three gating fields.\n\nThe server treats a flag that has no row as enabled, so a flag absent from this list is not necessarily off.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The flags",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "setting_key": {
                            "type": "string",
                            "example": "features.comments"
                          },
                          "setting_value": {
                            "type": "object",
                            "properties": {
                              "enabled": {
                                "type": "boolean"
                              },
                              "allowed_roles": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "Roles that may use the feature while it is disabled. Only operator roles are honoured."
                              },
                              "disabled_message": {
                                "type": "string"
                              }
                            }
                          },
                          "category": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Flags could not be loaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/numbers": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get the platform's published numbers",
        "operationId": "getPlatformNumbers",
        "description": "The figures FundlyHub publishes about itself on `/@fundlyhub`. The same answer for every reader, cached for five minutes; `computed_at` says how old it is. `GET /stats` is unchanged and still served.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "responses": {
          "200": {
            "description": "The numbers (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlatformNumbers"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The numbers could not be computed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/team": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "List the FundlyHub team",
        "operationId": "getPlatformTeam",
        "description": "The FundlyHub team members shown on `/@fundlyhub`: staff with a public, slugged profile and an account in good standing — at most 48, ordered by role then name. An empty list is a normal `200`.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Cache key for future localised fields. Anything else is treated as `en`.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlatformTeamMember"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The roster could not be loaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/ambassadors": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "List platform ambassadors with impact",
        "operationId": "getPlatformAmbassadors",
        "description": "Every holder of the `ambassador` role who is not banned (at most 100), ranked by impact. `total` counts all of them; `members` lists only those with a public, slugged profile, so `total` can exceed `members.length`.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ambassador rail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlatformAmbassador"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The rail could not be loaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/tips": {
      "post": {
        "tags": [
          "Platform"
        ],
        "summary": "Tip FundlyHub",
        "operationId": "createPlatformTip",
        "description": "Starts a tip to FundlyHub itself — not a donation to a cause, and not tax-deductible. Creates a pending tip row and a Stripe Checkout Session (`payment` mode for `one_time`, monthly `subscription` mode for `recurring`) and returns its `url`; send the browser there. Stripe returns the payer to `/tip/{tip_id}` on the site. The tip is settled by the Stripe webhook, not by this call.\n\nAuthentication is optional. With a session the tip is attributed to the account, and the account's name and email are used when the body does not give them. Currency is always USD.\n\nRate limited at 10 requests/minute per IP (authentication bucket).",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount_cents",
                  "kind"
                ],
                "properties": {
                  "amount_cents": {
                    "type": "integer",
                    "minimum": 100,
                    "maximum": 10000000,
                    "description": "Whole cents. `25.5` is rejected, not rounded."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "one_time",
                      "recurring"
                    ]
                  },
                  "donor_email": {
                    "type": "string",
                    "format": "email",
                    "description": "For the receipt; never published. Prefills Stripe's email field."
                  },
                  "donor_name": {
                    "type": "string"
                  },
                  "is_anonymous": {
                    "type": "boolean",
                    "default": false,
                    "description": "Keeps the name off the public receipt."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Checkout started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tip_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "one_time",
                        "recurring"
                      ]
                    },
                    "amount_cents": {
                      "type": "integer"
                    },
                    "currency": {
                      "type": "string",
                      "example": "usd"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Stripe-hosted Checkout page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`amount_cents` or `kind` invalid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INVALID_AMOUNT",
                        "INVALID_KIND"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The tip or the Stripe session could not be created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/tips/{id}": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get a tip receipt",
        "operationId": "getPlatformTipReceipt",
        "description": "The public receipt for one tip: amount, cadence, status and dates, and the signed-in tipper's profile name unless they tipped anonymously. Never an email or a Stripe identifier.\n\nA tip that has not settled yet is a `200` with `status: pending` — the payer often arrives from Stripe before the webhook does. A monthly tip also carries `subscription`; its next charge date is read from Stripe once the tip is paid.\n\nNo authentication: the tip id is the capability. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The receipt (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlatformTipReceipt"
                }
              }
            }
          },
          "404": {
            "description": "No such tip, or the id is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The receipt could not be loaded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/platform/tips/{id}/receipt/email": {
      "post": {
        "tags": [
          "Platform"
        ],
        "summary": "Email a tip receipt",
        "operationId": "emailPlatformTipReceipt",
        "description": "Emails the receipt for one **paid** tip to any address — every figure comes from the tip row, so the caller controls only the recipient. Capped at **5 sends per tip**, after which this answers `429` for that tip permanently. A missing and an unpaid tip get the same `404`.\n\nThe mail queue is processed immediately; `message` says whether the email was sent or only queued.\n\nAuthentication is optional. Rate limited at 5 requests/minute per IP.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "recipient_email"
                ],
                "properties": {
                  "recipient_email": {
                    "type": "string",
                    "format": "email",
                    "description": "Lowercased before use."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent or queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string",
                      "enum": [
                        "Receipt email sent successfully",
                        "Receipt email queued"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Tip id not a UUID, or `recipient_email` missing or malformed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No paid tip with that id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Either the shared limit (5 requests per minute per IP), or this tip has already been emailed 5 times.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Sending failed unexpectedly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The email provider rejected the message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/home/activity": {
      "get": {
        "tags": [
          "Platform"
        ],
        "summary": "Get the homepage live-activity feed",
        "operationId": "getHomeActivity",
        "description": "The homepage hero's live chips, newest first: gifts, referral-link views and clicks, people viewing a campaign right now, campaigns submitted for review (never named), and achievements earned. Every event passes the same public gates as the surface it comes from — only public, active or ended campaigns are named, and anonymous donors are not.\n\n`events` is a discriminated union on `kind`. The same answer for every reader of a language; publicly cached for 15 seconds.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 24
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Campaign titles in this language when translated.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The feed",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=15, s-maxage=15"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "events": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/HeroActivityEvent"
                          }
                        },
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The feed could not be built",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INTERNAL_ERROR"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/presence/fundraisers/{id}": {
      "post": {
        "tags": [
          "Platform"
        ],
        "summary": "Send a campaign-page presence heartbeat",
        "operationId": "postFundraiserPresence",
        "description": "Records that a visitor has a campaign page open; feeds the \"N people viewing\" chip in `GET /home/activity`. Designed for `navigator.sendBeacon` — no body. The visitor is the `visitor_id` cookie, or the per-tab `v` query parameter when there is no cookie; only a one-way hash of it is stored, and it expires on its own.\n\nNo authentication. Rate limited at **4 requests/minute per IP and campaign**, then the 300/minute public bucket.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Fundraiser UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "description": "Per-tab visitor id, used only when there is no `visitor_id` cookie.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,64}$"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Recorded (no body)"
          },
          "400": {
            "description": "`id` is not a UUID, or there is no usable visitor id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "BAD_REQUEST"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/analytics/campaigns/aggregate": {
      "get": {
        "tags": [
          "Fundraisers"
        ],
        "summary": "Get aggregate campaign stats",
        "operationId": "getAggregateCampaignStats",
        "description": "Counters for the `/causes` stat tiles, over public, non-deleted campaigns in `active` or `ended` status, filtered the same way as the listing. \"Closed\" means ended, or active with a past `end_date`. Every figure, `total_donors` and `topCategories` included, covers the same filtered set.\n\n**Counts and sums are returned as numeric strings.**\n\nNo authentication.",
        "parameters": [
          {
            "name": "is_project",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false",
                "1",
                "0"
              ]
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category slug, id or name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The aggregates (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total_campaigns": {
                      "type": "string",
                      "example": "128"
                    },
                    "active_campaigns": {
                      "type": "string"
                    },
                    "closed_campaigns": {
                      "type": "string"
                    },
                    "total_raised_cents": {
                      "type": "string",
                      "description": "Cents."
                    },
                    "total_goal": {
                      "type": "string",
                      "nullable": true,
                      "description": "Sum of goals, in cents."
                    },
                    "avg_completion_percent": {
                      "type": "string",
                      "nullable": true,
                      "description": "Mean of raised / goal × 100."
                    },
                    "total_donors": {
                      "type": "string",
                      "description": "Sum of unique donors per campaign, over the filtered campaigns."
                    },
                    "topCategories": {
                      "type": "array",
                      "description": "Top five categories by money raised over the filtered campaigns. A category with no matching campaign can appear with zero counts.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "campaign_count": {
                            "type": "string"
                          },
                          "total_raised_cents": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Aggregation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/meta/llms.txt": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "Get llms.txt",
        "operationId": "getLlmsTxt",
        "description": "The AI-crawler discovery file served at `https://fundlyhub.org/llms.txt`: live platform statistics, categories, trending campaigns, key pages and localised versions, in Markdown. Cached for an hour. No authentication.",
        "responses": {
          "200": {
            "description": "The file",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Generation failed",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/meta/llms-full.txt": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "Get llms-full.txt",
        "operationId": "getLlmsFullTxt",
        "description": "The long form of `llms.txt`: active campaigns, how the platform works, the money and trust model, FAQ, glossary and site structure. Cached for an hour. No authentication.",
        "responses": {
          "200": {
            "description": "The file",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Generation failed",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/meta/sitemap-index": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "Get the sitemap index",
        "operationId": "getSitemapIndex",
        "description": "A `<sitemapindex>` listing one sitemap per locale (`/sitemaps/{en,ru,uk,es}.xml` on the site). Cached for five minutes. No authentication.",
        "responses": {
          "200": {
            "description": "Sitemap index XML",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Generation failed",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/meta/sitemaps/{locale}": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "Get a locale sitemap",
        "operationId": "getLocaleSitemap",
        "description": "The `<urlset>` for one locale, with `hreflang` alternates. Accepts the bare locale or the locale with `.xml`. Cached for five minutes. No authentication.",
        "parameters": [
          {
            "name": "locale",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es",
                "en.xml",
                "ru.xml",
                "uk.xml",
                "es.xml"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sitemap XML",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown locale",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Generation failed",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/donations/recent": {
      "get": {
        "tags": [
          "Donations"
        ],
        "summary": "List recent gifts (public feed)",
        "operationId": "getRecentDonations",
        "description": "Paid gifts for the homepage hero, newest first, in one of two modes:\n\n- **Unscoped** (no `slugs`): the newest `limit` gifts platform-wide.\n- **Per campaign** (`slugs`): the newest `perCampaign` gifts for\n  *each* named campaign, ranked within that campaign.\n\n\nOnly gifts to public campaigns in `active` or `ended` status appear, and gifts a moderator pulled from the feed never do. An anonymous gift has its name, avatar, city and ordinal nulled. `amount_cents` is the **net** amount (after the Stripe fee) — the same figure the campaign page shows. Campaign titles are localised as for `GET /users/{id}/campaigns`.\n\nPublicly cached for 15 seconds. No authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Unscoped mode only.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 24,
              "default": 12
            }
          },
          {
            "name": "slugs",
            "in": "query",
            "required": false,
            "description": "Comma-separated campaign slugs (or a repeated parameter). At most 6 are used; extras are ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "perCampaign",
            "in": "query",
            "required": false,
            "description": "Per-campaign mode only.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 3
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ru",
                "uk",
                "es"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The gifts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecentGift"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/donations/receipt/{receiptId}/note": {
      "post": {
        "tags": [
          "Donations"
        ],
        "summary": "Post, edit or retract the donor's note",
        "operationId": "postDonationNote",
        "description": "Writes the note a donor leaves on their receipt, which appears in the campaign's comments. The **receipt id is the authorisation** — guest donors have no session — and the donation must be paid and to a campaign. A missing, unpaid or person-targeted donation gets the same `404`.\n\nEach donation allows **3 writes in total** (post, edits and retract combined); after that every write is `429`. Read the current note with the `GET` first so a reload does not spend one. The note is attributed to an account only when the caller is signed in as the donor.\n\nAuthentication is optional. Behind `features.comments`. Rate limited at 5 requests/minute per IP.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "receiptId",
            "in": "path",
            "required": true,
            "description": "The receipt id, or the donation's Stripe PaymentIntent id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Required unless `retract` is `true`."
                  },
                  "retract": {
                    "type": "boolean",
                    "description": "`true` withdraws the note instead of writing one."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Note retracted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "retracted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Note written",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "edits_remaining": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 2
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing receipt id, empty content, or content over 2000 characters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`features.comments` is disabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No paid campaign donation for that receipt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Either the shared limit (5 per minute per IP), or this note has used its 3 writes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The note could not be saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Donations"
        ],
        "summary": "Get the donor's note",
        "operationId": "getDonationNote",
        "description": "The note already on a paid donation, so the composer can show it and its remaining writes (3 − `note_edit_count`). `data` is `null` when there is no note.\n\nNo authentication: the receipt id is the capability. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "receiptId",
            "in": "path",
            "required": true,
            "description": "The receipt id, or the donation's Stripe PaymentIntent id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The note, or null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "content": {
                          "type": "string"
                        },
                        "gif": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Gif"
                            }
                          ],
                          "nullable": true,
                          "description": "Always null — a receipt note cannot carry a GIF (#1963)."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "note_edit_count": {
                          "type": "integer"
                        },
                        "retracted": {
                          "type": "boolean"
                        },
                        "hidden": {
                          "type": "boolean",
                          "description": "Hidden by a moderator."
                        },
                        "campaign_slug": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing receipt id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/creators/{user_id}/tiers": {
      "get": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "List a creator's tiers",
        "operationId": "listCreatorTiers",
        "description": "A creator's active tiers, by `sort_order` then price. Empty for a private profile, except to its owner.\n\nAuthentication is optional; it only matters for the owner.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "description": "The creator's profile UUID (slugs are not accepted).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active tiers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CreatorTier"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`user_id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/tiers": {
      "get": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "List my tiers",
        "operationId": "listMyCreatorTiers",
        "description": "The caller's tiers, archived ones included (active first). Empty for someone who has never created one.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's tiers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CreatorTier"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Create a tier",
        "operationId": "createMyCreatorTier",
        "description": "Creates a tier and its Stripe Product with a monthly Price (and an annual one when `annual_amount_cents` is given). Creating a first tier **grants the caller the `creator` role**; there is no separate \"become a creator\" step. Receiving payouts still needs Stripe Connect onboarding — fans can subscribe before then, and the money is held.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatorTierInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tier created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CreatorTier"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller's profile was not found while granting the creator role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The caller already has an active tier with that name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Creation failed (including a Stripe error, e.g. an unsupported currency)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/tiers/{tier_id}": {
      "patch": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Update a tier",
        "operationId": "updateMyCreatorTier",
        "description": "Partial update of one of the caller's tiers. Changing a price mints a new Stripe Price and deactivates the old one; existing subscribers keep their original price until renewal. Setting `annual_amount_cents` to `null` withdraws annual billing. Currency cannot be changed. A tier that is not the caller's is a `404`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tier_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1000,
                    "nullable": true
                  },
                  "amount_cents": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "annual_amount_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "nullable": true
                  },
                  "benefits": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "maxLength": 200
                    }
                  },
                  "cover_image": {
                    "type": "string",
                    "maxLength": 2048,
                    "nullable": true
                  },
                  "sort_order": {
                    "type": "integer"
                  },
                  "subscriber_limit": {
                    "type": "integer",
                    "minimum": 1,
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tier",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CreatorTier"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such tier for this caller, or `tier_id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Another of the caller's tiers already has that name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Update failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Archive a tier",
        "operationId": "archiveMyCreatorTier",
        "description": "Archives one of the caller's tiers (`is_active: false`), hiding it from the public list. Reversible, and safe for tiers with subscribers. To remove a tier for good, use `DELETE /me/tiers/{tier_id}/permanent`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tier_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Archived"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such tier for this caller, or `tier_id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Archive failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/tiers/{tier_id}/permanent": {
      "delete": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Delete a tier permanently",
        "operationId": "deleteMyCreatorTier",
        "description": "Deletes one of the caller's tiers outright. Only possible for a tier nobody has ever subscribed to — a cancelled subscription is still a billing record — otherwise `409` with `code: TIER_HAS_SUBSCRIBERS`; archive it instead.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tier_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such tier for this caller, or `tier_id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The tier has subscription history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "TIER_HAS_SUBSCRIBERS"
                      ]
                    },
                    "subscriber_count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Deletion failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/subscriptions": {
      "post": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Subscribe to a creator tier",
        "operationId": "createCreatorSubscription",
        "description": "Creates a Stripe Subscription to an active tier, recorded locally as `incomplete`. Confirm the first payment in the browser with Stripe Elements using `client_secret`; the webhook then moves it to `active`.\n\nCalling again while a previous attempt for the same tier is still `incomplete` or `past_due` resumes it — the same subscription and `client_secret` — rather than creating a second one. An `active` or `trialing` subscription to the tier is a `409`. Subscribing to a different tier of the same creator is allowed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tier_id",
                  "interval"
                ],
                "properties": {
                  "tier_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "interval": {
                    "type": "string",
                    "enum": [
                      "month",
                      "year"
                    ],
                    "description": "`year` only when the tier has an annual price."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created or resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "subscription_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "stripe_subscription_id": {
                          "type": "string"
                        },
                        "client_secret": {
                          "type": "string",
                          "nullable": true,
                          "description": "For confirming the first invoice's PaymentIntent."
                        },
                        "status": {
                          "$ref": "#/components/schemas/CreatorSubscriptionStatus"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`tier_id` not a UUID, bad `interval`, tier missing or archived, the caller owns the tier, or the tier has no price for that interval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Already subscribed to this tier, or the tier is at its subscriber limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Stripe rejected the subscription; `error` is Stripe's message",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Creation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "List my creator subscriptions",
        "operationId": "listMyCreatorSubscriptions",
        "description": "Every subscription the caller holds, ended ones included, with tier and creator display fields. Active and trialing first, then past due, paused, and the rest; newest first within each.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's subscriptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CreatorSubscription"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/subscriptions/{id}/cancel": {
      "post": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Cancel a creator subscription",
        "operationId": "cancelCreatorSubscription",
        "description": "An `active` or `trialing` subscription is set to cancel at the end of the paid period. One that is `incomplete`, `past_due`, `unpaid` or `paused` is cancelled immediately. Already cancelled or already scheduled is a no-op. A subscription that is not the caller's is a `404`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The local subscription id (`subscription_id`), not the Stripe id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled or scheduled"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such subscription for this caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Cancellation failed (including a Stripe error)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/subscriptions/{id}/resume": {
      "post": {
        "tags": [
          "Creator Subscriptions"
        ],
        "summary": "Resume a creator subscription",
        "operationId": "resumeCreatorSubscription",
        "description": "Undoes a scheduled cancellation while the subscription is still running. A no-op when nothing is scheduled. A subscription that has already ended cannot be resumed — subscribe again.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Resumed (or nothing to undo)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No such subscription for this caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The subscription has already ended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Resume failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/unsubscribe/preview": {
      "get": {
        "tags": [
          "Email Preferences"
        ],
        "summary": "Preview an unsubscribe link",
        "operationId": "previewUnsubscribe",
        "description": "Validates the signed token from an email's unsubscribe link and says which (masked) address and which scope it would silence. Changes nothing.\n\nNo authentication: the signed token is the authorisation. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The token is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UnsubscribeResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The token is missing, invalid or malformed. `error` is an i18n key: `unsubscribe.error_stale_link` when the link still carries an unrendered `{{unsubscribe_token}}` placeholder (request a fresh link with `POST /unsubscribe/request`), else `unsubscribe.error_invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "unsubscribe.error_stale_link",
                        "unsubscribe.error_invalid"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The link could not be checked because the database was unavailable (`error` is `unsubscribe.error_generic`). The link itself may be fine; try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/unsubscribe": {
      "post": {
        "tags": [
          "Email Preferences"
        ],
        "summary": "Unsubscribe",
        "operationId": "unsubscribe",
        "description": "Suppresses mail to the token's address for the token's scope, or for everything when `all` is `true`. Idempotent.\n\nNo authentication: the signed token is the authorisation. No feature flag can switch this off. Rate limited at 10 requests/minute per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnsubscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unsubscribed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UnsubscribeResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The token is missing, invalid or malformed. `error` is an i18n key: `unsubscribe.error_stale_link` when the link still carries an unrendered `{{unsubscribe_token}}` placeholder (request a fresh link with `POST /unsubscribe/request`), else `unsubscribe.error_invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "unsubscribe.error_stale_link",
                        "unsubscribe.error_invalid"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The suppression could not be saved (`error` is an i18n key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/unsubscribe/resubscribe": {
      "post": {
        "tags": [
          "Email Preferences"
        ],
        "summary": "Resubscribe",
        "operationId": "resubscribe",
        "description": "Lifts the suppression for the token's address and scope (or for `all`). The undo for `POST /unsubscribe`.\n\nNo authentication: the signed token is the authorisation. Rate limited at 10 requests/minute per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnsubscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resubscribed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UnsubscribeResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The token is missing, invalid or malformed. `error` is an i18n key: `unsubscribe.error_stale_link` when the link still carries an unrendered `{{unsubscribe_token}}` placeholder (request a fresh link with `POST /unsubscribe/request`), else `unsubscribe.error_invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "unsubscribe.error_stale_link",
                        "unsubscribe.error_invalid"
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The change could not be saved (`error` is an i18n key)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/unsubscribe/request": {
      "post": {
        "tags": [
          "Email Preferences"
        ],
        "summary": "Request a fresh unsubscribe link",
        "operationId": "requestUnsubscribeLink",
        "description": "Mails a new signed unsubscribe link to `email` — the repair path for old emails whose link carried no token. Sent only to an address FundlyHub has previously mailed, at most once per address per hour.\n\n**Always answers `{ \"data\": { \"sent\": true } }`**, whatever happened, so it cannot be used to test whether an address is known.\n\nNo authentication. Rate limited at 10 requests/minute per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "sent": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/ambassador-invites/redeem": {
      "post": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Redeem an ambassador invitation",
        "operationId": "redeemAmbassadorInvite",
        "description": "Accepts an ambassador invitation for the signed-in account — the path for Google and Apple sign-ups, which cannot carry the token through `POST /cognito/signup`. The invitation is matched against the account's own registered email address; someone else's token is declined with `email_mismatch`.\n\nA declined redemption is still a **`200`** with `granted: false` and a `reason`, so a client can always call this after sign-up even when the token was already spent. No special permission is needed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The `ambassador_invite` token from the invitation link."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Granted, or declined with a reason",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "granted": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present only when `granted` is false.",
                      "enum": [
                        "no_token",
                        "not_found",
                        "used",
                        "revoked",
                        "expired",
                        "email_mismatch",
                        "role_missing"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`token` missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Redemption failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ambassador-applications": {
      "post": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Apply to be an ambassador",
        "operationId": "createAmbassadorApplication",
        "description": "Files an application to the ambassador programme for review by FundlyHub. Works signed out; when a session is present, the application records which account filed it.\n\nOne pending application per address: a second is `409`, as is an address that already holds the ambassador role.\n\nAuthentication is optional. Rate limited at 5 requests/minute per IP.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fullName",
                  "email",
                  "location",
                  "experience",
                  "motivation"
                ],
                "properties": {
                  "fullName": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 120
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "location": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 160
                  },
                  "socialMedia": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "A URL, or an empty string."
                  },
                  "experience": {
                    "type": "string",
                    "minLength": 50,
                    "maxLength": 5000
                  },
                  "motivation": {
                    "type": "string",
                    "minLength": 50,
                    "maxLength": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Application received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "received": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field failed validation; `error` names it (e.g. `motivation:` …)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A pending application exists for this address, or it already belongs to an ambassador",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "The application could not be recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ambassadors": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "List ambassadors (public directory)",
        "operationId": "listAmbassadors",
        "description": "Ambassador role-holders with a public profile, ordered by the money their referral links have driven (the amounts themselves are not returned). Feeds the front-page rail.\n\nNo authentication. Rate limited at 300 requests/minute per IP.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The directory",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ambassadors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "userId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "href": {
                            "type": "string",
                            "description": "Profile path — by slug when there is one, by id otherwise."
                          },
                          "location": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ambassadors/selectable": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "List ambassadors for the endorsement picker",
        "operationId": "listSelectableAmbassadors",
        "description": "Ambassadors a campaign creator may ask to endorse their campaign: role-holders with a public profile, excluding the caller, with the lifetime money each has driven in cents. Authenticated because it carries those amounts.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 60,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The picker list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ambassadors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "userId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true
                          },
                          "avatar": {
                            "type": "string",
                            "nullable": true
                          },
                          "referredCents": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ambassadors/{id}/analytics": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get an ambassador's referral analytics",
        "operationId": "getAmbassadorAnalytics",
        "description": "All-time referral analytics for one ambassador: clicks by traffic type, conversions, attributed funds per currency, and breakdowns by campaign, UTM source and day.\n\nCallers may read their **own** analytics; anyone else's answers `403`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The ambassador's profile UUID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The analytics (bare object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralAnalytics"
                }
              }
            }
          },
          "400": {
            "description": "`id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/referrals/codes": {
      "post": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get or create my referral code",
        "operationId": "getOrCreateReferralCode",
        "description": "Returns the caller's referral code — the profile-level one, or the one for `fundraiser_id` — creating it on first use. Idempotent. Share links take the form `/r/{code}` on the site. Every signed-in user can hold a code; the ambassador role is not required.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "fundraiser_id": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Omit or `null` for the profile-level code."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The code",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "example": "aB3dE5fG7h"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`fundraiser_id` is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_body"
                      ]
                    },
                    "details": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "The code could not be created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/referrals/clicks": {
      "post": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Record a referral-link click",
        "operationId": "recordReferralClick",
        "description": "Called by the FundlyHub web app when a visitor opens a referral link (`/r/{code}`); not for third-party clients. Returns where to redirect and the visitor id to set as a cookie. Records a click classified as human, bot or unknown, and stamps the code into `utm_content`. When the visitor is signed in, the code is also remembered on their profile for attribution.\n\nAn unknown code still answers `200` with `resolved: false` and a `target_path` of the home page.\n\nAuthentication is optional. Rate limited per caller: 120 requests/minute, on top of the global per-IP limiter. Past the limit the answer is `429` with `Retry-After`.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  },
                  "user_agent": {
                    "type": "string",
                    "nullable": true
                  },
                  "request_method": {
                    "type": "string",
                    "maxLength": 8,
                    "default": "GET"
                  },
                  "referer": {
                    "type": "string",
                    "nullable": true
                  },
                  "visitor_id": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Generated when absent."
                  },
                  "ip": {
                    "type": "string",
                    "nullable": true,
                    "description": "The visitor's address. Stored only as a hash."
                  },
                  "utm_source": {
                    "type": "string",
                    "nullable": true
                  },
                  "utm_medium": {
                    "type": "string",
                    "nullable": true
                  },
                  "utm_campaign": {
                    "type": "string",
                    "nullable": true
                  },
                  "utm_content": {
                    "type": "string",
                    "nullable": true
                  },
                  "entry_point": {
                    "type": "string",
                    "enum": [
                      "redirect",
                      "landing"
                    ],
                    "default": "redirect"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Click processed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReferralClickResult"
                }
              }
            }
          },
          "400": {
            "description": "Body failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "invalid_body"
                      ]
                    },
                    "details": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Recording failed. The body still carries a safe fallback (`target_path: \"/\"`) so the caller can redirect.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ReferralClickResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/summary": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get my referral summary",
        "operationId": "getMyReferralSummary",
        "description": "The ambassador portal's headline: totals, the click-to-gift funnel and a daily series for the caller's own referral links over a window (default: the 28 days ending `to`). Every portal route reads only the session's own rows.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start. Defaults to 28 days before `to`. Must be before `to` and not earlier than 2020-01-01.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AmbassadorPortalSummary"
                    },
                    "range": {
                      "$ref": "#/components/schemas/AmbassadorPortalRange"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is not a date, out of order, or before 2020-01-01",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "from must be before to"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "invalid_range"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/campaigns": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "List campaigns I referred to",
        "operationId": "getMyReferredCampaigns",
        "description": "Per-campaign clicks, visitors, driven gifts and money raised from the caller's referral links in the window.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start. Defaults to 28 days before `to`. Must be before `to` and not earlier than 2020-01-01.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "fundraiser_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "fundraiser_title": {
                            "type": "string",
                            "nullable": true
                          },
                          "fundraiser_slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "clicks_human": {
                            "type": "integer"
                          },
                          "clicks_bot": {
                            "type": "integer"
                          },
                          "clicks_unknown": {
                            "type": "integer"
                          },
                          "distinct_human_visitors": {
                            "type": "integer"
                          },
                          "driven_gifts": {
                            "type": "integer"
                          },
                          "raised_cents": {
                            "type": "integer"
                          },
                          "last_click_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "range": {
                      "$ref": "#/components/schemas/AmbassadorPortalRange"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is not a date, out of order, or before 2020-01-01",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "from must be before to"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "invalid_range"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/campaigns/{fundraiserId}": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get what my referral link did for one campaign",
        "operationId": "getMyCampaignReferralStats",
        "description": "All-time figures for the caller's own referral link on one campaign: impressions (visits not identified as a bot), clicks by traffic type, distinct human visitors, driven gifts (paid, not self-referred), distinct donors, and the value of those gifts (donation + tip) per currency in cents. The same definitions as the per-campaign rows of the ambassador analytics. Also returns the caller's referral code and link for the campaign when one exists; this read never creates one. Feeds the ambassador bar on the campaign page.\n\nA campaign the caller may not open (draft, pending, private or deleted, and not their own) is a 404, as its page is.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "fundraiserId",
            "in": "path",
            "required": true,
            "description": "The campaign's id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's figures for the campaign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "fundraiser_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "impressions": {
                          "type": "integer",
                          "description": "Visits through the link that were not identified as a bot."
                        },
                        "clicks_by_traffic_type": {
                          "type": "object",
                          "properties": {
                            "human": {
                              "type": "integer"
                            },
                            "bot": {
                              "type": "integer"
                            },
                            "unknown": {
                              "type": "integer"
                            }
                          }
                        },
                        "distinct_human_visitors": {
                          "type": "integer"
                        },
                        "driven_gifts": {
                          "type": "integer"
                        },
                        "distinct_donors": {
                          "type": "integer"
                        },
                        "funds_attributed": {
                          "type": "array",
                          "description": "Donation + tip per currency, in cents, largest first.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "currency": {
                                "type": "string"
                              },
                              "amount_cents": {
                                "type": "integer"
                              }
                            }
                          }
                        },
                        "referral_code": {
                          "type": "string",
                          "nullable": true
                        },
                        "referral_url": {
                          "type": "string",
                          "nullable": true,
                          "example": "https://fundlyhub.org/r/AbC123xYz0"
                        },
                        "last_click_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "last_gift_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the figures were read."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`fundraiserId` is not a uuid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/traffic": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get my referral traffic breakdown",
        "operationId": "getMyReferralTraffic",
        "description": "Where the caller's referral clicks came from — by UTM source, referer and medium — plus traffic type, and country and device splits from Google Analytics when it is configured (`ga_note` says why those are `null` otherwise).\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start. Defaults to 28 days before `to`. Must be before `to` and not earlier than 2020-01-01.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AmbassadorPortalTraffic"
                    },
                    "range": {
                      "$ref": "#/components/schemas/AmbassadorPortalRange"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is not a date, out of order, or before 2020-01-01",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "from must be before to"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "invalid_range"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/gifts": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "List gifts I drove",
        "operationId": "getMyReferredGifts",
        "description": "The donations attributed to the caller's referral links in the window, refunded, failed and self-referred gifts included. Each row carries exactly the fields of `AmbassadorGift`: no donor email, card details or receipt reference, and no donor name on an anonymous gift.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start. Defaults to 28 days before `to`. Must be before `to` and not earlier than 2020-01-01.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of gifts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AmbassadorGift"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "drivenTotal": {
                      "type": "integer"
                    },
                    "range": {
                      "$ref": "#/components/schemas/AmbassadorPortalRange"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is not a date, out of order, or before 2020-01-01",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "from must be before to"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "invalid_range"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/referrals/standing": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "Get my ambassador standing",
        "operationId": "getMyReferralStanding",
        "description": "The caller's rank among ambassadors by money raised in the window, and the leaderboard (up to 200 rows; `truncated` when there are more). Other ambassadors with private profiles appear without name or avatar.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Window start. Defaults to 28 days before `to`. Must be before `to` and not earlier than 2020-01-01.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Window end. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The standing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "rank": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Null when the caller has no ranked activity in the window."
                        },
                        "total_ambassadors": {
                          "type": "integer"
                        },
                        "leaderboard": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "rank": {
                                "type": "integer"
                              },
                              "ambassador_user_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "display_name": {
                                "type": "string",
                                "nullable": true
                              },
                              "avatar_url": {
                                "type": "string",
                                "nullable": true
                              },
                              "raised_cents": {
                                "type": "integer"
                              },
                              "driven_gifts": {
                                "type": "integer"
                              },
                              "is_me": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "truncated": {
                          "type": "boolean"
                        }
                      }
                    },
                    "range": {
                      "$ref": "#/components/schemas/AmbassadorPortalRange"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`from` or `to` is not a date, out of order, or before 2020-01-01",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "from must be before to"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "invalid_range"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/campaigns": {
      "get": {
        "tags": [
          "Ambassadors"
        ],
        "summary": "List my own campaigns (ambassador portal)",
        "operationId": "getMyOwnedCampaignsForPortal",
        "description": "The campaigns the caller owns, with totals and how many clicks on their own referral links led to them (self-referrals, which do not count as driven). Takes no date range.\n\n**Requires permission:** `view_own_referral_portal`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's campaigns",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true
                          },
                          "slug": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "nullable": true
                          },
                          "goal_amount_cents": {
                            "type": "integer"
                          },
                          "total_raised_cents": {
                            "type": "integer"
                          },
                          "donation_count": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "self_referred_clicks": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/dmca/notice": {
      "post": {
        "tags": [
          "DMCA"
        ],
        "summary": "File a DMCA takedown notice",
        "operationId": "submitDmcaNotice",
        "description": "Files a §512(c) takedown notice against a campaign or one of its media files. No account is needed to file. Both attestations must be `true`, and either `mediaId` or `fundraiserId` is required. Returns only a reference id; the claimant is emailed a confirmation and updates.\n\nNo authentication. Behind `features.dmca_workflow`, which is **currently disabled** (`403`). Rate limited at 5 requests/minute per IP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claimantName",
                  "claimantEmail",
                  "claimantAddress",
                  "infringingUrl",
                  "copyrightedWorkDescription",
                  "electronicSignature",
                  "goodFaithStatement",
                  "authorityStatement"
                ],
                "properties": {
                  "claimantName": {
                    "type": "string"
                  },
                  "claimantOrganization": {
                    "type": "string",
                    "nullable": true
                  },
                  "claimantEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "claimantPhone": {
                    "type": "string",
                    "nullable": true
                  },
                  "claimantAddress": {
                    "type": "string"
                  },
                  "mediaId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "The media file. When given, the campaign is derived from it."
                  },
                  "fundraiserId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  },
                  "infringingUrl": {
                    "type": "string"
                  },
                  "copyrightedWorkDescription": {
                    "type": "string"
                  },
                  "copyrightedWorkUrl": {
                    "type": "string",
                    "nullable": true
                  },
                  "goodFaithStatement": {
                    "type": "boolean",
                    "description": "Must be `true` (§512(c)(3)(A)(v))."
                  },
                  "authorityStatement": {
                    "type": "boolean",
                    "description": "Must be `true` (§512(c)(3)(A)(vi))."
                  },
                  "electronicSignature": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Notice received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "complaintId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "submittedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing, an attestation is false, no target was given, or `mediaId` / `fundraiserId` is not a valid id (`code: INVALID_INPUT`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "403": {
            "description": "`features.dmca_workflow` is disabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`mediaId` does not match a media file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Unexpected server error (`Internal server error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/dmca/complaints/{id}/counter-notice": {
      "post": {
        "tags": [
          "DMCA"
        ],
        "summary": "File a DMCA counter-notice",
        "operationId": "submitDmcaCounterNotice",
        "description": "The uploader of media taken down under a notice files a §512(g) counter-notice. Only the media's original uploader may file, and only against a complaint in `valid` status. All three statements must be `true`. FundlyHub forwards it to the claimant.\n\nBehind `features.dmca_workflow`, which is **currently disabled** (`403`).",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The complaint id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "goodFaithStatement",
                  "consentToJurisdiction",
                  "perjuryStatement",
                  "electronicSignature",
                  "contactName",
                  "contactAddress",
                  "contactPhone",
                  "contactEmail"
                ],
                "properties": {
                  "goodFaithStatement": {
                    "type": "boolean",
                    "description": "Must be `true` (§512(g)(3)(C))."
                  },
                  "consentToJurisdiction": {
                    "type": "boolean",
                    "description": "Must be `true` (§512(g)(3)(D))."
                  },
                  "perjuryStatement": {
                    "type": "boolean",
                    "description": "Must be `true` (§512(g)(3)(A))."
                  },
                  "electronicSignature": {
                    "type": "string"
                  },
                  "contactName": {
                    "type": "string"
                  },
                  "contactAddress": {
                    "type": "string"
                  },
                  "contactPhone": {
                    "type": "string"
                  },
                  "contactEmail": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Counter-notice received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "counterNoticeId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "submittedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A field is missing or false, or the complaint has no media attached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The feature flag is off, or the caller did not upload the media",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "404": {
            "description": "No such complaint (including an `id` that is not a valid id), or its media no longer exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "409": {
            "description": "The complaint is not in `valid` status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DmcaError"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error (`Internal server error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/achievements": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get a profile's achievements",
        "operationId": "getUserAchievements",
        "description": "The achievements section of a profile: the badges the person has earned that visitors may see, each with its card, plus a public holder block, a summary counted over the returned rows, the profile's pinned card and the label vocabulary. `id` is a UUID or a profile slug.\n\nThe answer is the same for everyone, the owner included, except that the owner's own rows carry their counts (`stats`). A private or inactive profile read by anyone but its owner answers `{ \"data\": [], \"withheld\": true, \"vocabulary\": … }`.\n\nAuthentication is optional. Rate limited at 300 requests/minute per IP.",
        "security": [
          {
            "BearerAuth": []
          },
          {}
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Profile UUID or profile slug.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Locale for titles and labels; unsupported locales fall back to English.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The collection, or a withheld answer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AchievementView"
                      }
                    },
                    "withheld": {
                      "type": "boolean",
                      "description": "Present (true) only when the profile is private or inactive."
                    },
                    "holder": {
                      "$ref": "#/components/schemas/AchievementHolder"
                    },
                    "summary": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Counts over `data` (by tier, rarity and series). Shape is dynamic."
                    },
                    "pin": {
                      "$ref": "#/components/schemas/AchievementPin"
                    },
                    "vocabulary": {
                      "$ref": "#/components/schemas/AchievementVocabulary"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/achievements": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get my achievements",
        "operationId": "getMyAchievements",
        "description": "The owner view: every achievement the caller holds — themselves and through organizations they administer — including those set to \"only me\", with private stats and progress; `next_up`, the closest badges not yet earned; the caller's pin; and how many new cards are waiting to be revealed.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The owner collection",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AchievementView"
                      }
                    },
                    "holder": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AchievementHolder"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "full_name": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      ]
                    },
                    "summary": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "next_up": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "pin": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AchievementPin"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "publicly_readable": {
                              "type": "boolean",
                              "nullable": true,
                              "description": "Whether visitors can open the pinned card; null for the automatic choice."
                            }
                          }
                        }
                      ]
                    },
                    "pin_writable": {
                      "type": "boolean"
                    },
                    "unrevealed_count": {
                      "type": "integer"
                    },
                    "vocabulary": {
                      "$ref": "#/components/schemas/AchievementVocabulary"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/achievements/pin": {
      "put": {
        "tags": [
          "Achievements"
        ],
        "summary": "Pin an achievement card",
        "operationId": "pinMyAchievementCard",
        "description": "Pins one of the caller's own live cards (theirs, or an organization's they administer) to that holder's profile. A malformed, unknown, void or someone else's card all get the **same** `404` body, so the answer never says whether a code exists. Responses are `private, no-store`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "card_code"
                ],
                "properties": {
                  "card_code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pinned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "pin": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/AchievementPin"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "publicly_readable": {
                                  "type": "boolean"
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`card_code` missing or not a string",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Card not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "500": {
            "description": "Pin failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Achievements"
        ],
        "summary": "Unpin an achievement card",
        "operationId": "unpinMyAchievementCard",
        "description": "Clears the caller's pin, or with `organization` that organization's pin when the caller administers it. Idempotent. The profile then shows the automatic choice.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "organization",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Unpinned"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller does not administer that organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "500": {
            "description": "Unpin failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/achievements/reveals": {
      "get": {
        "tags": [
          "Achievements"
        ],
        "summary": "Get my reveal pack",
        "operationId": "getMyAchievementReveals",
        "description": "Cards waiting to be \"opened\": with `receipt`, the ones a particular donation earned (the thank-you page — `status: pending` while the award is still being computed); with `all=true`, every unrevealed card. Pass exactly one. Someone else's receipt answers like a donation that earned nothing (`status: none`). Responses are `private, no-store`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "receipt",
            "in": "query",
            "required": false,
            "description": "A Stripe PaymentIntent id.",
            "schema": {
              "type": "string",
              "pattern": "^pi_[A-Za-z0-9_]{1,255}$"
            }
          },
          {
            "name": "all",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pack",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "ready",
                            "none"
                          ]
                        },
                        "events": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "integer"
                              },
                              "event_ids": {
                                "type": "array",
                                "items": {
                                  "type": "integer"
                                },
                                "description": "Pass these to `POST /me/achievements/reveals/ack`."
                              },
                              "kind": {
                                "type": "string",
                                "enum": [
                                  "issued",
                                  "upgraded",
                                  "reawarded"
                                ]
                              },
                              "tier": {
                                "type": "object",
                                "properties": {
                                  "key": {
                                    "type": "string"
                                  },
                                  "label": {
                                    "type": "string"
                                  }
                                }
                              },
                              "from_tier": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "key": {
                                    "type": "string"
                                  },
                                  "label": {
                                    "type": "string"
                                  }
                                }
                              },
                              "revealed": {
                                "type": "boolean"
                              },
                              "card": {
                                "type": "object",
                                "additionalProperties": true,
                                "description": "The card as drawn (code, art, numbering, dates)."
                              },
                              "tierup_line": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither or both of `receipt` and `all`, or a malformed `receipt`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/achievements/reveals/ack": {
      "post": {
        "tags": [
          "Achievements"
        ],
        "summary": "Mark reveal events as opened",
        "operationId": "ackMyAchievementReveals",
        "description": "Marks the caller's own reveal events as revealed. Ids that are not the caller's are ignored silently; the answer is `204` either way.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event_ids"
                ],
                "properties": {
                  "event_ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "items": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Acknowledged"
          },
          "400": {
            "description": "`event_ids` is not 1–200 positive integers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Acknowledgement failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me/achievements/{slug}": {
      "patch": {
        "tags": [
          "Achievements"
        ],
        "summary": "Set an achievement's visibility",
        "operationId": "updateMyAchievementVisibility",
        "description": "The holder's \"Visible to: Everyone / Only me\" control for one badge. `default` follows the badge's own visibility. When the caller wears the badge both personally and through an organization (or through two organizations), `holder` must say which; otherwise `409` with the list of `holders`.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The achievement's slug.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "visibility_override"
                ],
                "properties": {
                  "visibility_override": {
                    "type": "string",
                    "enum": [
                      "default",
                      "public",
                      "private"
                    ]
                  },
                  "holder": {
                    "type": "object",
                    "description": "A malformed value is ignored rather than rejected.",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "user",
                          "organization"
                        ]
                      },
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated award",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AchievementView"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`visibility_override` missing or invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "The caller does not hold this achievement (as that holder)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodedError"
                }
              }
            }
          },
          "409": {
            "description": "Several of the caller's holders wear it; name one",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/CodedError"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "holders": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "user",
                                  "organization"
                                ]
                              },
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Update failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/chatwoot/identity-hash": {
      "get": {
        "tags": [
          "Support"
        ],
        "summary": "Get the live-chat identity hash",
        "operationId": "getChatwootIdentityHash",
        "description": "The caller's identity hash for the support chat widget's `setUser(…, { identifier_hash })`, so the chat knows which signed-in user it is talking to.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The hash",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "identifier_hash": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Hashing failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Identity validation is not configured on this deployment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}