components:
  schemas:
    CheckoutLinkResponseDto:
      properties:
        amount:
          description: >-
            The amount the link was minted for, in the unit described on the create request. **This is what you asked
            for, not what was collected** — the checkout lets the donor change the amount. Reconcile against
            `amount_paid`.
          example: 50000
          type: number
        amount_paid:
          description: >-
            What the donor actually paid, in the vaki’s own currency. Present only when `status` is `completed`. **This,
            not `amount`, is the authoritative figure** — the checkout amount is donor-editable, so a link minted for 50
            000 can settle at 30 000.
          example: 50000
          type: number
        anonymous:
          description: Whether the resulting contribution is shown without the donor’s name.
          example: false
          type: boolean
        callback_url:
          description: >-
            The callback URL stored with this link. Its presence records what we will notify **once outbound delivery
            exists** — nothing is delivered today. Do not read this as evidence that a callback was sent.
          example: https://sillega.co/api/vaki/callback
          type: string
        cancel_url:
          description: Where the donor is sent if they abandon the checkout.
          example: https://sillega.co/donar
          type: string
        completed_at:
          description: When the payment was approved. RFC 3339.
          example: '2026-08-17T14:32:10.000Z'
          type: string
        created_at:
          description: RFC 3339.
          example: '2026-08-17T14:00:00.000Z'
          type: string
        currency:
          description: ISO 4217 code.
          example: COP
          type: string
        currency_paid:
          description: Currency the payment settled in — the vaki’s currency.
          example: COP
          type: string
        email:
          description: The donor email you pre-filled, if any.
          example: donante@example.com
          type: string
        expires_at:
          description: After this instant an unpaid link reports `expired` and must not be used. RFC 3339.
          example: '2026-08-18T14:00:00.000Z'
          type: string
        external_reference:
          description: Your own reference, echoed back unchanged.
          example: DON_59LPC74YV
          type: string
        fee_percent:
          description: >-
            Vaki's commission on this donation, as a **percentage** — `5` means 5%. Gateway costs are not added on top
            and are never charged to the donor.
          example: 5
          type: number
        id:
          description: >-
            Opaque identifier. Also the value the checkout URL carries in `?ref=`, and what a payment is joined back to
            this link by. Treat it as an opaque string — do not parse it or assume a length.
          example: chl_01K2WQ8M0PZJ4T7VN3RXBGYD5C
          type: string
        metadata:
          description: The metadata you sent, echoed back unchanged. Never interpreted by us.
          example:
            need_id: N_2QFBQFJTY
          type: object
        object:
          description: Always `checkout_link`, so this resource can be discriminated in a union.
          example: checkout_link
          type: string
        payment_id:
          description: Id of the approved payment that completed this link. Present only when `status` is `completed`.
          example: aQ8kZ2mB1nP4rT6vX9wY
          type: string
        status:
          description: >-
            Lifecycle. `open` → `completed` | `expired` | `cancelled`, and the three terminal states never change again.
            Derived live on every read: an unpaid link past `expires_at` reports `expired` whether or not anything has
            swept it.
          enum:
            - open
            - completed
            - expired
            - cancelled
          example: open
          type: string
        success_url:
          description: Where the donor is sent after a successful payment.
          example: https://sillega.co/gracias
          type: string
        url:
          description: >-
            Send the donor here. **Store this value; never rebuild it.** The checkout host is ours to change and a link
            constructed by hand will not carry the `?ref=` that makes the donation traceable back to you.
          example: https://vaki.co/checkout/clinicalaliga?currency=COP&value=50000&ref=chl_01K2WQ8M0PZJ4T7VN3RXBGYD5C
          type: string
        vaki:
          description: Key of the vaki being funded.
          example: clinicalaliga
          type: string
      required:
        - id
        - object
        - url
        - status
        - vaki
        - amount
        - currency
        - fee_percent
        - expires_at
        - created_at
      type: object
    CreateCheckoutLinkDto:
      properties:
        amount:
          description: |-
            How much to collect, as a **positive integer in the major unit of `currency`** — not in minor units.

            `50000` with `currency: "COP"` is fifty thousand pesos. COP has no minor unit anywhere in Vaki —
            there are no centavos to express, so an integer is the whole story. USD *does* have a minor unit and
            v1 does not expose it: `50` with `currency: "USD"` is fifty dollars, and USD 12,50 cannot currently be
            requested. This is a real v1 limitation, not a rounding convention — request in COP if you need
            sub-dollar precision.

            Gateway minimums (PSE COP 1.600, card USD 0,50) apply when the donor pays, not when the link is minted.

            Note this is what you *ask* for, not necessarily what you collect: the checkout lets the donor change
            the amount. Reconcile against `amount_paid` on `GET /v1/checkout_links/{id}`.
          example: 50000
          maximum: 1000000000000
          minimum: 1
          type: number
        anonymous:
          description: Show the resulting contribution without the donor’s name on the public vaki page.
          example: false
          type: boolean
        callback_url:
          description: >-
            Where we will POST the `checkout_link.completed` event.


            **Nothing is delivered to this URL yet — outbound webhook delivery is Phase 1 and is out of scope in v1.**
            It is accepted, validated and stored from day one so your integration code does not change when delivery
            lands, but a stored value is not evidence that anything was ever sent. Until delivery exists, poll `GET
            /v1/checkout_links/{id}`: it derives `completed` from the actual approved payment rather than from a cached
            flag.


            Must be an absolute `https://` URL.
          example: https://sillega.co/api/vaki/callback
          type: string
        cancel_url:
          description: Where the donor is sent if they abandon the checkout. Absolute `https://` URL.
          example: https://sillega.co/donar
          type: string
        currency:
          description: >-
            ISO 4217 code. Must be the currency the vaki collects in — a vaki has exactly one, and a mismatch is
            rejected rather than converted, so the amount you request is the amount the donor is shown.
          enum:
            - ARS
            - AUD
            - BOB
            - BRB
            - BRL
            - CAD
            - CLP
            - COP
            - CRC
            - DOP
            - EUR
            - GBP
            - GTQ
            - HNL
            - MXN
            - NIO
            - PEN
            - PYG
            - USD
            - UYU
          example: COP
          type: string
        email:
          description: >-
            Pre-fill the donor’s email. Optional — the checkout collects it anyway, so sending it only saves the donor a
            step.
          example: donante@example.com
          type: string
        external_reference:
          description: >-
            Your own reference for this donation. Stored, echoed back on reads, and exported in the creator’s payments
            CSV so both sides can reconcile.


            **It never appears in the checkout URL.** The URL carries our `chl_…` id and nothing else that matters; your
            reference, the callback URL and the fee are all read server-side from the stored link. A donor can edit the
            query string, so nothing that travels in it is allowed to decide anything.


            Charset `[A-Za-z0-9_-]`, up to 64 characters — the same rule the existing `?ref=` param enforces, so a value
            accepted here survives every downstream system (one payment gateway rejects diacritics outright).
          example: DON_59LPC74YV
          type: string
        metadata:
          additionalProperties:
            type: string
          description: >-
            Free-form string map, stored and echoed back unchanged. Never interpreted by us and never shown to the
            donor. At most 20 keys, keys up to 40 characters, values up to 500.
          example:
            need_id: N_2QFBQFJTY
          type: object
        success_url:
          description: Where the donor is sent after a successful payment. Absolute `https://` URL.
          example: https://sillega.co/gracias
          type: string
        vaki:
          description: >-
            The vaki to fund — its `key`, the segment in `vaki.co/checkout/{key}`. The slug from the public page URL is
            accepted too and resolved to the key; the response always echoes the canonical `key`.
          example: clinicalaliga
          type: string
      required:
        - vaki
        - amount
        - currency
      type: object
    CreatePaymentIntentDto:
      properties:
        email:
          description: >-
            Donor email, used for the Stripe receipt. Optional — the checkout collects it anyway, and it does not affect
            what is charged.
          example: donante@example.com
          type: string
      type: object
    CreateVakiDto:
      properties:
        category:
          description: >-
            Vaki category id, e.g. `no-profit`. Stored as sent; v1 does not validate it against the category catalog, so
            a typo produces a cause with an unrecognised category rather than an error. Validation is a documented
            follow-up.
          example: no-profit
          type: string
        country:
          description: ISO 3166-1 alpha-2 country the cause collects in. Case-insensitive.
          example: CO
          type: string
        cover_image_url:
          description: >-
            HTTPS URL of the cover image. Referenced as-is in v1 — Vaki does not copy it, so it must stay reachable for
            as long as the cause is live.
          example: https://cdn.sillega.co/causes/hospital.jpg
          type: string
        description:
          description: Long-form story shown on the cause page. Plain text; markup is escaped.
          maxLength: 5000
          type: string
        external_reference:
          description: >-
            Your own identifier for this cause. Stored and echoed back on reads so you can reconcile without keeping a
            mapping table.
          example: N_2QFBQFJTY
          maxLength: 255
          type: string
        fee_percent:
          description: >-
            Vaki’s commission on every donation to this cause, as a **percentage** (`5` means 5%, not 0.05). Defaults to
            the published API rate. Sending anything other than that rate requires the `vakis:set_fee` permission on
            your key; without it the request is rejected with `403 fee_percent_not_permitted`.
          example: 5
          maximum: 20
          minimum: 0
          type: number
        goal:
          allOf:
            - $ref: '#/components/schemas/CreateVakiGoalDto'
          description: Fundraising target. Omit for a cause with no target (Vaki still collects).
        owner:
          allOf:
            - $ref: '#/components/schemas/CreateVakiOwnerDto'
          description: The real creator of the cause — the responsible person at the beneficiary organisation. Required.
        state:
          default: draft
          description: >-
            State to create the cause in. `draft` is the only accepted value in v1 — a draft is private and is published
            from the Vaki dashboard, so a partner cannot put an unreviewed cause live.
          enum:
            - draft
          type: string
        terms_accepted_at:
          description: >-
            When the owner accepted Vaki’s terms on your form, as an RFC 3339 / ISO 8601 timestamp **including the UTC
            offset** (`2026-08-17T09:14:22-05:00` or `…Z`). An offsetless timestamp is rejected: without it we cannot
            say what moment was recorded. Must not be in the future.
          example: '2026-08-17T09:14:22-05:00'
          type: string
        terms_accepted_ip:
          description: >-
            IP address the acceptance was submitted from. Evidence of the acceptance, not an identifier we use for
            anything else.
          example: 190.85.12.7
          type: string
        terms_version:
          description: >-
            Identifier of the terms text the owner was shown, as you version it. Stored verbatim so an acceptance can be
            tied back to the exact wording.
          example: '2026-08-01'
          maxLength: 60
          type: string
        title:
          description: Public title of the cause.
          example: Ayuda para el Hospital San Jorge
          maxLength: 150
          type: string
      required:
        - title
        - country
        - owner
        - terms_accepted_at
        - terms_version
        - terms_accepted_ip
      type: object
    CreateVakiGoalDto:
      properties:
        amount:
          description: >-
            Fundraising target in the currency’s smallest customary unit as Vaki stores it (COP has no cents: 50000000
            is fifty million pesos). `0` means "no target".
          example: 50000000
          maximum: 999999999999999
          minimum: 0
          type: number
        currency:
          description: >-
            ISO 4217 code the cause collects in. Defaults to `COP`. This becomes the vaki’s settlement currency and
            every donation to it is stored in that currency, so it cannot be changed later without support — and `POST
            /v1/checkout_links` will only mint links in it.
          enum:
            - ARS
            - AUD
            - BOB
            - BRB
            - BRL
            - CAD
            - CLP
            - COP
            - CRC
            - DOP
            - EUR
            - GBP
            - GTQ
            - HNL
            - MXN
            - NIO
            - PEN
            - PYG
            - USD
            - UYU
          example: COP
          type: string
      required:
        - amount
      type: object
    CreateVakiOwnerDto:
      properties:
        email:
          description: >-
            Email of the person who will own the cause. If they already have a Vaki account it is used; if not, one is
            provisioned for them (requires `creators:provision`) and they are emailed about it.
          example: contacto@hospitalsanjorge.org
          type: string
        name:
          description: >-
            Full name of the creator. Required: it is what appears on the cause and in the email telling them an account
            exists in their name.
          example: Ana María Restrepo
          maxLength: 120
          type: string
        phone:
          description: Contact phone in E.164, if you have it. Shown to Vaki support, never to donors.
          example: '+573001112233'
          type: string
      required:
        - email
        - name
      type: object
    PaymentIntentResponseDto:
      properties:
        client_secret:
          description: >-
            Pass to `stripe.confirmPayment` / the Payment Element. Scoped to this one PaymentIntent and useless for
            anything else.
          example: pi_3QxYz2Fv0YLOxoQn1AbCdEfG_secret_9XyZaBcDeFgHiJkLmNoPqRsTu
          type: string
        payment_intent_id:
          description: >-
            The PaymentIntent id. Stable for the lifetime of the link: calling this endpoint again returns the same id
            while the intent is still unconfirmed, so a donor who reloads or double-taps does not open a second one.
          example: pi_3QxYz2Fv0YLOxoQn1AbCdEfG
          type: string
        publishable_key:
          description: >-
            The publishable key for the Stripe account that will process this payment. **Read it from here rather than
            hardcoding it** — which account serves a link is a server-side decision, and returning it is what lets Vaki
            route a partner to a different Stripe entity later without a client release.
          example: pk_test_51Mz3HYFv0YLOxoQn…
          type: string
      required:
        - client_secret
        - publishable_key
        - payment_intent_id
      type: object
    ProblemDetails:
      properties:
        code:
          description: Stable machine-readable error code. This is the member to branch on; it never changes for a given condition.
          enum:
            - unauthorized
            - forbidden
            - insufficient_permissions
            - validation_failed
            - unsupported_media_type
            - idempotency_key_reused
            - idempotency_key_in_progress
            - rate_limit_exceeded
            - not_found
            - vaki_not_found
            - checkout_link_not_found
            - checkout_link_not_payable
            - owner_not_found
            - vaki_not_accepting_payments
            - currency_not_supported
            - fee_percent_not_permitted
            - not_implemented
            - internal_error
          example: vaki_not_found
          type: string
        detail:
          description: Human-readable explanation specific to this occurrence. May include request-specific detail.
          example: No vaki exists with key "clinicalaliga".
          type: string
        errors:
          description: Per-field validation failures. Present only on validation errors.
          items:
            properties:
              field:
                example: amount
                type: string
              message:
                example: amount must be a positive integer
                type: string
            type: object
          type: array
        instance:
          description: URI reference identifying this specific occurrence — the request path that produced it.
          example: /v1/vakis/clinicalaliga
          type: string
        status:
          description: The HTTP status code, repeated here for convenience.
          example: 404
          type: number
        title:
          description: >-
            Short, human-readable summary of the problem type. Stable per type, but for humans — branch on `code`, not
            on this.
          example: Vaki not found
          type: string
        type:
          description: URI identifying the problem type. Resolves to the docs page for this error.
          example: https://developers.vaki.co/errors/vaki_not_found
          type: string
      required:
        - type
        - title
        - status
        - detail
        - code
        - instance
      type: object
    PublicCheckoutLinkBrandingDto:
      properties:
        accent_color:
          description: Accent colour as `#RRGGBB`. Guaranteed to match that pattern exactly, so it is safe to interpolate into CSS.
          example: '#1F6FEB'
          type: string
        display_name:
          description: Partner name to show as the protagonist of the checkout. Plain text, at most 60 characters.
          example: Síllega
          type: string
        logo_url:
          description: Absolute `https://` logo URL. Never a `data:` or `javascript:` URI.
          example: https://sillega.co/assets/logo.svg
          type: string
      type: object
    PublicCheckoutLinkResponseDto:
      properties:
        amount:
          description: >-
            Amount the link was minted for, as an integer in the major unit of `currency`. A donor may choose to pay a
            different amount.
          example: 50000
          type: number
        amount_paid:
          description: >-
            What the donor actually paid, in the vaki’s currency. Present only when `status` is `completed`. May differ
            from `amount` — the donor can change it.
          example: 50000
          type: number
        branding:
          allOf:
            - $ref: '#/components/schemas/PublicCheckoutLinkBrandingDto'
          description: >-
            The partner’s branding, so the checkout can present them as the protagonist. **Partner-supplied, publicly
            visible content** — configured on their client record, validated on write and sanitized on read. Omitted
            entirely when the partner has configured none, in which case render the Vaki default.
        currency:
          description: ISO 4217 code.
          example: COP
          type: string
        expires_at:
          description: After this instant the link reports `expired` and must not be paid. RFC 3339.
          example: '2026-08-18T14:00:00.000Z'
          type: string
        id:
          description: The checkout link id, echoing the one in the path.
          example: chl_01K2WQ8M0PZJ4T7VN3RXBGYD5C
          type: string
        payment_id:
          description: >-
            Id of the approved payment. Present only when `status` is `completed`, so the confirmation screen can show a
            receipt reference.
          example: aQ8kZ2mB1nP4rT6vX9wY
          type: string
        status:
          description: Lifecycle state, derived live. Render the payment form only while this is `open`.
          enum:
            - open
            - completed
            - expired
            - cancelled
          example: open
          type: string
        vaki:
          allOf:
            - $ref: '#/components/schemas/PublicCheckoutLinkVakiDto'
          description: Just enough of the cause to render the page.
      required:
        - id
        - status
        - amount
        - currency
        - vaki
        - expires_at
      type: object
    PublicCheckoutLinkVakiDto:
      properties:
        image:
          description: Public cover image URL. Empty string when the cause has none.
          example: https://storage.googleapis.com/vaki-public/…/cover.jpg
          type: string
        key:
          description: The vaki key.
          example: clinicalaliga
          type: string
        name:
          description: Public display name of the cause.
          example: Clínica La Liga
          type: string
      required:
        - key
        - name
        - image
      type: object
    VakiFinanceResource:
      properties:
        currency:
          description: Currency the cause collects and settles in (ISO 4217).
          example: COP
          type: string
        fee_percent:
          description: Vaki’s commission on each donation, as a percentage (`5` = 5%). Gateway costs are separate.
          example: 5
          type: number
      required:
        - currency
        - fee_percent
      type: object
    VakiGoalResource:
      properties:
        amount:
          example: 50000000
          type: number
        currency:
          example: COP
          type: string
      required:
        - amount
        - currency
      type: object
    VakiOwnerResource:
      properties:
        email:
          description: Email of the Vaki account that owns the cause.
          example: bruno@sillega.co
          type: string
      required:
        - email
      type: object
    VakiResource:
      properties:
        category:
          example: no-profit
          type: string
        closes_at:
          description: RFC 3339 timestamp the cause stops collecting, when one is set.
          example: '2026-10-01T14:00:00.000Z'
          type: string
        country:
          description: ISO 3166-1 alpha-2, lower-case as Vaki stores it.
          example: co
          type: string
        created_at:
          description: RFC 3339 timestamp the cause was created.
          example: '2026-08-17T14:00:00.000Z'
          type: string
        description:
          description: Long-form story, when set.
          type: string
        external_reference:
          description: Your own identifier, if you sent one when creating the cause.
          example: N_2QFBQFJTY
          type: string
        finance_data:
          $ref: '#/components/schemas/VakiFinanceResource'
        goal:
          $ref: '#/components/schemas/VakiGoalResource'
        key:
          description: Stable identifier of the cause. Use this in every other API path.
          example: aB3kQ9zXyL2mN7pR4sT1
          type: string
        object:
          description: Always `vaki`. Present so a client can tell objects apart in a heterogeneous list or webhook payload.
          example: vaki
          type: string
        owners:
          items:
            $ref: '#/components/schemas/VakiOwnerResource'
          type: array
        slug:
          description: >-
            URL segment of the cause on vaki.co. Equal to `key` at creation, but a creator can change it, so treat it as
            display data and key off `key`.
          example: aB3kQ9zXyL2mN7pR4sT1
          type: string
        state:
          description: >-
            Lifecycle state. `unknown` means the stored state has no published name yet — report it rather than
            branching on it.
          enum:
            - draft
            - created
            - published
            - verified
            - in_verification
            - rejected
            - confirming_contributions
            - in_disbursement
            - info_required
            - disbursed
            - invoiced
            - archived
            - not_activated
            - fraud
            - paused
            - refunding
            - pending_withdrawal
            - refunded
            - funds_on_hold
            - unknown
          example: draft
          type: string
        title:
          example: Ayuda para el Hospital San Jorge
          type: string
        totals:
          allOf:
            - $ref: '#/components/schemas/VakiTotalsResource'
          description: >-
            Public running totals. **Omitted entirely** when the creator has chosen to hide amounts on the cause page —
            an absent object means "not published", which is different from zero.
        url:
          description: Canonical public page. Send donors here.
          example: https://vaki.co/aB3kQ9zXyL2mN7pR4sT1
          type: string
      required:
        - key
        - object
        - slug
        - url
        - state
        - title
        - country
        - goal
        - finance_data
        - owners
        - created_at
      type: object
    VakiTotalsResource:
      properties:
        collected:
          description: Amount collected so far, in the cause’s currency. Approved donations only.
          example: 1250000
          type: number
        contributions:
          description: Number of approved donations.
          example: 42
          type: number
        contributors:
          description: Number of distinct donors. Lower than `contributions` when someone gives more than once.
          example: 38
          type: number
      required:
        - collected
        - contributions
        - contributors
      type: object
  securitySchemes:
    apiKeyAuth:
      description: 'Your API key, sent as `X-API-Key: vk_live_…`.'
      in: header
      name: X-API-Key
      type: apiKey
    bearerAuth:
      bearerFormat: Vaki API key
      description: 'Your API key, sent as `Authorization: Bearer vk_live_…`.'
      scheme: bearer
      type: http
