API Reference
Complete reference for the CoverPay REST API. All endpoints accept and return JSON. Every response includes a request_id for tracing and support.
Base URL
https://api.coverpayme.comOne host for both environments. Your key picks the environment: a cp_test_ key works in sandbox, a cp_live_ key in production. Sandbox and production data never mix.
Authentication
Authenticate every request with your secret key in the x-api-key header. Keep it on your server; it never goes in a browser or an app. API keys are prefixed by environment:
cp_test_...for sandboxcp_live_...for production
All requests must include Content-Type: application/json.
curl https://api.coverpayme.com/v1/bnpl/offers \
-H "x-api-key: cp_live_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{"amount": 15000, "currency": "USD"}'Link tokensLive
Link is how a checkout starts and ends, the way Plaid Link works. Your server creates a link_token with your secret key, your page or app opens Link with it, and Link's onSuccess gives you a public_token that your server exchanges for the payment. The amount, order and customer live on the link token, so nothing about the payment comes from the browser. Your webhook stays the source of truth for whether it's paid.
POST /v1/link/token/create
Your server, with your secret key. Returns a short-lived, single-use link token.
| Field | Type | Description |
|---|---|---|
amount | integer, required | In cents, from your own catalog or cart. Never from the client. |
currency | string | Default USD. |
orderId | string | Your order id. Comes back on the exchange and in webhooks. |
customerId | string | Your id for the shopper. |
customerEmail | string | Optional; prefills the provider’s form. |
merchantName | string | Shown in Link. |
providers | string[] | Limit routing to these providers (for example ["zip", "affirm"]). Default: every provider you’ve enabled. |
mode | "choice" | "auto" | "choice" (default) shows the routed offers; "auto" picks the best one. |
purpose | "checkout" | "update" | "eligibility" | "terms" | Default "checkout". Sets the lifetime: 30 minutes for checkout and update, 4 hours for eligibility and terms. |
expiresInSeconds | integer | Overrides the lifetime, 60 to 86,400. |
allowedDomains | string[] | Pages allowed to open this token. |
metadata | object | Yours; returned with the payment. In sandbox, coverpay_test sets a test scenario (see Sandbox). |
curl https://api.coverpayme.com/v1/link/token/create \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{"amount": 12000, "orderId": "order_123", "customerId": "cust_42"}'
# 200
# { "link_token": "lt_test_…", "expiration": "2026-10-09T18:30:00.000Z", "request_id": "…" }GET /v1/link/token/result?link_token=…
Link (the SDK) calls this to learn how its checkout ended; you don't need to. Holding the link token is what authorizes it. It returns { status, session_id, public_token? }; the public_token appears once the provider has confirmed the payment, and only until it's exchanged.
POST /v1/link/public_token/exchange
Your server, with your secret key: trade the public_token from onSuccess for the payment. Don't trust a payment id that came from the client; trust this.
curl https://api.coverpayme.com/v1/link/public_token/exchange \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{"public_token": "public-sandbox-…"}'
# 200
# { "payment_id": "…", "status": "captured", "provider": "zip", "amount": 12000, "currency": "USD",
# "order_id": "order_123", "customer_id": "cust_42", "completed_at": "…",
# "already_exchanged": false, "plan": { … }, "request_id": "…" }| Response field | Description |
|---|---|
payment_id | The payment. Use it to match webhooks and to look the payment up later. |
status | The payment’s status when you asked. The webhook is the source of truth for “paid”. |
plan | Only when CoverPay’s own pay-in-4 was used: the plan and its schedule. |
already_exchanged | true when you’ve exchanged this public token before (see below). |
already_exchanged: true. No money moves on an exchange, so a repeat is harmless. Another business can never exchange your token.| Error | HTTP | When |
|---|---|---|
INVALID_PUBLIC_TOKEN | 400 | public_token is missing or isn’t one. |
INVALID_PUBLIC_TOKEN | 404 | Not found, not yours, or the checkout hasn’t completed. |
EXPIRED_PUBLIC_TOKEN | 410 | More than 30 minutes old. Use the webhook or GET the payment instead. |
INVALID_LINK_TOKEN | 401 | Opening Link with a link token that doesn’t exist. |
EXPIRED_LINK_TOKEN | 410 | Opening Link after the link token expired. Create a new one. |
BNPL Offers
Query available BNPL offers for a given purchase amount. The smart router ranks providers by availability, APR, and approval likelihood.
/v1/bnpl/offersReturns ranked BNPL offers from all available providers for the given amount.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| amount | integer | Yes | Purchase amount in cents (e.g. 15000 = $150.00) |
| currency | string | Yes | ISO 4217 currency code (currently only USD) |
| customerEmail | string | No | Pre-fill customer email for faster checkout |
Example request
curl -X POST https://api.coverpayme.com/v1/bnpl/offers \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "USD",
"customerEmail": "jane@example.com"
}'Example response
{
"request_id": "req_8f3a1b2c4d5e",
"offers": [
{
"provider": "klarna",
"plan_type": "pay_in_4",
"installments": 4,
"per_installment": 3750,
"total": 15000,
"apr": 0,
"currency": "USD",
"min_amount": 3500,
"max_amount": 150000,
"score": 92.4
},
{
"provider": "affirm",
"plan_type": "monthly",
"installments": 12,
"per_installment": 1336,
"total": 16032,
"apr": 15.0,
"currency": "USD",
"min_amount": 5000,
"max_amount": 1750000,
"score": 87.1
},
{
"provider": "afterpay",
"plan_type": "pay_in_4",
"installments": 4,
"per_installment": 3750,
"total": 15000,
"apr": 0,
"currency": "USD",
"min_amount": 100,
"max_amount": 200000,
"score": 84.6
},
{
"provider": "coverpay",
"plan_type": "pay_in_4",
"installments": 4,
"per_installment": 3750,
"total": 15000,
"apr": 0,
"currency": "USD",
"min_amount": 1000,
"max_amount": 50000,
"score": 80.2
}
]
}Checkout Sessions
Create a checkout session to redirect the customer to a specific provider's payment flow.
/v1/bnpl/sessionsCreates a checkout session for the selected provider. Returns a redirect URL the customer should be sent to.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | Provider slug: klarna, affirm, afterpay, paypal, sezzle, zip, coverpay |
| order | object | Yes | Order details (required minimal schema shown below; optional enrichment fields supported) |
| order.orderId | string | Yes | Your internal order ID |
| order.totalAmount | integer | Yes | Amount in cents |
| order.currency | string | No | ISO 4217 currency code (defaults to USD if omitted) |
| order.lineItems | array | No | Optional item-level detail for provider risk and reconciliation context |
| successUrl | string | Yes | URL to redirect after successful payment |
| cancelUrl | string | Yes | URL to redirect if customer cancels |
Example request
curl -X POST https://api.coverpayme.com/v1/bnpl/sessions \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"provider": "klarna",
"order": {
"orderId": "order_9f8e7d6c",
"totalAmount": 15000
},
"successUrl": "https://mystore.com/checkout/success",
"cancelUrl": "https://mystore.com/checkout/cancel"
}'Example response
{
"request_id": "req_2c4d6e8f0a1b",
"sessionId": "sess_kl_7a8b9c0d1e2f",
"provider": "klarna",
"redirectUrl": "https://pay.klarna.com/checkout/7a8b9c0d1e2f",
"expiresAt": "2026-01-30T12:30:00Z"
}redirectUrl is invalid and you must create a new session. Expired sessions may emit checkout.abandoned with reason session_expired./v1/checkout/confirmConfirms a client-side payment token server-side before order fulfillment.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| paymentToken | string | Yes | One-time token returned from SDK checkout success |
| orderId | string | No | Merchant order identifier for reconciliation |
Example request
curl -X POST https://api.coverpayme.com/v1/checkout/confirm \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"paymentToken": "cp_tok_kl_7a8b9c0d1e2f",
"orderId": "order_9f8e7d6c"
}'Example response
{
"request_id": "req_abc123",
"confirmed": true,
"paymentToken": "cp_tok_kl_7a8b9c0d1e2f",
"orderId": "order_9f8e7d6c",
"provider": "klarna",
"plan": {
"type": "pay_in_4",
"installments": 4,
"installmentAmount": 2500,
"totalAmount": 10000,
"apr": 0
},
"confirmedAt": "2026-01-29T14:35:00Z"
}Eligibility
Verify that a customer is eligible for BNPL before showing checkout options. The eligibility pipeline checks age, residency, KYC status, sanctions, and terms acceptance.
/v1/checkout/verify-eligibilityRuns the full eligibility pipeline and returns pass/fail for each check.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | CoverPay user ID |
| amount | integer | Yes | Requested amount in cents |
Example request
curl -X POST https://api.coverpayme.com/v1/checkout/verify-eligibility \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"userId": "usr_9a8b7c6d5e4f",
"amount": 15000
}'Example response
{
"request_id": "req_3d5f7a9b1c3e",
"eligible": true,
"failureReasons": [],
"checks": {
"age": { "passed": true },
"residency": { "passed": true },
"kyc": { "passed": true, "status": "approved" },
"sanctions": { "passed": true },
"terms": { "passed": true, "version": "2026-01-15" }
}
}Failure reasons
When eligible is false, the failureReasons array contains one or more of: UNDER_18, DATE_OF_BIRTH_MISSING, NON_US_RESIDENT, KYC_NOT_STARTED, KYC_PENDING, KYC_REJECTED, KYC_EXPIRED, SANCTIONS_FLAGGED, TERMS_NOT_ACCEPTED, USER_NOT_FOUND.
/v1/checkout/accept-termsRecords the customer's acceptance of CoverPay legal documents. Must be accepted before a checkout session can be created.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | CoverPay user ID |
| documents | string[] | Yes | Array of accepted document types: user_agreement, privacy_policy, regulatory_disclosures, third_party_disclosures |
Example request
curl -X POST https://api.coverpayme.com/v1/checkout/accept-terms \
-H "x-api-key: cp_test_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"userId": "usr_9a8b7c6d5e4f",
"documents": [
"user_agreement",
"privacy_policy",
"regulatory_disclosures",
"third_party_disclosures"
]
}'Example response
{
"request_id": "req_4e6g8i0k2m4o",
"accepted": true,
"version": "2026-01-15",
"acceptedAt": "2026-01-29T14:22:31Z"
}/v1/checkout/terms-status/:userIdReturns the current terms acceptance status for a user.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | CoverPay user ID |
Example request
curl https://api.coverpayme.com/v1/checkout/terms-status/usr_9a8b7c6d5e4f \
-H "x-api-key: cp_test_a1b2c3d4..."Example response
{
"request_id": "req_5f7h9j1l3n5p",
"accepted": true,
"version": "2026-01-15",
"documents": [
{ "type": "user_agreement", "acceptedAt": "2026-01-29T14:22:31Z" },
{ "type": "privacy_policy", "acceptedAt": "2026-01-29T14:22:31Z" },
{ "type": "regulatory_disclosures", "acceptedAt": "2026-01-29T14:22:31Z" },
{ "type": "third_party_disclosures", "acceptedAt": "2026-01-29T14:22:31Z" }
]
}Business
Manage your business account, settings, and API keys.
/v1/business/registerRegister a new business account. Returns an access token and the created business object.
Access tokens are short-lived session credentials for dashboard flows. For long-lived server integrations, create and use static API keys.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Business email address | |
| password | string | Yes | Minimum 8 characters |
| businessName | string | Yes | Legal business name |
| website | string | No | Business website URL |
Example request
curl -X POST https://api.coverpayme.com/v1/business/register \
-H "Content-Type: application/json" \
-d '{
"email": "dev@mystore.com",
"password": "secureP@ss123",
"businessName": "My Store Inc.",
"website": "https://mystore.com"
}'Example response
{
"request_id": "req_6a7b8c9d0e1f",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 86400,
"business": {
"id": "biz_3e4f5a6b7c8d",
"name": "My Store Inc.",
"email": "dev@mystore.com",
"website": "https://mystore.com",
"tier": "starter",
"createdAt": "2026-01-29T15:00:00Z"
}
}/v1/business/loginAuthenticate and receive an access token.
Access tokens expire after 24 hours. Use API keys for automated server-to-server calls.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Business email address | |
| password | string | Yes | Account password |
Example response
{
"request_id": "req_7b8c9d0e1f2a",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 86400
}/v1/business/meReturns the currently authenticated user and their associated business.
Example response
{
"request_id": "req_8c9d0e1f2a3b",
"user": {
"id": "usr_1a2b3c4d5e6f",
"email": "dev@mystore.com",
"role": "owner"
},
"business": {
"id": "biz_3e4f5a6b7c8d",
"name": "My Store Inc.",
"website": "https://mystore.com",
"tier": "growth",
"providers": ["klarna", "affirm", "afterpay"],
"createdAt": "2026-01-15T10:00:00Z"
}
}/v1/business/settingsUpdate business settings including enabled providers, branding, and routing preferences.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| businessName | string | No | Updated business name |
| website | string | No | Business website URL |
| providers | string[] | No | Enabled provider slugs |
| brandColor | string | No | Hex color for checkout branding |
Example response
{
"request_id": "req_9d0e1f2a3b4c",
"business": {
"id": "biz_3e4f5a6b7c8d",
"name": "My Store Inc.",
"website": "https://mystore.com",
"providers": ["klarna", "affirm", "afterpay", "coverpay"],
"brandColor": "#f59e0b",
"updatedAt": "2026-01-29T16:15:00Z"
}
}/v1/business/api-keysList all API keys for the authenticated business. Secret keys are masked after creation.
Example response
{
"request_id": "req_0e1f2a3b4c5d",
"keys": [
{
"id": "key_1a2b3c4d",
"name": "Production Key",
"prefix": "cp_live_a1b2...",
"environment": "production",
"createdAt": "2026-01-20T10:00:00Z",
"lastUsedAt": "2026-01-29T14:30:00Z"
},
{
"id": "key_5e6f7a8b",
"name": "Test Key",
"prefix": "cp_test_x9y8...",
"environment": "sandbox",
"createdAt": "2026-01-15T09:00:00Z",
"lastUsedAt": "2026-01-29T12:00:00Z"
}
]
}/v1/business/api-keysCreate a new API key. The full secret is returned only once in the response.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | A label for this key |
| environment | string | Yes | sandbox or production |
Example response
{
"request_id": "req_1f2a3b4c5d6e",
"key": {
"id": "key_9c0d1e2f",
"name": "Mobile App Key",
"secret": "cp_live_sk_7g8h9i0j1k2l3m4n5o6p7q8r9s0t",
"environment": "production",
"createdAt": "2026-01-29T16:30:00Z"
}
}Important: The secret field is only returned at creation time. Store it securely -- you will not be able to retrieve it again.
/v1/business/api-keys/:keyIdPermanently revoke an API key. This action cannot be undone. Any requests using the revoked key will receive a 401 response.
Example response
{
"request_id": "req_2a3b4c5d6e7f",
"deleted": true,
"keyId": "key_1a2b3c4d"
}Webhooks
Manage webhook endpoints to receive real-time event notifications. See the Webhooks Guide for payload schemas, signature verification, and best practices.
/v1/business/webhooksList all webhook endpoints configured for your business.
Example response
{
"request_id": "req_3b4c5d6e7f8a",
"endpoints": [
{
"id": "wh_1a2b3c4d",
"url": "https://mystore.com/webhooks/coverpay",
"events": ["checkout.completed", "payment.success", "payment.failed"],
"enabled": true,
"signingSecret": "whsec_a1b2...d4e5",
"createdAt": "2026-01-20T10:00:00Z"
}
]
}/v1/business/webhooksCreate a new webhook endpoint. The URL must use HTTPS.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | HTTPS endpoint URL |
| events | string[] | Yes | Event types to subscribe to |
Example response
{
"request_id": "req_4c5d6e7f8a9b",
"endpoint": {
"id": "wh_5e6f7a8b",
"url": "https://mystore.com/webhooks/coverpay",
"events": ["checkout.completed", "payment.success"],
"enabled": true,
"signingSecret": "whsec_9x8w7v6u5t4s3r2q1p0o",
"createdAt": "2026-01-29T17:00:00Z"
}
}/v1/business/webhooks/:idUpdate an existing webhook endpoint. You can change the URL, subscribed events, or enabled state.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | No | Updated HTTPS endpoint URL |
| events | string[] | No | Updated event types |
| enabled | boolean | No | Enable or disable the endpoint |
Example response
{
"request_id": "req_5d6e7f8a9b0c",
"endpoint": {
"id": "wh_5e6f7a8b",
"url": "https://mystore.com/webhooks/v2/coverpay",
"events": ["checkout.completed", "payment.success", "refund.completed"],
"enabled": true,
"updatedAt": "2026-01-29T17:30:00Z"
}
}/v1/business/webhooks/:idPermanently delete a webhook endpoint. Pending deliveries will be cancelled.
Example response
{
"request_id": "req_6e7f8a9b0c1d",
"deleted": true,
"endpointId": "wh_5e6f7a8b"
}/v1/business/webhooks/:id/testSend a test event to the webhook endpoint to verify it is reachable and correctly processing payloads.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| eventType | string | No | Event type to simulate (defaults to checkout.completed) |
Example response
{
"request_id": "req_7f8a9b0c1d2e",
"delivered": true,
"statusCode": 200,
"responseTimeMs": 142,
"eventType": "checkout.completed"
}/v1/business/webhooks/:id/deliveriesList recent delivery attempts for a webhook endpoint, including status codes and response times.
Example response
{
"request_id": "req_8a9b0c1d2e3f",
"deliveries": [
{
"id": "dlv_a1b2c3d4",
"eventType": "payment.success",
"statusCode": 200,
"responseTimeMs": 89,
"success": true,
"attempt": 1,
"deliveredAt": "2026-01-29T16:45:12Z"
},
{
"id": "dlv_e5f6a7b8",
"eventType": "checkout.completed",
"statusCode": 500,
"responseTimeMs": 2340,
"success": false,
"attempt": 3,
"deliveredAt": "2026-01-29T15:30:00Z",
"nextRetryAt": "2026-01-29T17:30:00Z"
}
]
}/v1/business/webhooks/eventsList all available webhook event types you can subscribe to.
Example response
{
"request_id": "req_9b0c1d2e3f4a",
"events": [
{ "type": "checkout.completed", "description": "Customer completed checkout" },
{ "type": "checkout.abandoned", "description": "Customer abandoned checkout session" },
{ "type": "payment.success", "description": "Payment was successfully processed" },
{ "type": "payment.failed", "description": "Payment processing failed" },
{ "type": "loan.created", "description": "BNPL loan was originated" },
{ "type": "loan.completed", "description": "All installments paid, loan closed" },
{ "type": "eligibility.failed", "description": "Customer failed eligibility check" },
{ "type": "refund.initiated", "description": "Refund was initiated" },
{ "type": "refund.completed", "description": "Refund was successfully processed" }
]
}Errors
CoverPay uses conventional HTTP status codes. All error responses include a machine-readableerror code and a human-readable message.
Error response format
{
"request_id": "req_0c1d2e3f4a5b",
"error": "invalid_amount",
"message": "Amount must be a positive integer in cents.",
"retryAfter": null
}retryAfter is integer | null. It is populated on 429 rate_limited responses (seconds to wait) and otherwise null.
Error codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | invalid_request | The request body is malformed or missing required fields. |
| 400 | invalid_amount | Amount must be a positive integer in cents. |
| 400 | invalid_provider | The specified provider is not supported or not enabled. |
| 401 | unauthorized | Missing or invalid API key. |
| 401 | token_expired | The access token has expired. Re-authenticate. |
| 403 | forbidden | The API key does not have permission for this action. |
| 404 | not_found | The requested resource does not exist. |
| 409 | duplicate | A resource with this identifier already exists. |
| 409 | token_already_confirmed | The payment token has already been confirmed. |
| 422 | eligibility_failed | The customer did not pass eligibility checks. |
| 400 | invalid_token | Payment token is invalid, expired, or already used. |
| 429 | rate_limited | Too many requests. Check the Retry-After header. |
| 500 | internal_error | An unexpected error occurred on our end. |
| 502 | provider_error | The upstream BNPL provider returned an error. |
| 503 | service_unavailable | CoverPay is temporarily unavailable. Try again shortly. |
Rate Limits
Rate limits are applied per API key and vary by subscription tier. When rate limited, the response includes a Retry-After header with the number of seconds to wait.
| Tier | Requests / minute | Requests / day | Burst |
|---|---|---|---|
| Starter | 60 | 10,000 | 20 |
| Growth | 200 | 50,000 | 50 |
| Scale | 600 | 200,000 | 100 |
| Enterprise | Custom | Custom | Custom |
Rate limit headers are included in every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix timestamp, UTC, seconds since epoch).
Enterprise tier limits are custom and shown per account in Dashboard settings.
