# CoverPay documentation (full) Base URL https://api.coverpayme.com. Index: https://coverpayme.com/llms.txt --- # CoverPay quickstart CoverPay is one checkout for Klarna, Affirm, Afterpay and Zip, smart-routed for each order, with CoverPay's own pay-in-4 filling in when no provider approves. It works like Plaid Link: your server creates a **link token**, your page or app opens **Link** with it, and your server exchanges the **public token** Link hands back. The amount, order and customer live on the link token, created with your secret key. Nothing about the payment comes from the browser or the app, so it cannot be changed there. ## Flow 1. Server: `POST /v1/link/token/create` with your secret key and the order amount from your own catalog. Returns `link_token`. 2. Client: open Link with only the `link_token` (Web, React or iOS SDK). 3. The shopper picks a plan and approves with the provider. Link calls `onSuccess` with a `public_token`. 4. Server: `POST /v1/link/public_token/exchange` returns the payment. 5. Webhooks are the source of truth for whether an order is paid. Base URL: `https://api.coverpayme.com`. Auth: header `x-api-key: cp_test_…` (sandbox) or `cp_live_…` (production). Money is integer cents. Machine-readable spec: https://coverpayme.com/openapi.yaml ## 1. Get your keys Create a test key (`cp_test_…`) in the dashboard at https://app.coverpayme.com/api-keys. Keep it on your server. Test keys work only in sandbox; live keys only in production. ## 2. Create a link token (server) ```js const res = await fetch('https://api.coverpayme.com/v1/link/token/create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.COVERPAY_SECRET_KEY }, body: JSON.stringify({ amount: 12000, // cents, from YOUR catalog or cart, never from the client currency: 'USD', merchantName: 'My Store', orderId: 'order_123', // comes back on the exchange and in webhooks customerId: 'user_456', customerEmail: 'buyer@example.com', }), }); const { link_token, expiration } = await res.json(); // "lt_test_…" ``` ## 3. Open Link (client) Web (`npm install @coverpay/sdk`): ```js import { CoverPayLink } from '@coverpay/sdk'; const callbacks = { onSuccess: (publicToken, metadata) => { /* POST publicToken to your server */ }, onExit: (error, metadata) => { /* closed (error null) or failed (error.code, error.message) */ }, }; CoverPayLink.handleRedirect(callbacks); // once on every load of the checkout page const link = CoverPayLink.create({ token: linkToken, ...callbacks }); button.addEventListener('click', () => link.open()); ``` React (`@coverpay/react`): ```tsx const { open, ready } = useCoverPayLink({ token: linkToken, onSuccess, onExit }); return ; ``` iOS (Swift, `CoverPaySDK`): ```swift CoverPayLink.open(linkToken: linkToken, from: self) { result in switch result { case .success(let payment): sendToServer(payment.publicToken) case .failure(.userCancelled): break case .failure(let error): show(error) } } ``` ## 4. Exchange the public token (server) ```js const res = await fetch('https://api.coverpayme.com/v1/link/public_token/exchange', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.COVERPAY_SECRET_KEY }, body: JSON.stringify({ public_token: publicToken }), }); const payment = await res.json(); // { payment_id, status: "captured", provider: "zip", amount: 12000, currency: "USD", order_id: "order_123", plan? } ``` | Field | Meaning | |---|---| | `payment_id` | CoverPay payment id; also in your webhooks. | | `status` | `captured` or `authorized`: the shopper's plan went through. | | `order_id` | Your order id from the link token. Check it matches the order you are completing. | | `plan` | Only when `provider` is `coverpay`: the 4-payment schedule and your payout. | Exchanging the same public token again is safe: you get the same payment back and no money moves. Other businesses cannot exchange your tokens (404). Public tokens expire after 30 minutes. ## 5. Listen for webhooks Register an endpoint for `session.captured` and `session.authorized`. When one arrives, read the payment back with your key (`GET /v1/bnpl/sessions/{payment_id}`), check order id and amount, then mark the order paid. See webhooks.md. --- # CoverPay Link SDKs All three SDKs open CoverPay's checkout from a **link token** your server created. The SDK never holds a secret key and never sets the amount. `onSuccess` returns a **public token**; your server exchanges it (`POST /v1/link/public_token/exchange`). Exactly one of `onSuccess` / `onExit` runs per open. Providers (Klarna, Affirm, Afterpay, Zip) and CoverPay's own plan take the shopper's whole page to approve, then send them back to the page that opened Link. Web and React handle that return; call it on every load of the checkout page. ## Web / JavaScript: `@coverpay/sdk` ```js import { CoverPayLink } from '@coverpay/sdk'; const callbacks = { onSuccess: (publicToken, metadata) => exchangeOnYourServer(publicToken), onExit: (error, metadata) => {}, }; CoverPayLink.handleRedirect(callbacks); // once on page load; returns true if it handled a return const link = CoverPayLink.create({ token: linkToken, ...callbacks, onEvent: (name, metadata) => {} }); payLaterButton.addEventListener('click', () => link.open()); ``` | Option | Type | Description | |---|---|---| | `token` | string | Required. `lt_test_…` or `lt_live_…`. Anything else throws immediately. | | `onSuccess` | `(publicToken, metadata) => void` | Required. `publicToken` is `null` only when the provider approved but CoverPay had not confirmed yet: do not charge again, wait for your webhook. | | `onExit` | `(error, metadata) => void` | Shopper closed Link (`error` null) or it failed (`error.code`, `error.message`). | | `onEvent` | `(name, metadata) => void` | Funnel events: `OPEN`, `OFFERS_LOADED`, `OFFER_SELECTED`, `STEP_CHANGE`, `CLOSE`. | Handler methods: `open()`, `exit()` (close as if the shopper did), `destroy()` (remove without callbacks). `onSuccess` metadata: ```js { provider: "zip", // klarna | affirm | afterpay | zip | coverpay sessionId: "pi_3UO3…", // CoverPay payment id (also returned by the exchange) status: "succeeded", // succeeded | requires_capture | processing paymentIntentId: "pi_3UO3…" // Stripe PaymentIntent for Stripe-backed providers, else null } ``` The return result is in the URL fragment (`#coverpay_status=success&coverpay_public_token=public-sandbox-…&coverpay_provider=zip`), so it never reaches server logs. The public token is useless without your secret key. Deprecated: `CoverPay.create({ clientId }).open({ amount })` works only in sandbox (amount from the browser). `checkEligibility` always rejects: call `POST /v1/checkout/verify-eligibility` from your server. ## React: `@coverpay/react` ```tsx import { useCoverPayLink } from '@coverpay/react'; function PayLaterButton({ linkToken }: { linkToken: string | null }) { const { open, ready, error } = useCoverPayLink({ token: linkToken, onSuccess: (publicToken, metadata) => { /* POST to your server */ }, onExit: (error, metadata) => {}, }); return ; } ``` Returns `open()` (call from a click), `ready` (true once a valid token is set), `exit()`, `error` (set if the token is not a CoverPay link token). The hook handles the redirect return on mount, so mount it on the page that opened Link. ## iOS: `CoverPaySDK` (Swift Package Manager, iOS 14+) ```swift import CoverPaySDK CoverPayLink.open(linkToken: linkToken, from: self) { result in switch result { case .success(let payment): // payment.publicToken, .provider, .sessionId, .status api.completeOrder(publicToken: payment.publicToken, orderId: orderId) case .failure(.userCancelled): break case .failure(let error): showError(error.localizedDescription) } } ``` SwiftUI: `.coverPayLink(isPresented: $showCheckout, linkToken: linkToken) { result in … }`. `publicToken` is nil only if the provider approved but CoverPay had not confirmed yet: rely on your webhook. The completion runs exactly once; swiping the sheet down counts as a cancel. `checkEligibility` on iOS fails immediately: call `POST /v1/checkout/verify-eligibility` from your server. `open(params:)` (amount from the app) is deprecated and sandbox-only. ## Server: exchange ``` POST https://api.coverpayme.com/v1/link/public_token/exchange x-api-key: cp_test_… { "public_token": "public-sandbox-…" } ``` See quickstart.md and openapi.yaml. --- # CoverPay webhooks Webhooks are the source of truth for whether an order is paid. Link's `onSuccess` and the public token exchange are fast paths; a webhook still arrives if the shopper closed their browser or your server missed the answer. Register endpoints in the dashboard (https://app.coverpayme.com, Webhooks). ## Events | Event | When | |---|---| | `session.authorized` | The shopper approved a plan; funds are authorized (captured later). | | `session.captured` | The order is paid. Mark it paid (once per `payment_id`). | | `session.declined` | The provider declined. | | `session.cancelled` | The payment was cancelled. | | `session.expired` | The checkout expired unpaid. | | `credit.installment_paid` | CoverPay plan: a later payment (2 to 4) was collected. | | `credit.installment_failed` | CoverPay plan: a payment failed; it retries after 24 hours. | ## Payload ```json { "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" } } ``` Treat the event as a hint. Before marking an order paid, read the payment back with your key: `GET /v1/bnpl/sessions/{payment_id}`. Check the order id and amount match your order. ## Verify the signature Every delivery has `X-CoverPay-Signature-V2: t=,v1=`, an HMAC-SHA256 of `"."` with your endpoint's signing secret. Verify over the raw body before parsing, and reject timestamps more than 5 minutes old. The older `X-CoverPay-Signature` (HMAC of the body without a timestamp) is still sent; prefer V2. ```js import crypto from 'crypto'; function verifyCoverPay(rawBody, header, secret) { if (!header) return false; const parts = header.split(',').map((p) => p.trim().split('=')); const t = Number(parts.find(([k]) => k === 't')?.[1]); const v1s = parts.filter(([k]) => k === 'v1').map(([, v]) => v); if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return v1s.some((v) => v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected))); } ``` ## Delivery and retries Answer 2xx once you have recorded the event. Any other answer, or no answer within the 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. ## Checklist - Verify `X-CoverPay-Signature-V2` before parsing; return 401 if it fails. - Read the payment back with your key; check order id and amount. - Record once per `payment_id` (the same order can also arrive through the public token exchange). - Return 2xx quickly; do slow work afterwards. --- # CoverPay errors Errors are JSON: `{ "error": "human message", "code": "MACHINE_CODE" }`. Validation failures add `details`. | HTTP | `code` | Meaning | What to do | |---|---|---|---| | 400 | `INVALID_LINK_TOKEN` | Link token missing or malformed. | Create a new one on your server. | | 400 | `INVALID_PUBLIC_TOKEN` | `public_token` missing or does not start with `public-`. | Send the token Link returned. | | 400 | (no code) `Invalid parameters` | Request body failed validation; see `details`. | Fix the fields listed. | | 401 | `INVALID_LINK_TOKEN` | Link token not valid for this use. | Create a new one. | | 401 | (no code) | Missing or invalid API key. | Send `x-api-key` with a key that matches the environment. | | 404 | `INVALID_PUBLIC_TOKEN` | Not found, not yours, or the payment is not completed. | Do not retry; the same answer is given for other businesses' tokens. | | 410 | `EXPIRED_LINK_TOKEN` | Link token expired (30 minutes for checkout). | Create a new one. | | 410 | `USED_LINK_TOKEN` | Link token already used or finished. | Create a new one for a new attempt. | | 410 | `EXPIRED_PUBLIC_TOKEN` | Public token older than 30 minutes. | Read the payment from your webhook or `GET /v1/bnpl/sessions/{payment_id}` instead. | Link `onExit(error)` codes include `PAYMENT_FAILED` when the shopper's approval fails at the provider. --- # CoverPay sandbox testing Test keys (`cp_test_…`) create sandbox link tokens (`lt_test_…`). Payments go through Stripe's test mode: no real money moves. ## Provider payments Pick Klarna, Affirm, Afterpay or Zip in Link. You land on the provider's test page. | Provider | Test page | To approve | To fail | |---|---|---|---| | Affirm, Afterpay, Zip | Stripe's test payment page | Authorize test payment | Fail test payment | | Klarna | Klarna's playground | Follow the test login | Cancel | Approving returns `onSuccess` with a public token; failing returns `onExit` with `PAYMENT_FAILED`. Each provider shows only for amounts in its range. ## CoverPay's own plan CoverPay fills in when no provider approves: pay in 4, every 2 weeks, 0% APR, up to $500 per shopper (minus what they still owe), from $10. To see it, make every provider decline with a sandbox-only flag on the link token: ```json POST /v1/link/token/create { "amount": 12000, "currency": "USD", "merchantName": "My Store", "orderId": "order_1", "customerId": "user_1", "metadata": { "coverpay_test": "providers_decline" } } ``` Pick CoverPay, then pay the first payment on Stripe's page with a test card: | Card | What happens | |---|---| | `4242 4242 4242 4242` | Succeeds now and later. | | `4000 0000 0000 0341` | Saves, then every later charge fails. | Any future expiry, any CVC, ZIP 94103. ## Collecting the rest without waiting Payments 2 to 4 are charged automatically when due. In sandbox: ``` POST /v1/credit/loans/{payment_id}/make-due # next payment due now; the collector charges it POST /v1/credit/loans/{payment_id}/collect-next # or charge it immediately GET /v1/credit/loans/{payment_id} # plan, schedule and your payout ``` A failed payment marks the plan `behind` and is retried after 24 hours, up to 3 times. ## Your payout When payment 1 succeeds, CoverPay pays you the plan amount minus your CoverPay fee (0% in sandbox) through Stripe Connect. Set up payouts once with `POST /v1/payouts/onboarding` (Stripe-hosted, test values accepted) and check them with `GET /v1/payouts`. ## Things that should be refused | Try | Expect | |---|---| | Change the amount in the browser before paying | The payment is for the link token amount anyway | | Open Link with an API key instead of a link token | Throws before opening | | Exchange a public token with another business's key | 404 | | Reuse a finished link token | 410 `USED_LINK_TOKEN` | | Send a webhook without a valid signature | 401 | Use test cards and Stripe's test identity values only. Never real personal or bank details. --- # OpenAPI spec (openapi.yaml) ```yaml 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=,v1=`, where `v1` is HMAC-SHA256 of `"."` 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 ```