openapi: 3.1.0
info:
  title: CoverPay API
  version: "1.0"
  summary: One integration for Klarna, Affirm, Afterpay and Zip, smart-routed, with CoverPay's own pay-in-4 as the fallback.
  description: |
    CoverPay works like Plaid Link. Your server creates a **link token** with your secret key, your page or app
    opens **Link** with that token, and your server exchanges the **public token** Link returns for the payment.

    Flow:
    1. `POST /v1/link/token/create` (server, secret key). Amount, order and customer live on the token.
    2. Open Link in the browser or app with only the `link_token` (Web, React or iOS SDK).
    3. Link calls `onSuccess` with a `public_token`. Send it to your server.
    4. `POST /v1/link/public_token/exchange` (server, secret key) returns the payment.
    5. Webhooks are the source of truth for whether an order is paid.

    Rules that hold everywhere:
    - Amounts are integer **cents**. The amount always comes from the link token, never from the client.
    - Secret keys (`cp_test_…`, `cp_live_…`) belong on your server only. Test keys work only in sandbox, live keys only in production.
    - Errors look like `{ "error": "message", "code": "MACHINE_CODE" }`.
    - Human docs: https://coverpayme.com/docs. Plain-text docs for AI tools: https://coverpayme.com/llms.txt.
  contact:
    url: https://coverpayme.com/docs
servers:
  - url: https://api.coverpayme.com
    description: Production API (test keys are accepted for sandbox payments)
security:
  - ApiKey: []
tags:
  - name: Link
    description: Link tokens and public token exchange (Plaid-style).
  - name: Payments
    description: Read a payment back after it completes.
  - name: CoverPay plans
    description: CoverPay's own pay-in-4 plan, used when no provider approves.
  - name: Payouts
    description: Merchant payouts through Stripe Connect.
