openapi: 3.1.0
info:
  title: Gate API
  version: '2026-05-25'
  summary: Hosted crypto on-ramp, off-ramp, and swap API for the 0Gate widget.
  description: >
    The Gate API lets partners create server-bound gate sessions,

    embed the widget in their site, and receive signed webhook

    notifications when sessions settle.


    Three audiences, three credentials:


    | Audience | Credential | Use |

    |---|---|---|

    | Partner server | `sk_test_*` / `sk_live_*` | Create sessions, retrieve, cancel. NEVER in browser. |

    | Partner web page | `pk_test_*` / `pk_live_*` | Bootstrap the iframe via `POST /v1/embed/bootstrap`. Browser-safe.
    |

    | Iframe runtime | embed JWT (from bootstrap) | Sent as `X-Embed-Token` on subsequent calls. Short-lived (1h). |


    All money-moving endpoints accept an `Idempotency-Key` header. Send a

    UUID; repeats with the same key collapse to the original response.


    Errors follow a single shape — see [`Error`](#components/schemas/Error).


    Webhook events are signed HMAC-SHA256, format Stripe-compatible. See

    [Webhooks](#tag/Webhooks) for the verification snippet.
  contact:
    name: Gate API support
    url: https://0bit.io/support
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://gate-api.0bit.app/v1
    description: Production
  - url: https://gate-api-sandbox.0bit.app/v1
    description: Sandbox (test_ keys)
tags:
  - name: Sessions
    description: >
      Hosted session creation. Partner server creates a session with `sk_*` and binds the amount, currency, and flow
      constraints. Browser uses the returned

      `client_secret` (with `pk_*`) to embed the widget with the amount

      locked. User cannot tamper.
  - name: Capabilities
    description: |
      Discovery API — countries, currencies, assets, payment/payout methods.
      Capability catalog for server-side integration (`sk_*` only).
  - name: Quotes
    description: |
      Indicative quote previews and account-gated locked quote operations.
  - name: Rails
    description: >
      Account-gated server-to-server rail operations for approved partners. These routes use signed locked quotes,
      idempotent pay-in/pay-out creation, KYC-approved users, and per-user volume controls.
  - name: Transactions
    description: |
      Partner-scoped transaction lookup and list for reconciliation (`sk_*`).
  - name: Embed
    description: |
      Iframe bootstrap + token refresh. Called from the widget on first
      load to redeem `pk_*` (+ optional `clientSecret`) for a short-lived
      JWT that stamps partner / session context on subsequent calls, and
      to rotate that JWT mid-session via `POST /v1/embed/refresh`.
      Embed responses carry a per-partner `frame-ancestors` CSP so only approved partner origins may frame the widget.
  - name: Webhooks
    description: |
      Server events POSTed to `partner.webhook_url` with HMAC-SHA256
      signature. See the [verification snippet](#section/Verifying-webhook-signatures).
  - name: Branding
    description: |
      Partner self-serve co-branding (widget theming). Read and update your
      own iframe logo / colors / brand name with `sk_*` — the same tokens the
      embed bootstrap surfaces to the widget. Set them here, they show up in
      the hosted iframe.
  - name: Customers
    description: |
      Partner-scoped customer (identity) records — your own reference for an
      end user plus optional contact fields (`sk_*`). NOTE: a customer is a
      CRM-style record, NOT the KYC-verified 0Bit user identity; `kyc_status`
      is informational and not partner-settable.
security: []
components:
  securitySchemes:
    PublishableKey:
      type: http
      scheme: bearer
      bearerFormat: pk_<test|live>_<32-char>
      description: |
        Browser-safe publishable key. Used for `POST /v1/embed/bootstrap`.
        Identifies the partner; CANNOT make money-moving calls.
    SecretKey:
      type: http
      scheme: bearer
      bearerFormat: sk_<test|live>_<32-char>
      description: |
        Server-only secret key. Used for `POST /v1/gate_sessions`, `GET`,
        and `cancel`. NEVER include in browser code or URLs.
    EmbedToken:
      type: apiKey
      in: header
      name: X-Embed-Token
      description: |
        Short-lived JWT (default TTL 1h) returned by
        `POST /v1/embed/bootstrap`. Sent by the iframe on every
        subsequent backend call to stamp partner + session context.
        Rotate it without re-bootstrapping while the bound session stays open.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        UUID. Required on `POST /v1/gate_sessions`. Subsequent calls with the
        same key + body collapse to the original response.
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: Validation error (missing field, malformed value, wrong format).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing / malformed credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: |
        Credential rejected (revoked key, origin not in allowed_domains,
        mode mismatch, session belongs to a different partner).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource does not exist OR caller's credential does not scope to it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: |
        State-transition conflict (cancelling an already-cancelled
        session, revoking an already-revoked key).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limit exceeded for this route / credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      description: |
        Unified cross-product error envelope — identical shape
        across Gate / Base / Pools / Link so partners integrate error
        handling once. `request_id` is also returned as the `X-Request-Id`
        response header. Validation errors are joined into a single `message`
        string. 5xx responses are scrubbed to `Internal server error`.
      required:
        - type
        - code
        - message
        - request_id
        - doc_url
        - statusCode
      properties:
        type:
          type: string
          enum:
            - invalid_request
            - not_found
            - conflict
            - unauthorized
            - forbidden
            - rate_limited
            - server_error
          description: Coarse error category for branching in handlers.
          example: forbidden
        code:
          type: string
          description: Stable machine code for branching (falls back to `type` when none).
          example: forbidden
        message:
          type: string
          description: Human-readable error description (validation errors joined with `; `).
          example: Origin "https://unapproved.example" is not approved for this partner
        request_id:
          type: string
          description: Per-request UUID, also returned as the X-Request-Id response header.
        doc_url:
          type:
            - string
            - 'null'
          description: Link to the docs for this error (null until partner docs URLs are final).
        statusCode:
          type: integer
          description: HTTP status code (mirrors response status).
          example: 403
    Eligibility:
      type: object
      required:
        - object
        - country_code
        - eligible
        - supported
        - geo_source
      properties:
        object:
          type: string
          enum:
            - eligibility
          description: Discriminator for client-side type narrowing.
        country_code:
          type:
            - string
            - 'null'
          description: |
            Resolved ISO 3166-1 alpha-2 region. `null` when neither an
            explicit `country_code` nor a usable `cf-ipcountry` header was
            available (paired with `unavailable_reason: country_unresolved`).
          example: GB
        eligible:
          type: boolean
          description: |
            True only when the region is supported AND every REQUESTED
            refinement (`currency`, `asset`) passed. The single deciding
            failure is in `unavailable_reason`.
          example: true
        supported:
          type: boolean
          description: Whether the resolved region is an on/off-ramp corridor.
          example: true
        default_currency:
          type:
            - string
            - 'null'
          description: The region's default fiat (ISO 4217), or `null`.
          example: GBP
        currency_supported:
          type:
            - boolean
            - 'null'
          description: |
            Whether the requested `currency` is offered in this region.
            `null` when no `currency` was supplied.
          example: null
        asset_supported:
          type:
            - boolean
            - 'null'
          description: |
            Whether the requested `asset` exists and is active.
            `null` when no `asset` was supplied.
          example: null
        geo_source:
          type: string
          enum:
            - explicit
            - ip
            - none
          description: |
            How `country_code` was resolved — `explicit` (query param),
            `ip` (Cloudflare `cf-ipcountry` header), or `none` (unresolved).
          example: explicit
        unavailable_reason:
          type:
            - string
            - 'null'
          enum:
            - country_unresolved
            - country_not_supported
            - currency_not_supported
            - asset_not_supported
            - null
          description: |
            Machine reason when `eligible` is false; `null` when eligible.
          example: null
    BrandingUpdate:
      type: object
      description: |
        Co-branding tokens for the hosted iframe. All optional — a PATCH
        merges the supplied tokens onto the current branding. Additional
        properties are rejected (`400`).
      additionalProperties: false
      properties:
        logo_url:
          type: string
          format: uri
          maxLength: 2048
          description: Partner logo — absolute `https` URL.
          example: https://cdn.acme.example/logo.svg
        primary_color:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
          description: Primary brand color (hex `#RRGGBB`).
          example: '#F97316'
        secondary_color:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
          example: '#111827'
        accent_color:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
          example: '#22C55E'
        brand_name:
          type: string
          maxLength: 64
          description: Display name shown alongside the logo.
          example: Acme
    Branding:
      type: object
      required:
        - object
        - logo_url
        - primary_color
        - secondary_color
        - accent_color
        - brand_name
      description: |
        Resolved co-branding. Each token is `null` when unset — all-null is
        the default Gate branding.
      properties:
        object:
          type: string
          enum:
            - branding
          description: Discriminator for client-side type narrowing.
        logo_url:
          type:
            - string
            - 'null'
          example: https://cdn.acme.example/logo.svg
        primary_color:
          type:
            - string
            - 'null'
          example: '#F97316'
        secondary_color:
          type:
            - string
            - 'null'
          example: null
        accent_color:
          type:
            - string
            - 'null'
          example: null
        brand_name:
          type:
            - string
            - 'null'
          example: Acme
    Customer:
      type: object
      required:
        - id
        - object
        - partner_id
        - mode
        - livemode
        - email
        - kyc_status
        - created_at
      description: |
        Partner-scoped customer (identity) record. A CRM-style reference for an
        end user — NOT the KYC-verified 0Bit user identity. `kyc_status` is
        informational (always `unverified` at launch) and not partner-settable.
      properties:
        id:
          type: string
          example: 67a1f3b9e4b0c10001234567
        object:
          type: string
          enum:
            - customer
        partner_id:
          type: string
        mode:
          type: string
          enum:
            - test
            - live
        livemode:
          type: boolean
        external_id:
          type:
            - string
            - 'null'
          description: Your own reference for this user. Unique per partner when set.
          example: user_42
        email:
          type: string
          example: jordan@example.com
        phone:
          type:
            - string
            - 'null'
          example: '+447700900000'
        first_name:
          type:
            - string
            - 'null'
        last_name:
          type:
            - string
            - 'null'
        country_code:
          type:
            - string
            - 'null'
          example: GB
        kyc_status:
          type: string
          enum:
            - unverified
            - processing
            - verified
            - rejected
            - expired
          description: Informational; not partner-settable. `unverified` at launch.
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    CustomerList:
      type: object
      required:
        - object
        - data
        - has_more
        - url
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: '#/components/schemas/Customer'
        has_more:
          type: boolean
        url:
          type: string
    CreateCustomerRequest:
      type: object
      required:
        - email
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
          maxLength: 320
        external_id:
          type: string
          maxLength: 256
          description: Your own reference. Unique per partner; duplicate → 409.
        phone:
          type: string
          description: E.164, e.g. +447700900000
        first_name:
          type: string
          maxLength: 128
        last_name:
          type: string
          maxLength: 128
        country_code:
          type: string
          description: ISO 3166-1 alpha-2
        metadata:
          type: object
          additionalProperties: true
    UpdateCustomerRequest:
      type: object
      additionalProperties: false
      description: |
        Partial update — only mutable fields. `email`, `phone`, `external_id`,
        `kyc_status` and `mode` are immutable / server-managed (sending them
        is a 400). A field sent as `null` clears it.
      properties:
        first_name:
          type:
            - string
            - 'null'
          maxLength: 128
        last_name:
          type:
            - string
            - 'null'
          maxLength: 128
        country_code:
          type:
            - string
            - 'null'
          description: ISO 3166-1 alpha-2
        metadata:
          type: object
          additionalProperties: true
    CustomerDeleted:
      type: object
      required:
        - id
        - object
        - deleted
      properties:
        id:
          type: string
        object:
          type: string
          enum:
            - customer
        deleted:
          type: boolean
          enum:
            - true
        deleted_at:
          type:
            - string
            - 'null'
          format: date-time
    WebhookDelivery:
      type: object
      required:
        - object
        - id
        - event_id
        - event_type
        - target_url
        - status
        - attempts
      description: |
        One outbound webhook delivery attempt record. `payload_raw` is never
        included — the delivery log is about health, not payload inspection.
      properties:
        object:
          type: string
          enum:
            - webhook_delivery
        id:
          type: string
          example: 67a1f3b9e4b0c10001234567
        event_id:
          type: string
          description: Stable event id (also sent as the `X-0bit-Event-Id` header).
          example: b3c1...
        event_type:
          type: string
          example: gate_session.completed
        target_url:
          type: string
          example: https://partner.example/webhooks/0bit
        status:
          type: string
          enum:
            - pending
            - in_flight
            - succeeded
            - dead_lettered
        attempts:
          type: integer
          example: 1
        last_response_status:
          type:
            - integer
            - 'null'
          example: 500
        last_error:
          type:
            - string
            - 'null'
          example: 'HTTP 500: (empty body)'
        next_attempt_at:
          type:
            - string
            - 'null'
          format: date-time
        delivered_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
    GateSession:
      type: object
      required:
        - id
        - object
        - partner_id
        - mode
        - amount
        - currency
        - return_url
        - status
        - expires_at
      properties:
        id:
          type: string
          description: Server-assigned id. Mongo ObjectId hex string.
          example: 67a1f3b9e4b0c10001234567
        object:
          type: string
          enum:
            - gate_session
          description: Discriminator for client-side type narrowing.
        partner_id:
          type: string
          description: Partner that owns this session.
        mode:
          type: string
          enum:
            - test
            - live
          description: Inherited from the `sk_*` that created the session.
        flow:
          type:
            - string
            - 'null'
          enum:
            - on_ramp
            - off_ramp
            - swap
            - null
          description: |
            Flow lock. When set, the iframe forces
            the user into the chosen flow and hides the buy/sell/swap tab
            strip. `null` (or omitted) = legacy "open" widget where the
            user picks.

            Use a flow lock when you've already collected the user's
            intent on your own surface (e.g. a "Buy crypto" button) and
            want the widget to pick up at the right step.
          example: on_ramp
        wallet_address:
          type:
            - string
            - 'null'
          description: |
            Optional partner-supplied value. Pre-filled destination wallet.
            When set, the iframe lands directly in the wallet step with
            this value (or skips wallet selection for flows where the
            wallet is the only input). Format is checked against the
            resolved network at flow time, not session-create.
          example: 0x742d35Cc6634C0532925a3b8D4...
        user_reference:
          type:
            - string
            - 'null'
          maxLength: 128
          description: |
            Optional partner-supplied value. Opaque partner-side
            identifier — your user id, order id, CRM record id, etc.
            Echoed in webhook payloads so you can correlate session
            events back to your user.
          example: order_abc123
        kyc_pre_verified:
          type: boolean
          default: false
          description: >
            Optional account-gated value. `true` when the partner submitted

            a `kyc_package` on this session AND was authorised

            (the account is approved for trusted verification handoff). Forwarded to the widget so it can follow the
            approved verification path. The raw `kyc_package` body is NEVER echoed in any

            API response (PII).
        amount:
          type: string
          description: |
            Decimal string. Server-bound at create time — buy/sell calls
            against this session must match exactly or are rejected.
          example: '100.00'
        currency:
          type: string
          minLength: 3
          maxLength: 3
          description: ISO 4217 three-letter code.
          example: EUR
        target_token:
          type:
            - string
            - 'null'
          description: |
            Optional partner constraint. If set, user can only buy/swap
            to this token (e.g. `USDC`).
          example: USDC
        target_network:
          type:
            - string
            - 'null'
          description: |
            Optional partner constraint. If set, user can only operate
            on this network (e.g. `ETHEREUM`, `POLYGON`).
          example: ETHEREUM
        return_url:
          type: string
          format: uri
          description: |
            Where the iframe sends the user after a successful flow.
            Must be HTTPS (loopback allowed in dev) and the URL's
            origin must be in `partner.allowed_domains`.
          example: https://partner.example/checkout/done
        cancel_url:
          type:
            - string
            - 'null'
          format: uri
        status:
          type: string
          enum:
            - open
            - completed
            - expired
            - cancelled
          description: |
            Lifecycle state.

            - `open` — accepting actions
            - `completed` — at least one intent succeeded (terminal)
            - `expired` — past `expires_at` without completing (terminal)
            - `cancelled` — partner cancelled (terminal)
        expires_at:
          type: string
          format: date-time
          description: |
            ISO-8601. Default 24h after creation. Past this point, the
            session auto-transitions to `expired` on the next read.
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        metadata:
          type: object
          additionalProperties: true
          description: |
            Opaque per-session notes. Partner-controlled, returned
            verbatim. Not used for business logic.
    GateSessionWithClientSecret:
      description: |
        Session shape returned from `POST /v1/gate_sessions`. Identical to
        `GateSession` plus the `client_secret`, which is shown to the
        partner ONCE on create. Subsequent reads omit it — the partner
        must store it themselves or rotate the session.
      allOf:
        - $ref: '#/components/schemas/GateSession'
        - type: object
          required:
            - client_secret
          properties:
            client_secret:
              type: string
              description: |
                Session-scoped secret format: `gsec_<sessionId>_<32-char>`. Sent
                to the browser, which presents it to
                `POST /v1/embed/bootstrap` to bind the iframe to this
                session.
              example: gsec_67a1f3b9e4b0c10001234567_AbCdEfGhIjKlMnOpQrStUvWxYz012345
    EmbedBootstrapRequest:
      type: object
      properties:
        clientSecret:
          type: string
          description: |
            From `POST /v1/gate_sessions`. When present, the returned embed
            JWT is session-bound; subsequent buy/sell calls enforce
            the session's amount/currency. Optional — partners not
            using sessions yet can bootstrap without it.
        requested_scopes:
          type: array
          description: |
            Forward-looking. Empty / absent today; reserved for
            per-route permissions in a future version.
          items:
            type: string
    EmbedBootstrapResponse:
      type: object
      required:
        - embed_token
        - expires_at
        - partner_id
        - mode
      properties:
        embed_token:
          type: string
          description: |
            Short-lived JWT (HS256, signed by 0Bit,
            default TTL 1h). Send as `X-Embed-Token` header on every
            subsequent backend request from the iframe.
        expires_at:
          type: string
          format: date-time
        partner_id:
          type: string
        mode:
          type: string
          enum:
            - test
            - live
        session_id:
          type:
            - string
            - 'null'
          description: Set when bootstrap was session-bound (clientSecret provided).
        amount:
          type:
            - string
            - 'null'
          description: Echoed from session when session-bound. Lets the iframe render the locked value.
        currency:
          type:
            - string
            - 'null'
        target_token:
          type:
            - string
            - 'null'
        target_network:
          type:
            - string
            - 'null'
        return_url:
          type:
            - string
            - 'null'
        flow:
          type:
            - string
            - 'null'
          enum:
            - on_ramp
            - off_ramp
            - swap
            - null
          description: |
            Kit-block flow lock from the session. Iframe forwards this
            to lock the tab strip.
        kyc_pre_verified:
          type: boolean
          default: false
          description: >
            `true` when the session was created with an approved partner verification handoff. Forwarded to the widget
            so it can follow the approved verification path.
        wallet_address:
          type:
            - string
            - 'null'
          description: Pre-filled destination wallet.
        user_reference:
          type:
            - string
            - 'null'
          description: Opaque partner-side identifier.
    GateSessionEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: |
        Base envelope for `gate_session.*` events. The `data` field is a
        [`GateSession`](#/components/schemas/GateSession) (or a redacted
        `KycPackageAcceptance` for `gate_session.kyc_package_accepted`).
      properties:
        id:
          type: string
          format: uuid
          description: |
            Unique per delivery. Use as your dedupe key on the partner
            side — webhook retries reuse the same `id`.
        type:
          type: string
          enum:
            - gate_session.created
            - gate_session.processing
            - gate_session.completed
            - gate_session.failed
            - gate_session.cancelled
            - gate_session.expired
            - gate_session.kyc_package_accepted
          example: gate_session.created
        created_at:
          type: integer
          description: Unix epoch seconds when the event was generated.
        data:
          oneOf:
            - $ref: '#/components/schemas/GateSession'
            - $ref: '#/components/schemas/KycPackageAcceptance'
          description: |
            For `gate_session.{created,cancelled,expired}` the payload is
            a full `GateSession`. For `gate_session.kyc_package_accepted`
            the payload is a redacted `KycPackageAcceptance` (raw KYC
            body NEVER sent in webhooks — partner has their own copy).
    KycPackageAcceptance:
      type: object
      required:
        - session_id
        - partner_id
        - provider
        - accepted_at
      description: >
        Verification handoff audit envelope. Delivered once per session when an approved partner submits verification
        context. Deliberately minimal: provides correlation and timestamp without re-sending PII.
      properties:
        session_id:
          type: string
        partner_id:
          type: string
        mode:
          type: string
          enum:
            - test
            - live
        provider:
          type:
            - string
            - 'null'
          description: Partner-supplied verification handoff label. Null if unspecified.
          example: verification_handoff
        accepted_at:
          type: string
          format: date-time
        user_reference:
          type:
            - string
            - 'null'
          description: Echoed from the session for partner-side correlation.
    GateSessionCompletedEvent:
      description: |
        Fired when a buy/sell linked to the session succeeds. Adds
        `tx_refid` to the standard session envelope so partners can
        join back to their order record via the underlying transaction.
      allOf:
        - $ref: '#/components/schemas/GateSessionEvent'
        - type: object
          properties:
            type:
              type: string
              enum:
                - gate_session.completed
            data:
              allOf:
                - $ref: '#/components/schemas/GateSession'
                - type: object
                  required:
                    - tx_refid
                  properties:
                    tx_refid:
                      type: string
                      description: |
                        Refid of the underlying user_crypto_transaction.
                        Use to deep-link to the receipt or join to your
                        order record.
                    transaction:
                      oneOf:
                        - $ref: '#/components/schemas/WebhookTransaction'
                        - type: 'null'
                      description: |
                        The underlying transaction (J.2.8) — amounts, payment
                        method and partner-visible payment reference. PII-excluded. Null if unavailable.
    WebhookTransaction:
      type: object
      description: PII-filtered transaction object embedded in session webhooks (J.2.8).
      required:
        - object
        - refid
        - action
      properties:
        object:
          type: string
          const: transaction
        refid:
          type: string
        action:
          type: string
          enum:
            - BUY
            - SELL
        status:
          type:
            - string
            - 'null'
        token:
          type:
            - string
            - 'null'
        network:
          type:
            - string
            - 'null'
        currency:
          type:
            - string
            - 'null'
        payment_method:
          type:
            - string
            - 'null'
        fiat_amount:
          type:
            - string
            - 'null'
        crypto_amount:
          type:
            - string
            - 'null'
        total_paid_or_received:
          type:
            - string
            - 'null'
        payment_provider_id:
          type:
            - string
            - 'null'
        created_at:
          type:
            - string
            - 'null'
          format: date-time
    GateSessionProcessingEvent:
      description: |
        Fired when a buy/sell linked to the session starts (J.2.2) — carries
        the tx-join key + the (PII-filtered) transaction object.
      allOf:
        - $ref: '#/components/schemas/GateSessionEvent'
        - type: object
          properties:
            type:
              type: string
              enum:
                - gate_session.processing
            data:
              allOf:
                - $ref: '#/components/schemas/GateSession'
                - type: object
                  required:
                    - tx_refid
                  properties:
                    tx_refid:
                      type: string
                    transaction:
                      oneOf:
                        - $ref: '#/components/schemas/WebhookTransaction'
                        - type: 'null'
    GateSessionFailedEvent:
      description: |
        Fired when a buy/sell linked to the session fails (J.2.2). The
        session stays `open` — the user can retry inside the iframe.
      allOf:
        - $ref: '#/components/schemas/GateSessionEvent'
        - type: object
          properties:
            type:
              type: string
              enum:
                - gate_session.failed
            data:
              allOf:
                - $ref: '#/components/schemas/GateSession'
                - type: object
                  required:
                    - tx_refid
                  properties:
                    tx_refid:
                      type: string
                    failure_code:
                      type: string
                    failure_message:
                      type: string
                    transaction:
                      oneOf:
                        - $ref: '#/components/schemas/WebhookTransaction'
                        - type: 'null'
    KycRequiredEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: Fired when a session is blocked pending KYC verification (J.2.1).
      properties:
        id:
          type: string
        type:
          type: string
          const: kyc.required
        created_at:
          type: integer
          description: Unix seconds.
        data:
          type: object
          required:
            - gate_session_id
          properties:
            gate_session_id:
              type: string
            kyc_status:
              type:
                - string
                - 'null'
            user_reference:
              type:
                - string
                - 'null'
    ListEnvelope:
      type: object
      required:
        - object
        - data
        - has_more
        - url
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items: {}
        has_more:
          type: boolean
        url:
          type: string
    PartnerTransaction:
      type: object
      required:
        - object
        - refid
        - action
        - status
      properties:
        object:
          type: string
          enum:
            - transaction
        refid:
          type: string
        session_id:
          type:
            - string
            - 'null'
        action:
          type: string
          enum:
            - BUY
            - SELL
        status:
          type: string
        token:
          type: string
        network:
          type: string
        currency:
          type: string
        payment_method:
          type: string
        fiat_amount:
          type: string
        token_amount:
          type: string
        total_pay_or_receive:
          type: string
        exchange_rate:
          type:
            - string
            - 'null'
        total_fees:
          type:
            - string
            - 'null'
        created_at:
          type:
            - string
            - 'null'
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
        status_timeline:
          type: array
          items:
            type: object
            properties:
              status:
                type: string
              at:
                type: string
                format: date-time
    QuotePreview:
      type: object
      properties:
        object:
          type: string
          enum:
            - quote_preview_list
        currency:
          type: string
        asset:
          type: string
        side:
          type: string
          enum:
            - on_ramp
            - off_ramp
        amount:
          type: string
        market_rate:
          type: string
        margin_bps:
          type: number
        expires_at:
          type: string
          format: date-time
        quote_preview_id:
          type:
            - string
            - 'null'
          pattern: ^[a-fA-F0-9]{24}$
          description: |
            J.1.9c — opaque id for the persisted preview lock. Pass this
            back on `POST /v1/gate_sessions` to bind the session to the
            previewed amount, currency, asset, and side. `null` when
            preview locks are disabled on the deployment.
        unavailable_reason:
          type:
            - string
            - 'null'
          enum:
            - country_not_supported
          description: >
            J.2.7 — set when the ramp is not offered for the resolved country (from `country_code` or the `cf-ipcountry`
            edge header). When present, `quotes` is an empty array and `quote_preview_id` is null. Null in normal
            responses.
        quotes:
          type: array
          items:
            type: object
    SignedQuote:
      type: object
      required:
        - object
        - id
        - status
        - side
        - currency
        - asset
        - payment_method
        - fiat_amount
        - crypto_amount
        - exchange_rate
        - fees
        - fiat_pay_or_receive
        - usd_amount
        - signature
        - expires_at
      properties:
        object:
          type: string
          const: signed_quote
        id:
          type: string
        status:
          type: string
          enum:
            - active
            - consumed
            - expired
        side:
          type: string
          enum:
            - on_ramp
            - off_ramp
        currency:
          type: string
        asset:
          type: string
        payment_method:
          type: string
        fiat_amount:
          type: string
        crypto_amount:
          type: string
        exchange_rate:
          type: string
        fees:
          type: object
          properties:
            spread:
              type: string
            fixed:
              type: string
            total:
              type: string
        fiat_pay_or_receive:
          type: string
        usd_amount:
          type: string
          description: USD-normalised value; feeds the per-user cumulative-volume cap.
        signature:
          type: string
          description: |
            HMAC-SHA256 over the canonical quote payload, Stripe-format
            `t=<unix_seconds>,v1=<hex>`. Verify with the quote signing secret issued for your account.
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    RailRequest:
      type: object
      required:
        - gate_session_id
        - quote_id
      additionalProperties: false
      description: |
        amount / currency / method are derived authoritatively from the
        locked quote (`quote_id`), NOT accepted here. Any PII field (name,
        PII or payout-instruction field) is rejected with 400.
      properties:
        gate_session_id:
          type: string
          maxLength: 64
        quote_id:
          type: string
          maxLength: 64
        reference:
          type: string
          maxLength: 128
        metadata:
          type: object
          additionalProperties: true
    RailPayIn:
      type: object
      required:
        - object
        - id
        - kind
        - status
        - gate_session_id
        - quote_id
        - method
        - amount
        - currency
        - provider
      properties:
        object:
          type: string
          const: rail_pay_in
        id:
          type: string
        kind:
          type: string
          const: pay_in
        status:
          type: string
          enum:
            - pending
            - processing
            - settled
            - failed
            - cancelled
        gate_session_id:
          type: string
        quote_id:
          type: string
        method:
          type: string
        amount:
          type: string
          description: >-
            The signed, fee-inclusive amount that settles — what the customer pays (pay_in: base + fees) or receives
            (pay_out: base − fees). Equals the quote's `fiat_pay_or_receive`, not the fee-exclusive base (`fiat_amount`,
            still available on the quote).
        currency:
          type: string
        reference:
          type:
            - string
            - 'null'
        provider:
          type: string
        provider_ref:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        account_blocked:
          type: boolean
          description: |
            Present + true when this rail pushed the user's cumulative volume
            past the cap; the account is now blocked for FUTURE rails pending
            manual review.
    RailPayOut:
      type: object
      required:
        - object
        - id
        - kind
        - status
        - gate_session_id
        - quote_id
        - method
        - amount
        - currency
        - provider
      properties:
        object:
          type: string
          const: rail_pay_out
        id:
          type: string
        kind:
          type: string
          const: pay_out
        status:
          type: string
          enum:
            - pending
            - processing
            - settled
            - failed
            - cancelled
        gate_session_id:
          type: string
        quote_id:
          type: string
        method:
          type: string
        amount:
          type: string
          description: >-
            The signed, fee-inclusive amount that settles — what the customer pays (pay_in: base + fees) or receives
            (pay_out: base − fees). Equals the quote's `fiat_pay_or_receive`, not the fee-exclusive base (`fiat_amount`,
            still available on the quote).
        currency:
          type: string
        reference:
          type:
            - string
            - 'null'
        provider:
          type: string
        provider_ref:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        account_blocked:
          type: boolean
          description: |
            Present + true when this rail pushed the user's cumulative volume
            past the cap; the account is now blocked for FUTURE rails pending
            manual review.
    RailEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: Webhook envelope for a rail status transition. `data` is the full rail object (no PII).
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - rail.pay_in.processing
            - rail.pay_in.settled
            - rail.pay_in.failed
            - rail.pay_in.cancelled
            - rail.pay_out.processing
            - rail.pay_out.settled
            - rail.pay_out.failed
            - rail.pay_out.cancelled
        created_at:
          type: integer
          description: Unix seconds.
        data:
          oneOf:
            - $ref: '#/components/schemas/RailPayIn'
            - $ref: '#/components/schemas/RailPayOut'
    QuoteConsumedEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: Webhook envelope fired when a rail redeems a signed quote.
      properties:
        id:
          type: string
        type:
          type: string
          const: quote.consumed
        created_at:
          type: integer
          description: Unix seconds.
        data:
          type: object
          properties:
            quote_id:
              type: string
            rail_id:
              type: string
            kind:
              type: string
              enum:
                - pay_in
                - pay_out
    WebhookTestEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: |
        Webhook envelope for a partner-triggered `POST /v1/webhooks/test`.
        Same signing/headers as a real event; `data.livemode` is always
        `false`. Never emitted by the platform itself.
      properties:
        id:
          type: string
        type:
          type: string
          const: webhook.test
        created_at:
          type: integer
          description: Unix seconds.
        data:
          type: object
          properties:
            livemode:
              type: boolean
              enum:
                - false
            message:
              type: string
    CustomerEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: Webhook envelope for customer.created / customer.updated.
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - customer.created
            - customer.updated
        created_at:
          type: integer
          description: Unix seconds.
        data:
          $ref: '#/components/schemas/Customer'
    CustomerDeletedEvent:
      type: object
      required:
        - id
        - type
        - created_at
        - data
      description: Webhook envelope for customer.deleted (compact payload).
      properties:
        id:
          type: string
        type:
          type: string
          const: customer.deleted
        created_at:
          type: integer
          description: Unix seconds.
        data:
          type: object
          properties:
            object:
              type: string
              enum:
                - customer
            id:
              type: string
            partner_id:
              type: string
            mode:
              type: string
              enum:
                - test
                - live
            external_id:
              type:
                - string
                - 'null'
            email:
              type: string
            deleted_at:
              type:
                - string
                - 'null'
              format: date-time
paths:
  /gate_sessions:
    get:
      operationId: listSessions
      tags:
        - Sessions
      summary: List gate sessions
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl 'https://gate-api.0bit.app/v1/gate_sessions?limit=10' \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const res = await fetch(
              'https://gate-api.0bit.app/v1/gate_sessions?limit=10',
              { headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const { data } = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.get(
                "https://gate-api.0bit.app/v1/gate_sessions",
                headers={"Authorization": "Bearer sk_test_..."},
                params={"limit": 10},
            )
            sessions = res.json()["data"]
      description: |
        Cursor-paginated list of sessions for the authenticated partner.
        Use `starting_after` with the last session `id` from the previous page.
      security:
        - SecretKey: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: starting_after
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum:
              - open
              - completed
              - expired
              - cancelled
      responses:
        '200':
          description: Session list.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/GateSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      operationId: createSession
      tags:
        - Sessions
      summary: Create a gate session
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/gate_sessions \
              -H "Authorization: Bearer sk_test_..." \
              -H "Idempotency-Key: $(uuidgen)" \
              -H "Content-Type: application/json" \
              -d '{
                "amount": "100.00",
                "currency": "EUR",
                "return_url": "https://partner.example/checkout/done"
              }'
        - lang: javascript
          label: Node
          source: |
            import crypto from 'node:crypto';

            const res = await fetch('https://gate-api.0bit.app/v1/gate_sessions', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Idempotency-Key': crypto.randomUUID(),
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                amount: '100.00',
                currency: 'EUR',
                return_url: 'https://partner.example/checkout/done',
              }),
            });
            const session = await res.json();
            // session.client_secret -> hand to the browser to embed the widget
        - lang: python
          label: Python
          source: |
            import uuid, requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/gate_sessions",
                headers={
                    "Authorization": "Bearer sk_test_...",
                    "Idempotency-Key": str(uuid.uuid4()),
                },
                json={
                    "amount": "100.00",
                    "currency": "EUR",
                    "return_url": "https://partner.example/checkout/done",
                },
            )
            session = res.json()
      description: >
        Stripe Checkout Session analog. Binds amount + currency

        server-side; returns a `client_secret` to share with the

        browser.


        **Rate limit**: 10 / minute / key.


        **Idempotency**: send an `Idempotency-Key` UUID. Repeats with

        the same key + body collapse to the original response.


        **Related guides:** [Embed the widget](/gate/guides/embed-the-widget) ·
        [Idempotency](/gate/api-reference/idempotency)
      security:
        - SecretKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - amount
                - currency
                - return_url
              properties:
                amount:
                  type: string
                  pattern: ^[0-9]+(\.[0-9]{1,8})?$
                  description: Decimal string, max 8 fractional digits, > 0.
                  example: '100.00'
                currency:
                  type: string
                  pattern: ^[A-Za-z]{3}$
                  description: ISO 4217 three-letter code.
                  example: EUR
                return_url:
                  type: string
                  format: uri
                  description: Origin must be in `partner.allowed_domains`. HTTPS only (loopback in dev).
                cancel_url:
                  type: string
                  format: uri
                target_token:
                  type: string
                  pattern: ^[A-Za-z0-9]{2,12}$
                target_network:
                  type: string
                  pattern: ^[A-Za-z0-9_-]{2,30}$
                flow:
                  type: string
                  enum:
                    - on_ramp
                    - off_ramp
                    - swap
                  description: |
                    Kit-block flow lock. When set, the iframe forces the
                    user into the chosen flow and hides the tab strip.
                    Omit for the legacy "open" widget where the user
                    picks via tabs.
                  example: on_ramp
                wallet_address:
                  type: string
                  maxLength: 128
                  description: |
                    Optional account-gated value. Pre-filled destination
                    wallet. Validated against the resolved network at
                    flow time. Accepted from any partner.
                user_reference:
                  type: string
                  maxLength: 128
                  description: |
                    Optional account-gated value. Opaque partner-side
                    user/order id. Echoed in webhook payloads for
                    correlation. Accepted from any partner.
                kyc_package:
                  type: object
                  additionalProperties: true
                  description: |
                    Optional account-gated value. Partner-supplied
                    pre-verified KYC. CONTRACT-GATED — only accepted
                    when the account is approved for trusted verification handoff. Returns 403
                    with `code: kyc_package_not_trusted` otherwise.

                    Shape is opaque to the API. Required: `provider`
                    (string) for audit. Other fields per your contract.

                    On acceptance, the session is created with
                    `kyc_pre_verified: true` AND a redacted
                    `gate_session.kyc_package_accepted` webhook is
                    delivered. Raw payload is NEVER echoed in any
                    API response.
                metadata:
                  type: object
                  additionalProperties: true
                quote_preview_id:
                  type: string
                  pattern: ^[a-fA-F0-9]{24}$
                  description: |
                    J.1.9c — optional id from `POST /v1/quotes/preview`.
                    When supplied, amount/currency/asset/side must match
                    the locked preview or the request is rejected.
      responses:
        '200':
          description: Session created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GateSessionWithClientSecret'
              examples:
                created:
                  summary: New open session
                  value:
                    id: 67a1f3b9e4b0c10001234567
                    object: gate_session
                    partner_id: 507f1f77bcf86cd799439011
                    mode: test
                    amount: '100.00'
                    currency: EUR
                    return_url: https://partner.example/checkout/done
                    cancel_url: null
                    status: open
                    expires_at: '2026-05-27T12:00:00.000Z'
                    flow: on_ramp
                    wallet_address: null
                    user_reference: order_abc123
                    kyc_pre_verified: false
                    metadata: {}
                    client_secret: gsec_67a1f3b9e4b0c10001234567_AbCdEfGhIjKlMnOpQrStUvWxYz012345
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /gate_sessions/{id}:
    get:
      operationId: retrieveSession
      tags:
        - Sessions
      summary: Retrieve a session
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567 \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const res = await fetch(
              'https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567',
              { headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const session = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.get(
                "https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567",
                headers={"Authorization": "Bearer sk_test_..."},
            )
            session = res.json()
      description: |
        Returns the session shape WITHOUT `client_secret` (raw secret
        is shown only once on creation).
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GateSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /gate_sessions/{id}/cancel:
    post:
      operationId: cancelSession
      tags:
        - Sessions
      summary: Cancel a session
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST \
              https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567/cancel \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const res = await fetch(
              'https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567/cancel',
              { method: 'POST', headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const session = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567/cancel",
                headers={"Authorization": "Bearer sk_test_..."},
            )
            session = res.json()
      description: |
        Marks an `open` session as `cancelled`. Conflict if the
        session is already in a terminal state.
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Session, now `cancelled`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GateSession'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /capabilities/countries:
    get:
      operationId: listCountries
      tags:
        - Capabilities
      summary: Supported countries
      security:
        - SecretKey: []
      parameters:
        - name: country_code
          in: query
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - name: supported
          in: query
          schema:
            type: string
            enum:
              - 'true'
      responses:
        '200':
          description: Country list.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /capabilities/currencies:
    get:
      operationId: listCurrencies
      tags:
        - Capabilities
      summary: Supported fiat currencies
      security:
        - SecretKey: []
      parameters:
        - name: country_code
          in: query
          schema:
            type: string
            minLength: 2
            maxLength: 2
      responses:
        '200':
          description: Currency list.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /capabilities/assets:
    get:
      operationId: listAssets
      tags:
        - Capabilities
      summary: Supported crypto assets
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl 'https://gate-api.0bit.app/v1/capabilities/assets?side=on_ramp&country_code=GB' \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const params = new URLSearchParams({ side: 'on_ramp', country_code: 'GB' });
            const res = await fetch(
              `https://gate-api.0bit.app/v1/capabilities/assets?${params}`,
              { headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const assets = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.get(
                "https://gate-api.0bit.app/v1/capabilities/assets",
                headers={"Authorization": "Bearer sk_test_..."},
                params={"side": "on_ramp", "country_code": "GB"},
            )
            assets = res.json()
      security:
        - SecretKey: []
      parameters:
        - name: side
          in: query
          schema:
            type: string
            enum:
              - on_ramp
              - off_ramp
        - name: country_code
          in: query
          schema:
            type: string
        - name: currency
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Asset list.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /capabilities/assets/{symbol}:
    get:
      operationId: getAsset
      tags:
        - Capabilities
      summary: Retrieve one asset
      security:
        - SecretKey: []
      parameters:
        - name: symbol
          in: path
          required: true
          schema:
            type: string
        - name: side
          in: query
          schema:
            type: string
            enum:
              - on_ramp
              - off_ramp
      responses:
        '200':
          description: Asset detail.
        '404':
          $ref: '#/components/responses/NotFound'
  /capabilities/payment-methods:
    get:
      operationId: listPaymentMethods
      tags:
        - Capabilities
      summary: Buy-side payment methods
      security:
        - SecretKey: []
      parameters:
        - name: currency
          in: query
          schema:
            type: string
        - name: country_code
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Payment method list.
  /capabilities/payout-methods:
    get:
      operationId: listPayoutMethods
      tags:
        - Capabilities
      summary: Sell-side payout methods
      security:
        - SecretKey: []
      parameters:
        - name: currency
          in: query
          schema:
            type: string
        - name: country_code
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Payout method list.
  /capabilities/eligibility:
    get:
      operationId: checkEligibility
      tags:
        - Capabilities
      summary: Region eligibility probe
      description: |
        Answers whether a user in this region can use
        the ramp (optionally for a given currency/asset)?" without creating a
        session or locking a quote. Read-only — never a money path.

        Region is resolved from an explicit `country_code` (wins) or the
        Cloudflare `cf-ipcountry` edge header; `geo_source` reports which.
        An unsupported or unresolved region returns `eligible: false` with a
        machine `unavailable_reason` rather than an error, so partners can
        branch client-side.
      security:
        - SecretKey: []
      parameters:
        - name: country_code
          in: query
          description: ISO 3166-1 alpha-2. Overrides the IP-derived country.
          schema:
            type: string
        - name: currency
          in: query
          description: Optional fiat (ISO 4217) to additionally check support for.
          schema:
            type: string
        - name: asset
          in: query
          description: Optional crypto asset symbol to additionally check support for.
          schema:
            type: string
      responses:
        '200':
          description: Eligibility decision (never errors on unsupported region).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Eligibility'
  /quotes/preview:
    post:
      operationId: previewQuotes
      tags:
        - Quotes
      summary: Preview quotes for all payment methods
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/quotes/preview \
              -H "Authorization: Bearer sk_test_..." \
              -H "Content-Type: application/json" \
              -d '{
                "currency": "EUR",
                "asset": "USDC",
                "amount": "100.00",
                "side": "on_ramp"
              }'
        - lang: javascript
          label: Node
          source: |
            const res = await fetch('https://gate-api.0bit.app/v1/quotes/preview', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                currency: 'EUR',
                asset: 'USDC',
                amount: '100.00',
                side: 'on_ramp',
              }),
            });
            const preview = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/quotes/preview",
                headers={"Authorization": "Bearer sk_test_..."},
                json={
                    "currency": "EUR",
                    "asset": "USDC",
                    "amount": "100.00",
                    "side": "on_ramp",
                },
            )
            preview = res.json()
      description: |
        Ramp `quote/all` analog. Returns indicative quotes for every
        applicable payment/payout method in one call. Not locked —
        see account-gated locked quotes.

        When quote-preview locks are enabled, the response includes
        `quote_preview_id`. Pass it to `POST /v1/gate_sessions` so
        mismatched amount/currency/asset/side are rejected (J.1.9c).
      security:
        - SecretKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - currency
                - asset
                - amount
                - side
              properties:
                currency:
                  type: string
                  pattern: ^[A-Za-z]{3}$
                asset:
                  type: string
                amount:
                  type: string
                  pattern: ^[0-9]+(\\.[0-9]{1,8})?$
                side:
                  type: string
                  enum:
                    - on_ramp
                    - off_ramp
                country_code:
                  type: string
                  pattern: ^[A-Z]{2}$
      responses:
        '200':
          description: Quote preview list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuotePreview'
        '400':
          $ref: '#/components/responses/BadRequest'
  /transactions:
    get:
      operationId: listTransactions
      tags:
        - Transactions
      summary: List partner transactions
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl 'https://gate-api.0bit.app/v1/transactions?limit=10&status=completed' \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const params = new URLSearchParams({ limit: '10', status: 'completed' });
            const res = await fetch(
              `https://gate-api.0bit.app/v1/transactions?${params}`,
              { headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const { data } = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.get(
                "https://gate-api.0bit.app/v1/transactions",
                headers={"Authorization": "Bearer sk_test_..."},
                params={"limit": 10, "status": "completed"},
            )
            transactions = res.json()["data"]
      security:
        - SecretKey: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: starting_after
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
        - name: session_id
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Transaction list.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/PartnerTransaction'
  /transactions/{refid}:
    get:
      operationId: retrieveTransaction
      tags:
        - Transactions
      summary: Retrieve a transaction by refid
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://gate-api.0bit.app/v1/transactions/0g_txn_abc123 \
              -H "Authorization: Bearer sk_test_..."
        - lang: javascript
          label: Node
          source: |
            const res = await fetch(
              'https://gate-api.0bit.app/v1/transactions/0g_txn_abc123',
              { headers: { Authorization: 'Bearer sk_test_...' } },
            );
            const transaction = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.get(
                "https://gate-api.0bit.app/v1/transactions/0g_txn_abc123",
                headers={"Authorization": "Bearer sk_test_..."},
            )
            transaction = res.json()
      security:
        - SecretKey: []
      parameters:
        - name: refid
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Transaction.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerTransaction'
        '404':
          $ref: '#/components/responses/NotFound'
  /embed/bootstrap:
    post:
      operationId: createEmbedToken
      tags:
        - Embed
      summary: Bootstrap an embed token
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/embed/bootstrap \
              -H "Authorization: Bearer pk_test_..." \
              -H "Content-Type: application/json" \
              -d '{"clientSecret":"gsec_67a1f3b9e4b0c10001234567_AbCdEf..."}'
        - lang: javascript
          label: Browser JS
          source: |
            // Runs in the browser with the publishable key (pk_*).
            const res = await fetch('https://gate-api.0bit.app/v1/embed/bootstrap', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer pk_test_...',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({ clientSecret: session.client_secret }),
            });
            const { embed_token } = await res.json();
            // Pass embed_token to the iframe as the X-Embed-Token header.
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/embed/bootstrap",
                headers={"Authorization": "Bearer pk_test_..."},
                json={"clientSecret": "gsec_67a1f3b9e4b0c10001234567_AbCdEf..."},
            )
            embed_token = res.json()["embed_token"]
      description: |
        Called by the iframe (or `widget.js` SDK) on first load.
        Exchanges `pk_*` (+ optional `clientSecret`) for a short-lived
        embed JWT used on subsequent iframe calls.

        Origin (or Referer fallback) must be in
        `partner.allowed_domains` — exact origin match, no wildcards.

        **Rate limit**: 30 / minute / IP.
      security:
        - PublishableKey: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbedBootstrapRequest'
      responses:
        '200':
          description: Embed token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmbedBootstrapResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            Origin not in allowed_domains; invalid / expired clientSecret;
            session belongs to a different partner or mode.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /branding:
    get:
      operationId: getBranding
      tags:
        - Branding
      summary: Get your co-branding
      description: |
        Returns the partner's current iframe co-branding tokens. All `null`
        means the default Gate branding is in effect. These are the same
        tokens surfaced to the hosted widget via the embed bootstrap.
      security:
        - SecretKey: []
      responses:
        '200':
          description: Current branding tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branding'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      operationId: updateBranding
      tags:
        - Branding
      summary: Update your co-branding
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X PATCH https://gate-api.0bit.app/v1/branding \
              -H "Authorization: Bearer sk_test_..." \
              -H "Content-Type: application/json" \
              -d '{"primary_color":"#F97316","brand_name":"Acme"}'
        - lang: javascript
          label: Node
          source: |
            const res = await fetch('https://gate-api.0bit.app/v1/branding', {
              method: 'PATCH',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({ primary_color: '#F97316', brand_name: 'Acme' }),
            });
            const branding = await res.json();
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.patch(
                "https://gate-api.0bit.app/v1/branding",
                headers={"Authorization": "Bearer sk_test_..."},
                json={"primary_color": "#F97316", "brand_name": "Acme"},
            )
            branding = res.json()
      description: |
        Partial update — MERGES the supplied tokens onto your current
        branding, so sending one field (e.g. `primary_color`) does NOT wipe
        the rest. Only the documented tokens are accepted; any other key is
        rejected with `400`. Colors are validated as hex (`#RRGGBB`),
        `logo_url` must be an absolute `https` URL.
      security:
        - SecretKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandingUpdate'
      responses:
        '200':
          description: Updated branding tokens.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branding'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /webhooks/deliveries:
    get:
      operationId: listPartnerWebhookDeliveries
      tags:
        - Webhooks
      summary: List your webhook deliveries
      description: |
        Your own outbound webhook delivery log (most-recent first) — which
        events delivered, which are retrying, which dead-lettered and why
        (`last_response_status` + `last_error`). Scoped to your partner; you
        never see another partner's deliveries. `payload_raw` is omitted (the
        log is about delivery health, not payload inspection). Filter by
        `status`; page with `limit` (≤200) + `skip`.
      security:
        - SecretKey: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - in_flight
              - succeeded
              - dead_lettered
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: skip
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Delivery log page.
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                  - has_more
                  - url
                properties:
                  object:
                    type: string
                    enum:
                      - list
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
                  has_more:
                    type: boolean
                  url:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /webhooks/deliveries/{id}/replay:
    post:
      operationId: replayWebhookDelivery
      tags:
        - Webhooks
      summary: Replay a dead-lettered delivery
      description: |
        Re-queue a `dead_lettered` delivery for another attempt — the worker
        picks it up on the next tick (status returns to `pending`). Only your
        own deliveries, and only ones in `dead_lettered` state (others 400).
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The re-queued delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDelivery'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /webhooks/test:
    post:
      operationId: sendTestWebhook
      tags:
        - Webhooks
      summary: Send a test event
      description: |
        Enqueue a synthetic `webhook.test` event to your configured
        `webhook_url` so you can verify signature handling + endpoint
        reachability end-to-end without a real event. Returns the queued
        delivery (poll `GET /v1/webhooks/deliveries` for its outcome).
        `400` if you have no `webhook_url` set yet.
      security:
        - SecretKey: []
      responses:
        '200':
          description: The queued test delivery.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDelivery'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /customers:
    post:
      operationId: createCustomer
      tags:
        - Customers
      summary: Create a customer
      description: |
        Create a partner-scoped customer record. Idempotent via the
        `Idempotency-Key` header. `email` is unique per partner, and `external_id`
        is unique per partner when supplied — a duplicate of either returns `409`
        (`customer_already_exists`).
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/customers \
              -H "Authorization: Bearer sk_test_..." \
              -H "Idempotency-Key: $(uuidgen)" \
              -H "Content-Type: application/json" \
              -d '{"email":"jordan@example.com","external_id":"user_42"}'
        - lang: javascript
          label: Node
          source: |
            const res = await fetch('https://gate-api.0bit.app/v1/customers', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Idempotency-Key': crypto.randomUUID(),
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({ email: 'jordan@example.com', external_id: 'user_42' }),
            });
            const customer = await res.json();
        - lang: python
          label: Python
          source: |
            import requests, uuid

            res = requests.post(
                "https://gate-api.0bit.app/v1/customers",
                headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": str(uuid.uuid4())},
                json={"email": "jordan@example.com", "external_id": "user_42"},
            )
            customer = res.json()
      security:
        - SecretKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerRequest'
      responses:
        '201':
          description: The created customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: A customer with this `email` or `external_id` already exists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    get:
      operationId: listCustomers
      tags:
        - Customers
      summary: List customers
      description: |
        Cursor-paginated list of your customers (most-recent first), excluding
        deleted. Filter by `external_id` or `email` (exact match).
      security:
        - SecretKey: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 10
        - name: starting_after
          in: query
          description: A customer id; returns the page after it.
          schema:
            type: string
        - name: external_id
          in: query
          schema:
            type: string
        - name: email
          in: query
          schema:
            type: string
      responses:
        '200':
          description: A page of customers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerList'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /customers/{id}:
    get:
      operationId: getCustomer
      tags:
        - Customers
      summary: Retrieve a customer
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      operationId: updateCustomer
      tags:
        - Customers
      summary: Update a customer
      description: |
        Partial update of mutable fields only. `email`, `phone`, `external_id`,
        `kyc_status` and `mode` are immutable / server-managed (sending them is
        a 400). A field sent as `null` clears it.
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCustomerRequest'
      responses:
        '200':
          description: The updated customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteCustomer
      tags:
        - Customers
      summary: Delete a customer
      description: Soft-delete a customer. Idempotent — deleting an already-deleted customer returns the same shape.
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The deleted marker.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerDeleted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /quotes:
    post:
      operationId: lockQuote
      tags:
        - Rails
      summary: Lock a signed quote
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/quotes \
              -H "Authorization: Bearer sk_test_..." \
              -H "Content-Type: application/json" \
              -d '{
                "currency": "EUR",
                "asset": "USDC",
                "amount": "1000.00",
                "side": "on_ramp",
                "payment_method": "sepa_credit_transfer"
              }'
        - lang: javascript
          label: Node
          source: |
            const res = await fetch('https://gate-api.0bit.app/v1/quotes', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                currency: 'EUR',
                asset: 'USDC',
                amount: '1000.00',
                side: 'on_ramp',
                payment_method: 'sepa_credit_transfer',
              }),
            });
            const quote = await res.json(); // quote.id -> redeem in pay-in/pay-out
        - lang: python
          label: Python
          source: |
            import requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/quotes",
                headers={"Authorization": "Bearer sk_test_..."},
                json={
                    "currency": "EUR",
                    "asset": "USDC",
                    "amount": "1000.00",
                    "side": "on_ramp",
                    "payment_method": "sepa_credit_transfer",
                },
            )
            quote = res.json()
      description: >
        Locks a rate for a short time and returns an HMAC-signed, one-time-redeemable quote. Requires the account-gated
        rail entitlement.
      security:
        - SecretKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - currency
                - asset
                - amount
                - side
                - payment_method
              properties:
                currency:
                  type: string
                  pattern: ^[A-Za-z]{3}$
                asset:
                  type: string
                  pattern: ^[A-Za-z0-9]{2,12}$
                amount:
                  type: string
                  pattern: ^[0-9]+(\\.[0-9]{1,8})?$
                side:
                  type: string
                  enum:
                    - on_ramp
                    - off_ramp
                payment_method:
                  type: string
                  maxLength: 64
                country_code:
                  type: string
                  pattern: ^[A-Z]{2}$
      responses:
        '201':
          description: Signed quote locked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignedQuote'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /quotes/{id}:
    get:
      operationId: retrieveSignedQuote
      tags:
        - Rails
      summary: Retrieve a signed quote
      description: |
        Returns a locked quote (partner-scoped). An elapsed quote reports
        `status: expired` rather than 404.
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Signed quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignedQuote'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /rails/pay_ins:
    post:
      operationId: createPayIn
      tags:
        - Rails
      summary: Create a pay-in
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/rails/pay_ins \
              -H "Authorization: Bearer sk_test_..." \
              -H "Idempotency-Key: $(uuidgen)" \
              -H "Content-Type: application/json" \
              -d '{
                "gate_session_id": "67a1f3b9e4b0c10001234567",
                "quote_id": "qt_9f8e7d6c5b4a"
              }'
        - lang: javascript
          label: Node
          source: |
            import crypto from 'node:crypto';

            const res = await fetch('https://gate-api.0bit.app/v1/rails/pay_ins', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Idempotency-Key': crypto.randomUUID(),
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                gate_session_id: '67a1f3b9e4b0c10001234567',
                quote_id: 'qt_9f8e7d6c5b4a',
              }),
            });
            const payIn = await res.json();
        - lang: python
          label: Python
          source: |
            import uuid, requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/rails/pay_ins",
                headers={
                    "Authorization": "Bearer sk_test_...",
                    "Idempotency-Key": str(uuid.uuid4()),
                },
                json={
                    "gate_session_id": "67a1f3b9e4b0c10001234567",
                    "quote_id": "qt_9f8e7d6c5b4a",
                },
            )
            pay_in = res.json()
      description: >
        Redeems a signed quote and initiates a fiat pay-in for the session user. Requires the account-gated rail
        entitlement and an `Idempotency-Key` header. The user context is derived from the gate session.
      security:
        - SecretKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailRequest'
      responses:
        '201':
          description: Pay-in created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RailPayIn'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /rails/pay_ins/{id}:
    get:
      operationId: retrievePayIn
      tags:
        - Rails
      summary: Retrieve a pay-in
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Pay-in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RailPayIn'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /rails/pay_outs:
    post:
      operationId: createPayOut
      tags:
        - Rails
      summary: Create a pay-out
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://gate-api.0bit.app/v1/rails/pay_outs \
              -H "Authorization: Bearer sk_test_..." \
              -H "Idempotency-Key: $(uuidgen)" \
              -H "Content-Type: application/json" \
              -d '{
                "gate_session_id": "67a1f3b9e4b0c10001234567",
                "quote_id": "qt_1a2b3c4d5e6f"
              }'
        - lang: javascript
          label: Node
          source: |
            import crypto from 'node:crypto';

            const res = await fetch('https://gate-api.0bit.app/v1/rails/pay_outs', {
              method: 'POST',
              headers: {
                Authorization: 'Bearer sk_test_...',
                'Idempotency-Key': crypto.randomUUID(),
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({
                gate_session_id: '67a1f3b9e4b0c10001234567',
                quote_id: 'qt_1a2b3c4d5e6f',
              }),
            });
            const payOut = await res.json();
        - lang: python
          label: Python
          source: |
            import uuid, requests

            res = requests.post(
                "https://gate-api.0bit.app/v1/rails/pay_outs",
                headers={
                    "Authorization": "Bearer sk_test_...",
                    "Idempotency-Key": str(uuid.uuid4()),
                },
                json={
                    "gate_session_id": "67a1f3b9e4b0c10001234567",
                    "quote_id": "qt_1a2b3c4d5e6f",
                },
            )
            pay_out = res.json()
      description: >
        Redeems a signed quote and initiates a fiat pay-out to the session user. Payout instructions come from the
        verified user record, not from the partner request body. Requires the account-gated rail entitlement and an
        `Idempotency-Key` header.
      security:
        - SecretKey: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailRequest'
      responses:
        '201':
          description: Pay-out created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RailPayOut'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /rails/pay_outs/{id}:
    get:
      operationId: retrievePayOut
      tags:
        - Rails
      summary: Retrieve a pay-out
      security:
        - SecretKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Pay-out.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RailPayOut'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
webhooks:
  gate_session.created:
    post:
      operationId: webhookGateSessionCreated
      tags:
        - Webhooks
      summary: Session created
      description: |
        Fired when `POST /v1/gate_sessions` succeeds.

        ## Headers

        - `Gate-Signature: t=<unix-ts>,v1=<hex>` — Stripe-format HMAC.
        - `X-0bit-Timestamp: <unix-ts>` — same value as the `t=` in Signature.
        - `X-0bit-Event-Id: <uuid>` — dedupe key.
        - `X-0bit-Event-Type: gate_session.created`.

        ## Verifying

        ```js
        const crypto = require('crypto');
        function verify(rawBody, header, secret, skewSeconds = 300) {
          const parts = Object.fromEntries(
            header.split(',').map(p => p.split('=')),
          );
          const t = parseInt(parts.t, 10);
          if (Math.abs(Date.now() / 1000 - t) > skewSeconds) return false;
          const expected = crypto
            .createHmac('sha256', secret)
            .update(`${t}.${rawBody}`)
            .digest('hex');
          return crypto.timingSafeEqual(
            Buffer.from(expected, 'hex'),
            Buffer.from(parts.v1, 'hex'),
          );
        }
        ```
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionEvent'
            examples:
              created:
                summary: Session created
                value:
                  id: f47ac10b-58cc-4372-a567-0e02b2c3d479
                  type: gate_session.created
                  created_at: 1716729600
                  data:
                    id: 67a1f3b9e4b0c10001234567
                    object: gate_session
                    partner_id: 507f1f77bcf86cd799439011
                    mode: test
                    amount: '100.00'
                    currency: EUR
                    return_url: https://partner.example/checkout/done
                    status: open
                    expires_at: '2026-05-27T12:00:00.000Z'
      responses:
        '200':
          description: Acknowledged. Any 2xx is treated as success.
  gate_session.completed:
    post:
      operationId: webhookGateSessionCompleted
      tags:
        - Webhooks
      summary: Session completed
      description: Fired when an intent linked to this session succeeds.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionCompletedEvent'
      responses:
        '200':
          description: Acknowledged.
  gate_session.expired:
    post:
      operationId: webhookGateSessionExpired
      tags:
        - Webhooks
      summary: Session expired
      description: Fired lazily on the first read past `expires_at`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionEvent'
      responses:
        '200':
          description: Acknowledged.
  gate_session.cancelled:
    post:
      operationId: webhookGateSessionCancelled
      tags:
        - Webhooks
      summary: Session cancelled
      description: Fired when `POST /v1/gate_sessions/:id/cancel` succeeds.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionEvent'
      responses:
        '200':
          description: Acknowledged.
  gate_session.processing:
    post:
      operationId: webhookGateSessionProcessing
      tags:
        - Webhooks
      summary: Session processing
      description: Fired when a buy/sell linked to the session starts (J.2.2).
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionProcessingEvent'
      responses:
        '200':
          description: Acknowledged.
  gate_session.failed:
    post:
      operationId: webhookGateSessionFailed
      tags:
        - Webhooks
      summary: Session transaction failed
      description: |
        Fired when a buy/sell linked to the session fails (J.2.2). The
        session stays `open` — the user can retry inside the iframe.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GateSessionFailedEvent'
      responses:
        '200':
          description: Acknowledged.
  kyc.required:
    post:
      operationId: webhookKycRequired
      tags:
        - Webhooks
      summary: KYC required
      description: |
        Fired when a session is blocked pending KYC. The user completes KYC
        in the hosted Gate flow; partners never run their own KYC.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KycRequiredEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_in.processing:
    post:
      operationId: webhookRailPayInProcessing
      tags:
        - Webhooks
      summary: Pay-in processing
      description: Fired when a pay-in is accepted and settlement has begun.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_in.settled:
    post:
      operationId: webhookRailPayInSettled
      tags:
        - Webhooks
      summary: Pay-in settled
      description: Fired when a pay-in settles successfully.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_in.failed:
    post:
      operationId: webhookRailPayInFailed
      tags:
        - Webhooks
      summary: Pay-in failed
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_in.cancelled:
    post:
      operationId: webhookRailPayInCancelled
      tags:
        - Webhooks
      summary: Pay-in cancelled
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_out.processing:
    post:
      operationId: webhookRailPayOutProcessing
      tags:
        - Webhooks
      summary: Pay-out processing
      description: Fired when a pay-out is accepted and settlement has begun.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_out.settled:
    post:
      operationId: webhookRailPayOutSettled
      tags:
        - Webhooks
      summary: Pay-out settled
      description: Fired when a pay-out settles successfully.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_out.failed:
    post:
      operationId: webhookRailPayOutFailed
      tags:
        - Webhooks
      summary: Pay-out failed
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  rail.pay_out.cancelled:
    post:
      operationId: webhookRailPayOutCancelled
      tags:
        - Webhooks
      summary: Pay-out cancelled
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RailEvent'
      responses:
        '200':
          description: Acknowledged.
  quote.consumed:
    post:
      operationId: webhookQuoteConsumed
      tags:
        - Webhooks
      summary: Signed quote consumed
      description: Fired when a rail redeems a locked signed quote.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteConsumedEvent'
      responses:
        '200':
          description: Acknowledged.
  webhook.test:
    post:
      operationId: webhookTest
      tags:
        - Webhooks
      summary: Test event
      description: |
        Synthetic event fired by `POST /v1/webhooks/test`. Use it to verify
        your signature handling + endpoint reachability without a real event.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookTestEvent'
      responses:
        '200':
          description: Acknowledged.
  customer.created:
    post:
      operationId: webhookCustomerCreated
      tags:
        - Webhooks
      summary: Customer created
      description: Fired after a successful POST /v1/customers.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerEvent'
      responses:
        '200':
          description: Acknowledged.
  customer.updated:
    post:
      operationId: webhookCustomerUpdated
      tags:
        - Webhooks
      summary: Customer updated
      description: Fired after a successful PATCH /v1/customers/{id}.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerEvent'
      responses:
        '200':
          description: Acknowledged.
  customer.deleted:
    post:
      operationId: webhookCustomerDeleted
      tags:
        - Webhooks
      summary: Customer deleted
      description: Fired after a customer is soft-deleted via DELETE /v1/customers/{id}.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerDeletedEvent'
      responses:
        '200':
          description: Acknowledged.