info:
  contact: {}
  description: |-
    The Vaki public API. Create causes, mint checkout links and follow their status.

    Donors give on the local rail where they are — PSE or a card in Colombia, PayPal in
    Mexico — and by card from anywhere else in the world.

    A cause collects in exactly one currency, fixed when it is created. `POST /v1/checkout_links`
    rejects any other currency with `currency_not_supported` rather than applying an unquoted
    exchange rate, so read `finance_data.currency` from `GET /v1/vakis/{key}` instead of assuming
    `COP`.

    Authenticate every request with your live key, either as `Authorization: Bearer vk_live_…`
    or as `X-API-Key: vk_live_…`. Keys are scoped: a key can only reach the endpoints its
    permissions allow.

    Errors are RFC 9457 `application/problem+json`. Branch on the `code` member — it is
    stable forever; `title` and `detail` are for humans and may change.

    Retry any `POST` safely by sending an `Idempotency-Key` header: the first response for a
    given key is stored for 24 hours and replayed instead of creating a second object.

    The `url` on a checkout link is **opaque**. Store it and redirect the donor to it. Never
    construct it, never parse it, and never depend on its host: the hosted checkout is moving
    to `checkout.vaki.co`, and that migration is invisible to every partner who redirects to
    the value we returned instead of rebuilding it.

    Two servers are declared below. Production is live money; the sandbox is our staging
    deployment, which we redeploy constantly and whose data may be reset — see
    https://developers.vaki.co/get-started/sandbox/.
  title: Vaki API
  version: 1.0.0
