API reference
The Subsie API is a predictable REST API: resource-oriented URLs under /api/v1, JSON requests and responses, Bearer authentication, and conventional HTTP status codes. Your API key's mode (test or live) scopes every request.
Basics
Base URL and authentication
All endpoints live under /api/v1 on the host where your Subsie instance runs. Authenticate with a secret key in the Authorization header:
Authorization: Bearer orven_sk_test_... # or orven_sk_live_...Test keys (orven_sk_test_) operate on Base Sepolia and see only test-mode data; live keys (orven_sk_live_) operate on Base mainnet and see only live data. Keys can be revoked at any time on the Developers page.
Errors
Errors return conventional status codes and a consistent envelope. details appears on validation failures with per-field messages; type is a deprecated alias of code.
{
"error": {
"code": "invalid_request_error",
"type": "invalid_request_error",
"message": "amount is required for payment mode",
"details": [{ "field": "amount", "message": "Required" }]
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request_error | The request body or query failed validation. |
| 401 | authentication_error | Missing, invalid, or revoked API key. |
| 404 | not_found_error | No such resource for this merchant and mode. |
| 409 | conflict_error | State conflict, e.g. a concurrent request in flight. |
| 409 | idempotency_error | Idempotency key reused with a different body. |
| 429 | rate_limit_error | Too many requests; honor the Retry-After header. |
| 500 | api_error | Something failed on the Subsie side. |
Idempotency
Creation endpoints accept an Idempotency-Key header (up to 200 characters). Replaying the same key with the same body within 24 hours returns the original response with an Idempotency-Replayed: true header; the same key with a different body returns 409. Keys are scoped to your merchant account, endpoint, and key mode — failed requests release the key for retry.
Pagination
List endpoints return newest-first and share one scheme: limit (1–100, default 20) and starting_after (the id of the last item from the previous page).
{
"object": "list",
"data": [ ... ],
"has_more": true
}Rate limits and amounts
Requests are limited per key (120 per minute by default); a 429 response includes a Retry-After header in seconds. Monetary amounts are decimal strings in human USDC units with at most six decimal places — "25", "0.5", "9.99" — never floats.
Checkout sessions
A checkout session is one intent to charge: the terms, a hosted payment URL, an expiry, and a snapshot of your fee terms taken at creation. Sessions are open until they are paid (completed) or time out (expired).
{
"id": "cs_Wn4hFY2pTrXammxQkCEnHJqz",
"object": "checkout.session",
"livemode": false,
"url": "https://pay.example.com/c/cs_Wn4hFY2pTrXammxQkCEnHJqz",
"mode": "payment",
"status": "open",
"title": "Pro plan — June",
"description": null,
"amount": "25",
"currency": "USDC",
"fee_bps": 200,
"fee_amount": "0.5",
"merchant_net_amount": "24.5",
"fee_tier": "free",
"chain_id": 84532,
"token_address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"interval_seconds": null,
"chain_plan_id": null,
"recipient_address": "0xE4b5E865CA9F9910C666014C02550F17Ce690149",
"customer": null,
"subscription": null,
"customer_reference": "user_8231",
"success_url": "https://example.com/billing/success",
"cancel_url": null,
"wallet_address": null,
"tx_hash": null,
"metadata": {},
"expires_at": "2026-06-13T10:00:00.000Z",
"completed_at": null,
"created_at": "2026-06-12T10:00:00.000Z"
}amount is gross: the buyer pays it in full, fee_amount goes to Subsie, and merchant_net_amount reaches your wallet — the on-chain split is enforced to match this snapshot exactly. url is null once the session is no longer open. After completion, wallet_address and tx_hash record who paid and in which transaction.
Create a checkout session
modestringrequired"payment" for a one-time charge, "subscription" to start recurring billing against an on-chain plan (live mode only).
titlestringrequiredWhat the buyer is paying for, shown on the hosted page. 1–120 characters.
amountstringrequiredGross USDC amount as a decimal string with up to 6 decimals, e.g. "25" or "9.99". Required for payment mode; for subscription mode it is an optional cross-check against the plan.
descriptionstringOptional detail line, up to 500 characters.
recipientstringVerified settlement wallet address to receive funds. Optional when you have exactly one verified wallet; required when you have several.
customer_referencestringYour internal id for the buyer (user id, order id). Returned on the session, the payment, and webhooks. Up to 250 characters.
metadataobjectArbitrary key–value data stored on the session and echoed back verbatim.
success_urlstringWhere the buyer is sent after paying. Must be a valid URL, up to 2000 characters.
cancel_urlstringWhere the buyer is sent if they back out.
expires_in_secondsnumberSession lifetime: 600 (10 minutes) to 604800 (7 days). Default 86400 (24 hours).
chainnumber | stringOptional cross-check: 8453/"base" or 84532/"base-sepolia". Must match the chain implied by your key mode; mismatches return 400.
chain_plan_idstring | numberSubscription mode only: the on-chain billing plan id the buyer subscribes to. Required for subscription mode.
interval_secondsnumberSubscription mode only: optional cross-check against the plan interval.
currencystring"USDC" (default) or "USDT" for payment mode; subscriptions settle in USDC only.
Returns 201 with the session. Supports Idempotency-Key.
Retrieve a checkout session
Returns the session, or 404 if it does not exist for your merchant and mode. Expiry is applied lazily: reading a session past its expires_at flips it to expired.
List checkout sessions
limitnumber1–100, default 20.
starting_afterstringSession id cursor for the next page.
statusstring"open", "completed", or "expired".
customer_referencestringExact-match filter on your internal reference.
Payments
A payment is a settled on-chain charge — created when a checkout session completes or a subscription is charged. Payments are read-only through the API; they are produced by verified on-chain settlement.
{
"id": "pay_e8aiLUZADe3yCMh5RsAecHV3",
"object": "payment",
"livemode": true,
"status": "confirmed",
"charge_type": "one_time",
"subscription": null,
"customer": "cus_Jx2mPq8GfTuWbnZmVRkdQy4N",
"checkout_session": "cs_uFSRYGdz7HnFzYUXHzb7z606",
"amount": "0.1",
"fee_amount": "0.002",
"merchant_amount": "0.098",
"refunded_amount": "0",
"wallet_address": "0xE4b5E865CA9F9910C666014C02550F17Ce690149",
"chain_id": 8453,
"token_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tx_hash": "0xb86fb1f721260e6fdceebe533ed5c4f1847c4a4551ba6285f474b48762f69714",
"block_number": "47237447",
"created_at": "2026-06-11T18:42:10.000Z"
}Retrieve a payment
List payments
limitnumber1–100, default 20.
starting_afterstringPayment id cursor for the next page.
subscriptionstringFilter to one subscription's charges.
checkout_sessionstringFilter to the payment of one checkout session.
statusstring"pending", "confirmed", "failed", or "refunded".
Refunds
Refunds are non-custodial: Subsie never moves money. You send the stablecoin back to the original payer from one of your verified settlement wallets, then report the transaction — Subsie verifies it on-chain and records the refund. Refunds can be partial and repeated up to the gross paid. Each recorded refund — partial or full — fires a payment.refunded webhook, and the payment flips to refunded once fully refunded.
{
"id": "re_3pQ2mZ7wKdXn8aLbVtY1cR4s",
"object": "refund",
"livemode": true,
"status": "succeeded",
"payment": "pay_e8aiLUZADe3yCMh5RsAecHV3",
"amount": "0.1",
"currency": "USDC",
"chain_id": 8453,
"token_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"from_address": "0xE4b5E865CA9F9910C666014C02550F17Ce690149",
"to_address": "0x1111111111111111111111111111111111111111",
"tx_hash": "0x9a1f…",
"reason": "customer request",
"created_at": "2026-06-15T10:00:00.000Z"
}Create a refund
First send the refund on-chain from a verified settlement wallet to the payment's payer (same token), then report it here. Subsie fetches the receipt and requires a matching ERC-20 Transfer; re-reporting the same transaction returns the existing refund.
paymentstringrequiredThe payment to refund (pay_…).
tx_hashstringrequiredHash of the on-chain transfer that returned funds to the payer.
amountstringOptional. Must equal the on-chain transfer amount; omit to record the full transfer. Cumulative refunds cannot exceed the gross paid.
reasonstringOptional note, up to 500 characters.
Returns 201 with the refund (200on an idempotent re-report). A transfer that doesn't match — wrong token, not from a verified wallet, not to the payer, or over the refundable amount — is rejected.
Retrieve and list
limitnumber1–100, default 20.
starting_afterstringRefund id cursor for the next page.
paymentstringFilter to one payment's refunds.
Subscriptions
A subscription is created when a buyer completes a subscription-mode checkout session. Subsie charges it on schedule and manages retries (dunning) when a charge fails — state transitions arrive as subscription.* webhooks.
Billing terms live in immutable on-chain plans, created by you — not by Subsie. Your backend (or keeper/SDK) signs one createPlan call on the SubscriptionManager contract; Subsie never holds your keys. Pass the resulting chain_plan_id to POST /api/v1/checkout/sessions with mode: "subscription"— Subsie reads the plan from the chain, verifies its recipient is one of your verified settlement wallets, and registers it automatically. The dashboard's Billing plans page is a read-only viewer (terms, subscriber counts, on-chain status) plus a register-by-id tool; it never creates plans. Changing price or interval means a new plan.
{
"id": "sub_Mq7wRb3KfXcVtnYpLZjdGh2A",
"object": "subscription",
"livemode": true,
"status": "active",
"title": "Pro plan",
"customer": "cus_Jx2mPq8GfTuWbnZmVRkdQy4N",
"wallet_address": "0x1111111111111111111111111111111111111111",
"chain_id": 8453,
"chain_subscription_id": "12",
"chain_plan_id": "3",
"amount": "25",
"interval_seconds": 2592000,
"next_payment_at": "2026-07-12T10:00:00.000Z",
"canceled_at": null,
"retry_count": 0,
"next_retry_at": null,
"last_failure_code": null,
"created_at": "2026-06-12T10:00:00.000Z"
}Statuses: pending, active, past_due (a charge failed and retries are scheduled), paused, canceled, and expired (retries exhausted).
Retrieve a subscription
List subscriptions
limitnumber1–100, default 20.
starting_afterstringSubscription id cursor for the next page.
statusstring"pending", "active", "past_due", "paused", "canceled", or "expired".
wallet_addressstringFilter by the subscriber's wallet address.
Payment attempts
Every recurring charge try — successful or failed — is recorded as a payment attempt, so you can see exactly why a subscription went past due and when the next retry runs.
{
"id": "att_Bv9cTk2NfWqXrnLpMZjdAh6E",
"object": "payment_attempt",
"livemode": true,
"subscription": "sub_Mq7wRb3KfXcVtnYpLZjdGh2A",
"payment": null,
"status": "failed",
"attempt_number": 2,
"period_key": "2026-07-12T10:00:00.000Z",
"failure_code": "insufficient_allowance",
"failure_reason": "USDC allowance below charge amount",
"tx_hash": null,
"created_at": "2026-07-13T10:00:00.000Z"
}List payment attempts
limitnumber1–100, default 20.
starting_afterstringAttempt id cursor for the next page.
subscriptionstringFilter to one subscription.
statusstring"succeeded" or "failed".
Settlement wallets
Payments settle directly to wallets you control. Before a wallet can receive payments, you prove ownership by signing a free, gas-less EIP-191 message. Both standard wallets and ERC-1271 smart wallets (e.g. Coinbase Smart Wallet) are supported.
Register a wallet
addressstringrequiredThe EVM address that will receive settlements.
chain_idnumberDefaults to 8453 (Base). A wallet verified once is usable for test and live sessions.
Returns the wallet with a verification object containing the message to sign and its expires_at (challenges live for one hour and are single-use). Registering an already-verified address returns it unchanged.
Verify a wallet
signaturestringrequiredHex signature (0x…) of the challenge message, produced by the wallet being verified.
Returns the wallet with status: "verified". An expired or already-used challenge returns 400/409 — re-register to get a fresh one.
Retrieve, list, revoke
Revoking sets status: "revoked"; the wallet can no longer be used as a session recipient. Existing completed payments are unaffected.
Webhook endpoints
Endpoints receive signed event deliveries. An endpoint belongs to the mode of the key that created it — test endpoints receive only test events, live endpoints only live events. Full delivery semantics are covered in the webhooks guide.
Create a webhook endpoint
urlstringrequiredHTTPS URL to deliver events to (http://localhost is allowed for development). Up to 2000 characters.
enabled_eventsstring[]Subset of event types to deliver. Empty or omitted means all events.
Returns 201 including the signing secret (whsec_…) — shown only in this response. Supports Idempotency-Key; replays return the same endpoint without re-revealing the secret.
Send a test event
typestringEvent type to simulate, e.g. "checkout.session.completed". Must be within the endpoint’s enabled_events filter. Defaults to "payment.succeeded".
Returns 201with the enqueued webhook event. The delivery is real — signed with the endpoint's secret and retried on failure — but data.object is synthetic: its ids never resolve via the retrieval APIs.
List and delete
List responses omit the secret. Deleting returns { "id": "we_…", "object": "webhook_endpoint", "deleted": true }.
Webhook events
Delivery observability: one record per event per endpoint, with attempt counts, the last response status, and the next retry time. Use it to debug a misbehaving receiver without waiting for retries.
List webhook events
limitnumber1–100, default 20.
starting_afterstringEvent id cursor for the next page.
statusstring"pending", "succeeded", or "failed".
typestringFilter by event type, e.g. "payment.succeeded".
{
"id": "evt_Xc4nQv8MfYwRtnKpBZjdLh9F",
"object": "webhook_event",
"livemode": false,
"type": "checkout.session.completed",
"endpoint": "we_Pz5mWk2VfNqXrnGpDZjdTh3C",
"status": "succeeded",
"attempts": 1,
"next_attempt_at": null,
"last_attempt_at": "2026-06-12T10:05:03.000Z",
"last_error": null,
"response_status": 200,
"resend_of": null,
"payload": { "id": "evt_…", "type": "checkout.session.completed", "...": "..." },
"created_at": "2026-06-12T10:05:00.000Z"
}Resend an event
Redelivers the event: a fresh delivery with the original payload byte-for-byte (same envelope id, so consumer dedupe treats both as one logical event) and a full retry budget. Returns 201 with the new webhook event, its resend_of set to the original root event id. Returns 409 while the original or an existing redelivery is still pending, and 400 if its endpoint is disabled.