API reference
Base URL is your deployment. Authenticate with Authorization: Bearer <api key>.
The key can create payments, so it belongs on your server only.
Create a payment
POST /api/v1/payments
| Field | Type | |
|---|---|---|
amount | string | Decimal USDC, e.g. "25.00". Optional when line_items is given. |
line_items | array | Itemised breakdown. Derives amount when that is omitted. |
description | string | Shown at checkout. Max 500 characters. |
reference | string | Your own order id. Shown at checkout. |
success_url | string | Where the payer returns after paying. Must be https. |
customer_email | string | Shown at checkout. Never required. |
customer_name | string | Shown at checkout. |
invoice_number | string | Turns the payment into an invoice. |
due_at | string | ISO 8601. Extends the payment window to this date. |
metadata | object | Returned untouched in webhooks and on retrieval. |
Send Idempotency-Key to make retries safe — the same key returns the original payment rather than creating a second one.
Line items
{
"line_items": [
{ "description": "Pro plan (30 days)", "unit_amount": "20.00", "quantity": 1 },
{ "description": "Extra seat", "unit_amount": "5.00", "quantity": 2 }
]
}
The amount is derived — 30.00 here — so an itemisation can never disagree with the charge. You may also send amount as well, and a mismatch is rejected with both figures, which catches an integration bug before a customer sees it.
At most 50 items. Quantities are whole numbers.
Response
{
"id": "0x7f3a...",
"status": "pending",
"amount": "25.00",
"amount_received": null,
"currency": "USDC",
"pay_url": "https://your-deployment/pay/0x7f3a...",
"deposit_address": "0xe374...",
"fee_bps": 50,
"expires_at": "2026-07-20T12:00:00.000Z",
"created_at": "2026-07-20T11:00:00.000Z"
}
id is a 256-bit random value. It is safe to show a payer — it grants nothing except the ability to see and pay that one payment — but it must never be guessable, which is why it is not sequential.
Retrieve a payment
GET /api/v1/payments/{id}
Use this to reconcile when a webhook is late or was missed. Scoped to your account: another merchant's payment is a 404, not a disclosure.
Statuses
| Status | Meaning |
|---|---|
pending | Created, nothing received |
settling | Funds seen, settlement in flight |
confirmed | Paid in full and settled |
underpaid | Something arrived, less than invoiced |
overpaid | More than invoiced arrived |
expired | Window closed with nothing received |
failed | Settlement reverted; funds are recoverable |
Only confirmed means you have been paid. Treat underpaid as unpaid until the remainder arrives. expired is not final — a late payment to a derived address still settles.
Webhook endpoints
POST /api/v1/webhook_endpoints with { "url": "https://..." } returns a signing secret, shown once.
GET /api/v1/webhook_endpoints lists them. DELETE /api/v1/webhook_endpoints/{id} removes one.
URLs must be https and publicly resolvable. Private, loopback and link-local addresses are rejected — see security.
Public endpoints
No authentication; the payment id is the capability.
GET /api/public/payments/{id} | What the checkout renders from |
|---|---|
GET /api/public/payments/{id}/qr | Deposit address as SVG |
POST /api/public/payments/{id}/sync | Confirm from a transaction hash |
GET /api/public/payments/{id}/cross-chain | List source chains, or quote one with ?chain= |
POST /api/public/payments/{id}/cross-chain | Record a burn so it gets redeemed on Arc |
These send CORS headers so the widget can call them from your domain.
Errors
Standard codes with a JSON body:
{ "error": "line_items sum to 10.00 USDC but amount is 40.00 USDC" }
| 400 | Validation failed; the message says what |
|---|---|
| 401 | Missing, unknown or revoked API key |
| 404 | Not found, or not yours |