openapi: 3.0.0
paths:
  /v1/checkout_links:
    post:
      description: >-
        Mints a hosted checkout URL for a vaki. Returns the URL to send the donor to; store the returned `url` rather
        than constructing it, so the checkout host can change without breaking your integration.
      operationId: createCheckoutLink
      parameters:
        - description: >-
            Send a unique value to make this POST safely retryable. The first response for a given key is replayed for
            24 hours.
          in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCheckoutLinkDto'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutLinkResponseDto'
          description: The checkout link was created.
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            The request body failed validation. The `errors` member lists the offending fields.

            Possible `code` values: `validation_failed`.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Missing or invalid API key.

            Possible `code` values: `unauthorized`.
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The key is valid but lacks the required permission, or the request came from an IP outside the key
            allowlist.


            Possible `code` values: `forbidden`.
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            A request with this `Idempotency-Key` is still being processed. Retry in a few seconds.

            Possible `code` values: `idempotency_key_in_progress`.
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The request was well-formed but could not be applied — the vaki does not exist, it cannot currently receive
            contributions, the currency is not the one it collects in, or the `Idempotency-Key` was already used with a
            different body.


            Possible `code` values: `vaki_not_found`, `vaki_not_accepting_payments`, `currency_not_supported`,
            `idempotency_key_reused`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      summary: Create a checkout link
      tags:
        - Checkout links
  /v1/checkout_links/{id}:
    get:
      description: >-
        Poll this to close the loop on a donation. `status` moves `open` → `completed` | `expired` | `cancelled`.
        Outbound webhooks are not delivered yet, so polling is the supported mechanism in v1. `status` is derived from
        the actual payment on every read, not from a stored flag, and `amount_paid` — not `amount` — is what the donor
        actually paid.
      operationId: getCheckoutLink
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutLinkResponseDto'
          description: The checkout link.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Missing or invalid API key.

            Possible `code` values: `unauthorized`.
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The key is valid but lacks the required permission, or the request came from an IP outside the key
            allowlist.


            Possible `code` values: `forbidden`.
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            No checkout link with this id belongs to your account.

            Possible `code` values: `checkout_link_not_found`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      summary: Retrieve a checkout link
      tags:
        - Checkout links
  /v1/checkout_links/{id}/payment_intent:
    post:
      description: |-
        Unauthenticated, for the hosted checkout at `checkout.vaki.co/c/{id}`. Returns everything Stripe.js
        needs to collect a card and confirm.

        **The amount and currency come from the stored link, never from this request.** There is no `amount`
        field in the body, and sending one is a `400` rather than being ignored. A donor can choose *whether*
        to pay; they cannot choose *how much* through this endpoint.

        **Idempotent by construction.** One PaymentIntent per link: call this twice and you get the same
        `payment_intent_id` back while the intent is still unconfirmed, so a double-tap or a page reload
        cannot open two payments against one link. A new intent is only minted if the stored one has become
        unusable (already succeeded, cancelled, or no longer matching the link’s amount).

        `publishable_key` is returned by the server on purpose — which Stripe account serves a link is a
        server-side decision, so a client must not hardcode it.

        Refused with `checkout_link_not_payable` when the link is already `completed`, has `expired`, or was
        `cancelled`; and with `vaki_not_accepting_payments` when the cause itself has stopped collecting.
      operationId: createCheckoutLinkPaymentIntent
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentIntentDto'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntentResponseDto'
          description: The payment is ready to be confirmed by the client.
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The body failed validation — most commonly because it carried a field this endpoint does not accept, such as
            `amount`.


            Possible `code` values: `validation_failed`.
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            No checkout link with this id.

            Possible `code` values: `checkout_link_not_found`.
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The link cannot be paid: it is already completed, expired, or cancelled. The `status_detail` member says
            which.


            Possible `code` values: `checkout_link_not_payable`.
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            The cause is no longer able to receive contributions.

            Possible `code` values: `vaki_not_accepting_payments`.
        '429':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Too many requests from this IP. Honour `Retry-After`.

            Possible `code` values: `rate_limit_exceeded`.
        '503':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Card payments are not configured on this deployment. A problem on our side, not with the request.

            Possible `code` values: `internal_error`.
      summary: Start a card payment for a checkout link
      tags:
        - Checkout links
  /v1/checkout_links/{id}/public:
    get:
      description: |-
        Unauthenticated. Powers the hosted checkout page at `checkout.vaki.co/c/{id}`, which is a static
        site and therefore cannot hold an API key.

        **Why this is safe to be public.** The `chl_…` id is the capability, the same model as a Stripe
        `cs_…` session id. It is minted from `crypto.randomBytes` with **80 bits of randomness** on top of a
        timestamp, so it cannot be guessed or enumerated — do not "simplify" the id generator without
        reading `checkout-link-id.ts` first, because this endpoint is what makes that entropy load-bearing.
        Anyone holding the id is someone the partner sent the link to, and everything returned here is
        already visible to that donor on the checkout page itself.

        **What it deliberately omits.** `callback_url`, `external_reference`, `metadata`, `success_url`,
        `cancel_url`, `fee_percent` and `client_id` are never returned. Those are the partner’s business,
        not the donor’s — `callback_url` names a server-to-server endpoint and `fee_percent` is commercial
        terms. Use the authenticated `GET /v1/checkout_links/{id}` for those.

        **`branding` is partner-supplied, publicly visible content.** It is the one part of this response a
        partner controls: it comes from the `branding` object on their client record, configured once with
        `PUT /v0/clients/{clientId}` rather than sent per link. Values are validated on write (logo must be
        an absolute `https://` URL, accent colour must match `#RRGGBB`, display name is length-capped plain
        text) and sanitized again on read, so a renderer can rely on their shape — but treat them as
        third-party content, not as Vaki-authored copy. The object is omitted entirely when the partner has
        configured no branding; render the Vaki default in that case.

        Cacheable briefly (10s while `open`, 5min once terminal, since terminal states never change).
        Rate-limited per IP; unlike the authenticated routes this one does return `429`.
      operationId: getPublicCheckoutLink
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicCheckoutLinkResponseDto'
          description: The public view of the checkout link.
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            No checkout link with this id.

            Possible `code` values: `checkout_link_not_found`.
        '429':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Too many requests from this IP. Honour `Retry-After`.

            Possible `code` values: `rate_limit_exceeded`.
      summary: Retrieve the public view of a checkout link
      tags:
        - Checkout links
  /v1/vakis:
    post:
      description: >-
        Creates a cause in `draft`. The cause is private and collects nothing until its owner publishes it from the Vaki
        dashboard.


        `owner` is the **real creator** — the responsible person at the beneficiary organisation, never your own account
        and never Vaki. If they already have a Vaki account it is used. If they do not, and your key carries
        `creators:provision`, one is created for them with no password; they are emailed immediately and claim it with a
        login code. Vaki is added as a co-owner of every cause created this way.


        Because an account can be created on your word, the acceptance you collected is required and recorded:
        `terms_accepted_at`, `terms_version` and `terms_accepted_ip`.
      operationId: createVaki
      parameters:
        - description: >-
            Send a unique value to make this POST safely retryable. The first response for a given key is replayed for
            24 hours.
          in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateVakiDto'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VakiResource'
          description: The vaki was created.
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            The request body failed validation. The `errors` member lists the offending fields.

            Possible `code` values: `validation_failed`.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Missing or invalid API key.

            Possible `code` values: `unauthorized`.
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The key lacks `vakis:write`, came from an IP outside its allowlist, or asked for a `fee_percent` other than
            the standard API rate without carrying `vakis:set_fee`.


            Possible `code` values: `forbidden`, `fee_percent_not_permitted`.
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            A request with this `Idempotency-Key` is still being processed. Retry in a few seconds.

            Possible `code` values: `idempotency_key_in_progress`.
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The request was well-formed but could not be applied — most commonly `owner.email` has no Vaki account and
            your key cannot provision one, in which case either the owner signs up at vaki.co or your key needs
            `creators:provision`.


            Possible `code` values: `owner_not_found`, `idempotency_key_reused`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      summary: Create a vaki
      tags:
        - Vakis
  /v1/vakis/{key}:
    get:
      description: >-
        Fetches a vaki by its key (the slug in its vaki.co URL). Promoted unchanged from `/v0/vakis/{id}`, but projected
        onto the public `vaki` object: English state names, and `totals` omitted when the creator hides amounts.
      operationId: getVaki
      parameters:
        - in: path
          name: key
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VakiResource'
          description: The vaki.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            Missing or invalid API key.

            Possible `code` values: `unauthorized`.
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: >-
            The key is valid but lacks the required permission, or the request came from an IP outside the key
            allowlist.


            Possible `code` values: `forbidden`.
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
          description: |-
            No vaki exists with this key.

            Possible `code` values: `vaki_not_found`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      summary: Retrieve a vaki
      tags:
        - Vakis
servers:
  - description: Production
    url: https://api.vaki.co
  - description: Sandbox
    url: https://public-api-staging.vaki.co
tags: []