paths:
  /v1/link/token/create:
    post:
      tags: [Link]
      operationId: createLinkToken
      summary: Create a link token
      description: |
        Server-side. Creates a short-lived token that carries the amount, order and customer. Pass only the
        returned `link_token` to the client. Default lifetime is 30 minutes for `checkout`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkTokenCreateRequest"
            examples:
              checkout:
                value:
                  amount: 12000
                  currency: USD
                  merchantName: My Store
                  orderId: order_123
                  customerId: user_456
                  customerEmail: buyer@example.com
              sandboxProvidersDecline:
                summary: Sandbox only. Makes every provider decline so CoverPay's own plan is offered.
                value:
                  amount: 12000
                  orderId: order_1
                  customerId: user_1
                  metadata:
                    coverpay_test: providers_decline
      responses:
        "200":
          description: Link token created.
          content:
            application/json:
              schema:
                type: object
                required: [link_token, expiration, request_id]
                properties:
                  link_token:
                    type: string
                    description: "`lt_test_…` or `lt_live_…`. Safe to give to the client."
                  expiration:
                    type: string
                    format: date-time
                  request_id:
                    type: string
                    format: uuid
        "400":
          $ref: "#/components/responses/InvalidParameters"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /v1/link/token/invalidate:
    post:
      tags: [Link]
      operationId: invalidateLinkToken
      summary: Invalidate an unused link token
      description: Server-side. Cancels a token that has not been used. A token that is already used returns 410 `USED_LINK_TOKEN`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [linkToken]
              properties:
                linkToken:
                  type: string
      responses:
        "200":
          description: Token invalidated.
        "400":
          $ref: "#/components/responses/InvalidParameters"
        "410":
          description: The token was already used.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/link/public_token/exchange:
    post:
      tags: [Link]
      operationId: exchangePublicToken
      summary: Exchange a public token for the payment
      description: |
        Server-side, with your secret key. Trade the `public_token` that Link's `onSuccess` returned for the payment.
        Safe to repeat: if your server lost the answer, exchange the same token again and you get the same payment
        back (`already_exchanged: true`). No money moves on an exchange. Public tokens expire after 30 minutes.
        Another business's key gets 404, the same as a token that does not exist.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [public_token]
              properties:
                public_token:
                  type: string
                  description: "Starts with `public-` (for example `public-sandbox-…`)."
      responses:
        "200":
          description: The payment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExchangeResponse"
        "400":
          description: Missing or malformed `public_token` (`INVALID_PUBLIC_TOKEN`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Token not found, not yours, or not completed (`INVALID_PUBLIC_TOKEN`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "410":
          description: Public token expired (`EXPIRED_PUBLIC_TOKEN`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/bnpl/sessions/{sessionId}:
    get:
      tags: [Payments]
      operationId: getPayment
      summary: Read a payment
      description: |
        Server-side. The `payment_id` from the exchange and from webhooks is this `sessionId`. Before marking an
        order paid from a webhook, read the payment back here and check that `orderId` and `totalAmount` match your order.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The payment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"
        "404":
          description: Not found for this business.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /v1/credit/loans/{id}:
    get:
      tags: [CoverPay plans]
      operationId: getPlan
      summary: Read a CoverPay plan and its schedule
      description: Server-side. `id` is the `payment_id` of a payment whose `provider` is `coverpay`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The plan.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Plan"
  /v1/credit/loans/{id}/make-due:
    post:
      tags: [CoverPay plans]
      operationId: makePlanPaymentDue
      summary: Sandbox only. Make the next plan payment due now
      description: The collector then charges it within a few minutes. Sandbox keys only.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The next payment is now due.
  /v1/credit/loans/{id}/collect-next:
    post:
      tags: [CoverPay plans]
      operationId: collectNextPlanPayment
      summary: Sandbox only. Charge the next plan payment immediately
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The charge was attempted.
  /v1/payouts/onboarding:
    post:
      tags: [Payouts]
      operationId: startPayoutOnboarding
      summary: Start Stripe-hosted payout onboarding
      description: Set up where CoverPay pays you (Stripe Connect Express). Do this once per business.
      responses:
        "200":
          description: Onboarding details, including a Stripe-hosted link to finish.
  /v1/payouts:
    get:
      tags: [Payouts]
      operationId: listPayouts
      summary: List payouts
      responses:
        "200":
          description: Payouts for this business.
webhooks:
  paymentEvent:
    post:
      summary: A payment or plan event
      description: |
        Register an endpoint in the dashboard (https://app.coverpayme.com, Webhooks).

        **Verify the signature** over the raw body before parsing. Header
        `X-CoverPay-Signature-V2: t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 of `"<t>.<raw body>"` with your
        endpoint's signing secret. Reject timestamps more than 5 minutes old. The older `X-CoverPay-Signature`
        (HMAC of the body without a timestamp) is still sent; prefer V2.

        **Delivery.** Answer 2xx once recorded. Any other answer, or a timeout, is retried after 1 minute,
        5 minutes and 30 minutes. Events can arrive more than once and out of order: record each `payment_id` once.

        **Treat the event as a hint.** Read the payment back (`GET /v1/bnpl/sessions/{payment_id}`) and check the
        order id and amount before marking an order paid.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            example:
              id: evt_3UO1
              type: session.captured
              created: "2026-10-07T21:05:00.000Z"
              data:
                payment_id: pi_3UO1
                order_id: order_123
                amount: 12000
                currency: USD
                provider: zip
                session_status: captured
      responses:
        "200":
          description: Recorded.
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: Your secret key, `cp_test_…` (sandbox) or `cp_live_…` (production). Server-side only.
  responses:
    InvalidParameters:
      description: Invalid parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string
          description: |
            Machine-readable code, for example INVALID_LINK_TOKEN, EXPIRED_LINK_TOKEN, USED_LINK_TOKEN,
            INVALID_PUBLIC_TOKEN, EXPIRED_PUBLIC_TOKEN.
        details:
          type: array
          items: {}
    LinkTokenCreateRequest:
      type: object
      required: [amount]
      properties:
        amount:
          type: integer
          minimum: 1
          description: Cents. Take it from your own catalog or cart, never from the client.
        currency:
          type: string
          default: USD
        merchantName:
          type: string
        orderId:
          type: string
          description: Your order id. It comes back on the exchange and in webhooks.
        customerId:
          type: string
        customerEmail:
          type: string
          format: email
        providers:
          type: array
          items:
            type: string
          description: Limit the providers offered.
        mode:
          type: string
          enum: [choice, auto]
          default: choice
          description: "`choice` shows the shopper the offers; `auto` routes to the best one."
        scenario:
          type: string
          enum: [full, bucketing, downpayment, coverpay]
          default: full
        allowedDomains:
          type: array
          items:
            type: string
        metadata:
          type: object
          additionalProperties: true
          description: "Sandbox flag `coverpay_test: providers_decline` makes every provider decline."
        purpose:
          type: string
          enum: [checkout, update, eligibility, terms]
          default: checkout
          description: Default lifetime is 30 minutes for `checkout` and `update`, 4 hours for `eligibility` and `terms`.
        expiresInSeconds:
          type: integer
          minimum: 60
          maximum: 86400
        checkout_customization_name:
          type: string
          description: Named checkout configuration (for A/B testing).
    ExchangeResponse:
      type: object
      properties:
        payment_id:
          type: string
          description: The CoverPay payment id. The same id appears in webhooks.
        status:
          type: [string, "null"]
          description: "`captured` or `authorized` means the shopper's plan went through."
        provider:
          type: [string, "null"]
          description: klarna, affirm, afterpay, zip or coverpay.
        amount:
          type: integer
          description: Cents, from the link token.
        currency:
          type: string
        order_id:
          type: [string, "null"]
          description: Your order id from the link token. Check it matches the order you are completing.
        customer_id:
          type: [string, "null"]
        completed_at:
          type: [string, "null"]
          format: date-time
        already_exchanged:
          type: boolean
        plan:
          $ref: "#/components/schemas/Plan"
        request_id:
          type: string
          format: uuid
    Payment:
      type: object
      properties:
        sessionId:
          type: string
        provider:
          type: string
        status:
          type: string
          description: "Lifecycle: created, pending, authorized, captured, refunded; or cancelled, declined, expired."
        orderId:
          type: [string, "null"]
        totalAmount:
          type: integer
          description: Cents.
        currency:
          type: string
        checkoutUrl:
          type: [string, "null"]
        authorizationId:
          type: [string, "null"]
        captureId:
          type: [string, "null"]
        capturedAmount:
          type: [integer, "null"]
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: [string, "null"]
          format: date-time
    Plan:
      type: object
      description: A CoverPay pay-in-4 plan. Only present when `provider` is `coverpay`.
      properties:
        id:
          type: string
        status:
          type: string
          description: A failed payment marks the plan `behind`; it retries after 24 hours, up to 3 times.
        amount:
          type: integer
          description: Cents.
        currency:
          type: string
        order_id:
          type: [string, "null"]
        customer_id:
          type: [string, "null"]
        merchant_owed:
          type: integer
          description: Cents owed to the merchant.
        installments:
          type: array
          items:
            type: object
            properties:
              n:
                type: integer
              due_at:
                type: string
                format: date-time
              amount:
                type: integer
              status:
                type: string
              paid_at:
                type: [string, "null"]
                format: date-time
        payout:
          type: [object, "null"]
          properties:
            status:
              type: string
            net:
              type: integer
            fee:
              type: integer
            transfer_id:
              type: [string, "null"]
            error:
              type: string
    WebhookEvent:
      type: object
      required: [id, type, created, data]
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - session.authorized
            - session.captured
            - session.declined
            - session.cancelled
            - session.expired
            - credit.installment_paid
            - credit.installment_failed
          description: |
            session.captured: the order is paid. session.authorized: funds authorized, captured later.
            credit.installment_paid / credit.installment_failed: CoverPay plan payments 2 to 4.
        created:
          type: string
          format: date-time
        data:
          type: object
          properties:
            payment_id:
              type: string
            order_id:
              type: string
            amount:
              type: integer
            currency:
              type: string
            provider:
              type: string
            session_status:
              type: string
