openapi: 3.1.0
info:
  title: Von Pay Checkout API
  description: |
    API for Von Payments hosted checkout page. Merchants create checkout sessions
    and redirect buyers to the hosted payment page. The API handles session
    lifecycle, payment orchestration, and webhook processing.
  version: 0.1.0
  contact:
    name: Von Payments
    url: https://vonpay.com

servers:
  - url: https://checkout.vonpay.com
    description: Production
  - url: http://localhost:3001
    description: Local development

security:
  - BearerAuth: []

paths:
  /v1/sessions:
    post:
      operationId: createSession
      summary: Create a checkout session
      description: |
        Creates a checkout session and returns a checkout URL. The merchant
        redirects the buyer to this URL to complete payment.

        Accepts both publishable (`vp_pk_*`) and secret (`vp_sk_*`) API keys.
        This is the only endpoint accessible with publishable keys.

        Sessions default to a 30-minute TTL. Override via `expiresIn` (range 300-86400 — 5 min to 24 h).

        **Mode (sandbox vs live) is derived from the authenticating API key's
        merchant config** (`merchants.is_sandbox` on the replicated merchants
        table), NOT from any request body field. The decision is frozen at
        session creation and persists for the session's lifetime — subsequent
        reads and webhook reconciliation honor the same mode. The request body
        has no `sandbox` or `mode=test|live` field; attempting to send one is
        ignored.
      tags:
        - Sessions
      security:
        - BearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Unique key to ensure idempotent session creation
          schema:
            type: string
            maxLength: 255
            example: "order_123_attempt_1"
        - name: dry_run
          in: query
          required: false
          description: |
            Validate the request without creating a session. Returns
            `{ "valid": true, "warnings": [] }` on success or
            `400` with validation errors. No session is created and
            no payment provider is contacted.
          schema:
            type: boolean
            default: false
        - name: Von-Pay-Version
          in: header
          required: false
          description: API version date (e.g. 2026-04-14). Echoed in response.
          schema:
            type: string
            example: "2026-04-14"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
            examples:
              basic:
                summary: Basic payment
                value:
                  merchantId: default
                  amount: 1499
                  currency: USD
                  country: US
                  successUrl: https://mystore.com/confirm
              withItems:
                summary: With line items and buyer info
                value:
                  merchantId: default
                  amount: 2298
                  currency: USD
                  country: US
                  successUrl: https://mystore.com/order/123/confirm
                  cancelUrl: https://mystore.com/cart
                  buyerId: cust_123
                  buyerName: Jane Doe
                  buyerEmail: jane@example.com
                  lineItems:
                    - name: Premium Widget
                      quantity: 1
                      unitAmount: 1499
                    - name: Shipping
                      quantity: 1
                      unitAmount: 799
                  metadata:
                    orderId: order_123
      responses:
        "201":
          description: Session created
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: |
            Auth service unavailable — emitted with `code: auth_service_unavailable`
            when the replicated auth schema is mid-migration and the key-state
            classifier cannot safely enforce grace / expiry predicates (PostgreSQL
            `42703` undefined_column fail-closed path). Clients should retry with
            exponential backoff; the condition clears as soon as replication
            catches up.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/sessions/{sessionId}:
    get:
      operationId: getSession
      summary: Get session status
      description: |
        Returns the current status and details of a checkout session.
        Requires a secret key (`vp_sk_*`). Publishable keys receive 403.
      tags:
        - Sessions
      security:
        - BearerAuth: []
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            example: vp_cs_live_k7x9m2n4p3
      responses:
        "200":
          description: Session found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Forbidden — publishable keys cannot access this endpoint
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          description: |
            Auth service unavailable — emitted with `code: auth_service_unavailable`
            when the replicated auth schema is mid-migration and the key-state
            classifier cannot safely enforce grace / expiry predicates. Retry
            with exponential backoff.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/checkout/init:
    post:
      operationId: initCheckout
      summary: Initialize payment embed (internal)
      description: |
        Called by the hosted checkout page to initialize the payment embed.
        Validates the session, moves it to "processing", and returns a provider token.

        **Internal use only** — not intended for merchant integration.
      tags:
        - Checkout (Internal)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sessionId
              properties:
                sessionId:
                  type: string
                  example: vp_cs_live_k7x9m2n4p3
      responses:
        "200":
          description: Checkout initialized
          content:
            application/json:
              schema:
                type: object
                description: |
                  Response fields vary by payment path. Only the fields needed
                  for the active payment method are returned.
                properties:
                  type:
                    type: string
                    description: |
                      Payment method type for client-side rendering. Vendor-neutral
                      string per `provider/no-vendor-names-exposed`. Values:
                      - `card_element` — Stripe Connect Direct
                      - `hosted_fields` — connector-hosted iframe (Gr4vy)
                      - `vault_hosted_fields` — tokenize-then-charge flow (Spreedly)
                      - `agent_hosted_fields` — agent-attested SDK (Aspire)
                      - `sandbox` — mock UI for sandbox merchants
                    enum: [card_element, hosted_fields, vault_hosted_fields, agent_hosted_fields, sandbox]
                  token:
                    type: string
                    description: Payment embed token or client secret
                  environment:
                    type: string
                    enum: [sandbox, production]
                  publishableKey:
                    type: string
                    description: Client-side publishable key (card_element / agent_hosted_fields only)
                  accountId:
                    type: string
                    description: Connected account ID (card_element / agent_hosted_fields only)
                  gatewayInstanceId:
                    type: string
                    description: |
                      Opaque connector instance identifier (hosted_fields / vault_hosted_fields).
                      The client SDK derives the embed host URL from this id;
                      the response body itself does not include vendor host strings.
                  merchantAccountId:
                    type: string
                    description: Merchant account ID on the connector (hosted_fields / vault_hosted_fields)
                  chargeEndpoint:
                    type: string
                    description: Server-side charge endpoint path (agent_hosted_fields only)
                  applicationFeeCents:
                    type: integer
                    minimum: 0
                    description: |
                      Platform application fee in minor units (hosted_fields /
                      vault_hosted_fields, present when fee > 0). The client
                      converts this to vendor-specific connector wire shape
                      near the SDK setup; the response body itself stays
                      vendor-neutral.
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Session is not in a valid state for initialization
        "410":
          description: Session has expired
        "503":
          description: |
            Session state changed mid-init (rare race between concurrent
            re-init calls and webhook landing). Buyer's next click will
            succeed.

  /api/checkout/complete:
    post:
      operationId: completeCheckout
      summary: Complete checkout (internal)
      description: |
        Called by the hosted checkout page after the payment embed reports a
        successful transaction. Updates session status and returns a signed
        redirect URL.

        **Internal use only** — not intended for merchant integration.
      tags:
        - Checkout (Internal)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - sessionId
                - transactionId
              properties:
                sessionId:
                  type: string
                transactionId:
                  type: string
      responses:
        "200":
          description: Checkout completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: succeeded
                  sessionId:
                    type: string
                  transactionId:
                    type: string
                  redirectUrl:
                    type: string
                    nullable: true
                    description: Signed redirect URL for the merchant

  /api/webhooks/{gatewayId}:
    post:
      operationId: receiveWebhook
      summary: Receive payment webhook
      description: |
        Webhook endpoint for payment providers. Each gateway has its own
        endpoint path. Persists the event and reconciles with checkout
        sessions asynchronously.

        **Internal use only** — webhook URLs are configured in provider
        dashboards, not by merchants.
      tags:
        - Webhooks
      security: []
      parameters:
        - name: gatewayId
          in: path
          required: true
          schema:
            type: string
            example: vp_gw_xxxx
          description: Internal gateway identifier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: string
                  description: Webhook event ID
                type:
                  type: string
                  description: Event type (e.g. transaction.captured)
                data:
                  type: object
                  description: Event payload
      responses:
        "200":
          description: Webhook received
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    example: true

  /api/health:
    get:
      operationId: healthCheck
      summary: Health check
      description: Returns the health status of the checkout service and its dependencies.
      tags:
        - Operations
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
        "503":
          description: Service is degraded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"

  /api/checkout/client-error:
    post:
      operationId: reportClientError
      summary: Report a client-side checkout error (internal)
      description: |
        Called by the hosted checkout page to report recoverable client-side
        errors (payment embed failures, form validation trips, provider SDK
        warnings). Origin-validated; request body capped at 4 KB.

        **Internal use only** — not intended for merchant integration.
      tags:
        - Checkout (Internal)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
                message:
                  type: string
                  maxLength: 2000
                context:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Client error logged
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        "400":
          description: Validation error — malformed body
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: false
                  error:
                    type: string
        "403":
          description: Origin rejected — request did not pass origin check
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: false
                  error:
                    type: string
        "413":
          description: Payload too large — body exceeded 4 KB cap
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: false
                  error:
                    type: string
                    example: Payload too large

  /v1/capabilities:
    get:
      operationId: getCapabilities
      summary: Retrieve the merchant's supported-operations matrix
      description: |
        Returns the capability matrix for the authenticated merchant —
        which discrete-lifecycle operations the merchant's payment
        provider supports (deferred capture, partial refund, MIT,
        network tokens, 3DS2, ACH, payouts API).

        Capability values are merchant-visible feature flags only. The
        response NEVER carries the merchant's payment provider name,
        gateway type, or any vendor-specific identifier. Integrators
        write generic code against the matrix; SDKs use it to gate
        conditional UX (e.g. only show "Save card for later" when
        `mit: true`).

        Accepts both publishable (`vp_pk_*`) and secret (`vp_sk_*`)
        API keys. Capabilities are read-only metadata.

        Sandbox merchants always receive the sandbox capability matrix
        regardless of any stored gateway type.

        Cached client-side for 60 seconds. Capabilities change on the
        order of hours when a merchant onboards a new provider — a
        slightly-stale matrix on a 60-second window is acceptable.
      tags:
        - Capabilities
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Capability matrix retrieved successfully.
          headers:
            X-Request-Id:
              schema:
                type: string
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=60"
            Vary:
              schema:
                type: string
                example: "Authorization"
          content:
            application/json:
              schema:
                type: object
                required: [supported_operations, settlement_currencies, rate_limits]
                properties:
                  supported_operations:
                    type: object
                    required:
                      - auth_capture_separation
                      - partial_capture
                      - partial_refund
                      - unreferenced_refund
                      - void_after_capture
                      - mit
                      - network_tokens
                      - three_d_secure_2
                      - ach
                      - payouts_api
                    properties:
                      auth_capture_separation: { type: boolean }
                      partial_capture: { type: boolean }
                      partial_refund: { type: boolean }
                      unreferenced_refund: { type: boolean }
                      void_after_capture:
                        type: string
                        enum: [supported, rerouted_to_refund, not_supported]
                      mit: { type: boolean }
                      network_tokens: { type: boolean }
                      three_d_secure_2: { type: boolean }
                      ach: { type: boolean }
                      payouts_api: { type: boolean }
                  settlement_currencies:
                    type: array
                    items:
                      type: string
                      pattern: "^[A-Z]{3}$"
                    example: ["USD", "EUR", "GBP", "CAD", "AUD"]
                  rate_limits:
                    type: object
                    properties:
                      payment_intents_per_minute:
                        type: integer
                        example: 30
        "401":
          description: Authentication failed.
        "422":
          description: Merchant is not configured (no replicated row found yet).

  /v1/payment_intents:
    post:
      operationId: createPaymentIntent
      summary: Create a payment intent
      description: |
        Creates a Vora payment intent. The same request shape works
        regardless of which payment provider processes the charge
        underneath. Each provider activates independently and live
        activations are announced in the changelog.

        **Behavior:**
        - Sandbox merchants (test API keys): full end-to-end flow. Returns
          201 with a normalized `PaymentIntent` shape. Sandbox is
          deterministic — `amount: 200` always returns
          `status: failed, decline_code: card_declined`; any other amount
          returns `status: succeeded`.
        - Live merchants whose payment provider has been activated: full
          end-to-end flow against the real provider. Returns 201 with the
          same normalized shape.
        - Live merchants whose payment provider has not yet been activated
          for the discrete-lifecycle API: returns 501 `endpoint_not_implemented`.
          Sandbox keys continue to work while you wait.

        **Idempotency:**
        - Pass an `Idempotency-Key` header (≤ 255 printable ASCII chars).
          Replay with the same key returns the original response with
          HTTP 200 (vs 201 on first create).
        - Replay with the same key but different `amount` / `currency` /
          `capture_method` returns 422 `idempotency_replay_incompatible`.
          Use a fresh key or send the original body.

        **Versioning:** propagate `Von-Pay-Version` header for stability.
      tags:
        - Payment Intents
      security:
        - BearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
            pattern: "^[\\x20-\\x7E]+$"
        - name: Von-Pay-Version
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency]
              additionalProperties: false
              properties:
                amount:
                  type: integer
                  minimum: 1
                  description: Minor units (cents). 1499 = $14.99.
                currency:
                  type: string
                  pattern: "^[A-Za-z]{3}$"
                  description: ISO 4217 alpha-3 currency code. Normalized to uppercase server-side.
                capture_method:
                  type: string
                  enum: [automatic, manual]
                  default: automatic
                metadata:
                  type: object
                  description: |
                    Free-form merchant metadata. Server scrubs PII keys
                    (buyer email/name, card-adjacent fields, platform-fee
                    fields, raw provider response) before storage. Total
                    serialized size capped at 8 KB.
                  additionalProperties: true
      responses:
        "200":
          description: Idempotent replay — original response returned unchanged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentIntent"
        "201":
          description: Payment intent created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentIntent"
        "400":
          description: Invalid request (Zod validation, oversized Idempotency-Key, or oversized metadata).
        "401":
          description: Authentication failed.
        "403":
          description: Publishable key used (writes require secret keys).
        "409":
          description: Merchant has no configured gateway and is not sandbox-mode.
        "422":
          description: |
            Either:
            - `merchant_not_configured` (replicated row missing), or
            - `idempotency_replay_incompatible` (Idempotency-Key reused with mismatched body).
        "501":
          description: |
            `endpoint_not_implemented` — the merchant's payment provider has
            not yet been activated for the discrete-lifecycle API. Sandbox
            keys continue to work; activations are announced per-provider
            in the changelog.

  /v1/sdk-telemetry:
    post:
      operationId: postSdkTelemetry
      summary: Submit an opt-in SDK telemetry event
      description: |
        Optional integrator-side error reporting. Vonpay never receives data
        unless the SDK constructor explicitly opts in
        (`telemetry: { enabled: true }`, default `false`).

        **Secret keys only.** Publishable keys are rejected with `403
        auth_key_type_forbidden` — there is no legitimate browser-side caller
        for this route.

        The schema is intentionally narrow. `sdk_name` and `operation` are
        closed enums. `runtime` matches `/^[a-z][a-z0-9._+-]{0,62}$/i` to
        block path / env-var / `=` injection vectors. `sdk_version` matches
        semver. `request_id_hash` is 64-char SHA-256 hex of the caller's
        `X-Request-Id` — the raw value is never accepted (devsec-mandated
        soft-PII guard against join-paths back to encrypted buyer PII via
        `checkout_request_logs`). Schema is `.strict()` — any unknown field
        returns 400.

        Server-side blocklist regex on string fields rejects shapes matching
        common secrets (`vp_sk_*`, `sk_live_*`, `whsec_*`) and emails as
        defense-in-depth.

        Replay-defense: `occurred_at` must be within ±5 minutes of server
        clock. Rate limit 30/min keyed on the API key hash.

        Insert is best-effort — a storage-layer blip surfaces as `503
        provider_unavailable`. The SDK MUST drop and not retry on any 5xx
        from this endpoint; telemetry must not generate retry pressure.

        Full design + privacy posture at `docs/_design/phase-3-sdk-telemetry.md`.
      tags:
        - SDK Telemetry
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sdk_name, sdk_version, runtime, error_code, operation, occurred_at]
              additionalProperties: false
              properties:
                sdk_name:
                  type: string
                  enum:
                    - checkout-node
                    - checkout-python
                    - checkout-php
                    - checkout-ruby
                sdk_version:
                  type: string
                  maxLength: 32
                  pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-z0-9.]+)?$"
                  example: "0.3.1"
                runtime:
                  type: string
                  maxLength: 64
                  pattern: "^[a-z][a-z0-9._+-]{0,62}$"
                  example: "node-20.10.0"
                error_code:
                  type: string
                  maxLength: 64
                  description: |
                    From the existing error-code catalog. Schema-level
                    enforcement is by length + blocklist refine (not closed
                    enum) because `ErrorCode` evolves on the server side and
                    a strict enum would force an SDK schema-bump on every
                    catalog addition.
                  example: "auth_invalid_key"
                operation:
                  type: string
                  enum:
                    - sessions.create
                    - sessions.retrieve
                    - webhooks.constructEvent
                    - webhooks.verifySignature
                request_id_hash:
                  type: string
                  pattern: "^[0-9a-f]{64}$"
                  description: |
                    SHA-256 hex of the caller's `X-Request-Id`. Raw
                    `request_id` MUST NOT be sent — see the design doc.
                occurred_at:
                  type: string
                  format: date-time
                  description: ISO 8601. Must be within ±5 minutes of server clock.
                context:
                  type: object
                  additionalProperties: false
                  properties:
                    duration_ms:
                      type: integer
                      minimum: 0
                      maximum: 120000
                    retry_count:
                      type: integer
                      minimum: 0
                      maximum: 10
                    http_status:
                      type: integer
                      minimum: 0
                      maximum: 599
                    payload_size_bytes:
                      type: integer
                      minimum: 0
                      maximum: 1048576
      responses:
        "204":
          description: Event accepted; no body returned.
          headers:
            X-Request-Id:
              schema:
                type: string
        "400":
          description: |
            `validation_error` — Zod schema rejected the body (unknown field,
            wrong shape, regex mismatch, blocklist hit, or `occurred_at`
            outside the ±5min replay window).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid auth (`auth_missing_bearer` / `auth_invalid_key`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: |
            `auth_key_type_forbidden` — endpoint requires a secret key
            (`vp_sk_*`); publishable key rejected.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: |
            `rate_limit_exceeded_per_key` — 30/min per API key on this bucket.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: |
            `provider_unavailable` — telemetry pipeline degraded.
            SDK MUST drop and not retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/csp-report:
    post:
      operationId: receiveCspReport
      summary: Receive a browser CSP violation report (internal)
      description: |
        Accepts Content-Security-Policy violation reports emitted by the buyer's
        browser from the hosted checkout page. Unauthenticated by spec — CSP
        reports are dispatched by the browser and carry no auth envelope.
        Defended by Origin check and rate-limited via the `clientError` bucket.

        Body content-type is `application/csp-report` (the legacy browser
        format); `application/reports+json` may also be emitted by newer
        browsers and is accepted.

        **Internal use only** — not part of the merchant API.
      tags:
        - Operations
      security: []
      requestBody:
        required: true
        content:
          application/csp-report:
            schema:
              type: object
              description: |
                Browser-emitted CSP violation report. Shape per the W3C
                Reporting API / legacy `application/csp-report` format.
              properties:
                csp-report:
                  type: object
                  additionalProperties: true
          application/reports+json:
            schema:
              type: array
              items:
                type: object
                additionalProperties: true
      responses:
        "204":
          description: Report accepted
        "400":
          description: Malformed report body
        "403":
          description: Origin rejected
        "413":
          description: Payload too large

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API key authentication. Pass your API key as a Bearer token.

        Secret keys (`vp_sk_*`) have full API access. Publishable keys (`vp_pk_*`)
        can only create sessions. Legacy keys (`vp_key_*`) are treated as secret.

        ```
        Authorization: Bearer vp_sk_live_xxx
        ```

  headers:
    X-Request-Id:
      schema:
        type: string
      description: Unique request ID for tracing and debugging

  schemas:
    PaymentIntent:
      type: object
      description: |
        Vora's unified payment intent shape — same shape regardless of
        which payment provider processes the underlying transaction. Raw
        vendor decline codes are persisted in the durable ledger and
        surfaced only via ops-auth endpoints.
      required:
        - id
        - status
        - amount
        - currency
        - capture_method
      additionalProperties: false
      properties:
        id:
          type: string
          pattern: "^vpi_(test|live)_[a-zA-Z0-9]+$"
        status:
          type: string
          enum: [requires_action, authorized, captured, succeeded, voided, failed]
        amount:
          type: integer
          minimum: 0
        currency:
          type: string
          pattern: "^[A-Z]{3}$"
        capture_method:
          type: string
          enum: [automatic, manual]
        next_action:
          nullable: true
          oneOf:
            - type: object
              required: [type, redirect_to_url]
              properties:
                type:
                  type: string
                  enum: [redirect_to_url]
                redirect_to_url:
                  type: object
                  required: [url]
                  properties:
                    url:
                      type: string
                      format: uri
        decline_code:
          nullable: true
          type: string
          enum:
            - card_declined
            - insufficient_funds
            - expired_card
            - incorrect_cvc
            - incorrect_zip
            - card_velocity_exceeded
            - fraudulent
            - stolen_card
            - lost_card
            - do_not_honor
            - issuer_unavailable
            - processing_error
            - generic_decline
        card:
          nullable: true
          type: object
          required: [brand, last4]
          properties:
            brand:
              type: string
              enum: [visa, mastercard, amex, discover, diners, jcb, unionpay, unknown]
            last4:
              type: string
              pattern: "^[0-9]{4}$"
        created_at:
          nullable: true
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true

    CreateSessionRequest:
      type: object
      required:
        - merchantId
        - amount
        - currency
        - country
      properties:
        merchantId:
          type: string
          description: Merchant account ID
          maxLength: 100
          example: default
        amount:
          type: integer
          description: Amount in minor units (cents)
          minimum: 1
          maximum: 99999999
          example: 1499
        currency:
          type: string
          description: ISO 4217 currency code
          minLength: 3
          maxLength: 3
          example: USD
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          minLength: 2
          maxLength: 2
          example: US
        mode:
          type: string
          description: Session mode
          enum:
            - payment
          default: payment
        successUrl:
          type: string
          format: uri
          description: Redirect URL after successful payment (HTTPS required)
          maxLength: 2048
          example: https://mystore.com/order/123/confirm
        cancelUrl:
          type: string
          format: uri
          description: Redirect URL when buyer cancels (HTTPS required)
          maxLength: 2048
          example: https://mystore.com/cart
        description:
          type: string
          description: Human-readable description of the payment
          maxLength: 500
        locale:
          type: string
          description: Locale for the checkout page (e.g. en-US)
          maxLength: 10
        expiresIn:
          type: integer
          description: Session TTL in seconds, default 1800
          minimum: 300
          maximum: 3600
        buyerId:
          type: string
          description: Merchant's external buyer/customer ID
          maxLength: 200
        buyerName:
          type: string
          description: Buyer's full name (encrypted at rest)
          maxLength: 200
        buyerEmail:
          type: string
          format: email
          description: Buyer's email address (encrypted at rest)
          maxLength: 254
        lineItems:
          type: array
          description: Order line items to display on the checkout page
          maxItems: 100
          items:
            $ref: "#/components/schemas/LineItem"
        metadata:
          type: object
          description: Arbitrary key-value metadata (passed through to webhooks)
          additionalProperties:
            type: string
            maxLength: 500

    LineItem:
      type: object
      required:
        - name
        - quantity
        - unitAmount
      properties:
        name:
          type: string
          maxLength: 200
          example: Premium Widget
        quantity:
          type: integer
          minimum: 1
          maximum: 9999
          example: 1
        unitAmount:
          type: integer
          minimum: 0
          description: Unit price in minor units (cents)
          example: 1499
        imageUrl:
          type: string
          format: uri
          maxLength: 2048

    CheckoutSession:
      type: object
      properties:
        id:
          type: string
          description: Session ID (prefixed, e.g. vp_cs_live_xxx)
          example: vp_cs_live_k7x9m2n4p3
        checkoutUrl:
          type: string
          format: uri
          description: URL to redirect the buyer to
          example: https://checkout.vonpay.com/checkout?session=vp_cs_live_k7x9m2n4p3
        expiresAt:
          type: string
          format: date-time
          description: Session expiry time (30 minutes from creation)

    SessionStatus:
      type: object
      properties:
        id:
          type: string
          example: vp_cs_live_k7x9m2n4p3
        status:
          type: string
          enum:
            - pending
            - processing
            - succeeded
            - failed
            - expired
        merchantId:
          type: string
        amount:
          type: integer
        currency:
          type: string
        mode:
          type: string
          enum:
            - payment
        description:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        collectShipping:
          type: boolean
        shippingAddress:
          type: object
          nullable: true
          description: Shipping address (only returned when status is succeeded)
        transactionId:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
          additionalProperties:
            type: string
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    HealthStatus:
      type: object
      properties:
        status:
          type: string
          enum: [ok, healthy, degraded]
        circuits:
          type: object
          description: Circuit breaker states
          additionalProperties:
            type: string
        checks:
          type: object
          description: Dependency health (only with ?deep=true)
          additionalProperties:
            type: string
            enum: [ok, error, skipped]
        latencyMs:
          type: integer
        timestamp:
          type: string
          format: date-time

    Error:
      type: object
      required:
        - error
        - code
        - fix
        - docs
        - selfHeal
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
          enum:
            - auth_missing_bearer
            - auth_invalid_key
            - auth_key_expired
            - auth_key_type_forbidden
            - auth_merchant_inactive
            - auth_service_unavailable
            - session_not_found
            - session_expired
            - session_wrong_state
            - session_integrity_error
            - validation_error
            - validation_missing_field
            - validation_invalid_amount
            - merchant_not_configured
            - rate_limit_exceeded
            - rate_limit_exceeded_per_key
            - provider_unavailable
            - provider_attestation_failed
            - provider_charge_failed
            - internal_error
            - webhook_missing_signature
            - webhook_invalid_signature
            - webhook_not_configured
            - origin_forbidden
            - transaction_verification_failed
            - unsupported_media_type
        fix:
          type: string
          description: Actionable fix suggestion for the developer
        docs:
          type: string
          format: uri
          description: Link to relevant documentation
        selfHeal:
          type: object
          description: |
            Phase 2.5b self-healing remediation envelope (bridge 2026-04-25
            21:21Z). Mirrors the SDK's `VonPayError.selfHeal` surface so
            curl / PHP / Ruby integrators get the same structured guidance
            that the Node SDK exposes. Zero PII; deterministic from `code`.
          required:
            - retryable
            - nextAction
            - llmHint
          properties:
            retryable:
              type: boolean
              description: Whether the same request can succeed on retry without changes
            nextAction:
              type: string
              description: Highest-level remediation step the caller should take next
              enum:
                - retry
                - rotate_key
                - fix_request
                - wait_and_retry
                - contact_support
                - complete_onboarding
                - create_new_session
                - no_action
            llmHint:
              type: string
              description: 1-3 sentence natural-language hint for an LLM agent debugging the failure
            actions:
              type: array
              description: |
                Optional list of structured machine-readable remediation
                steps. Each step has a discriminated `type`. Five types
                today: `verify_env_var`, `check_format`, `regenerate_key`,
                `wait_and_retry`, `contact_support`.
              items:
                type: object
                required: [type]
                properties:
                  type:
                    type: string
                    enum:
                      - verify_env_var
                      - check_format
                      - regenerate_key
                      - wait_and_retry
                      - contact_support
                  name:
                    type: string
                    description: For `verify_env_var` — the env var name
                  field:
                    type: string
                    description: For `check_format` — the field being validated
                  expected_prefix:
                    type: array
                    items: { type: string }
                  expected_pattern:
                    type: string
                  url:
                    type: string
                    format: uri
                    description: For `regenerate_key` — dashboard URL
                  retryAfterSeconds:
                    type: integer
                    description: For `wait_and_retry` — recommended backoff in seconds
                  context:
                    type: string
                    description: For `contact_support` — short reason code

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Too many requests
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
