SubsieBeta
Reference

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 envelope
{
  "error": {
    "code": "invalid_request_error",
    "type": "invalid_request_error",
    "message": "amount is required for payment mode",
    "details": [{ "field": "amount", "message": "Required" }]
  }
}
StatusCodeMeaning
400invalid_request_errorThe request body or query failed validation.
401authentication_errorMissing, invalid, or revoked API key.
404not_found_errorNo such resource for this merchant and mode.
409conflict_errorState conflict, e.g. a concurrent request in flight.
409idempotency_errorIdempotency key reused with a different body.
429rate_limit_errorToo many requests; honor the Retry-After header.
500api_errorSomething 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).

List envelope
{
  "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).

The checkout session object
{
  "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

POST/api/v1/checkout/sessions
modestringrequired

"payment" for a one-time charge, "subscription" to start recurring billing against an on-chain plan (live mode only).

titlestringrequired

What the buyer is paying for, shown on the hosted page. 1–120 characters.

amountstringrequired

Gross 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.

descriptionstring

Optional detail line, up to 500 characters.

recipientstring

Verified settlement wallet address to receive funds. Optional when you have exactly one verified wallet; required when you have several.

customer_referencestring

Your internal id for the buyer (user id, order id). Returned on the session, the payment, and webhooks. Up to 250 characters.

metadataobject

Arbitrary key–value data stored on the session and echoed back verbatim.

success_urlstring

Where the buyer is sent after paying. Must be a valid URL, up to 2000 characters.

cancel_urlstring

Where the buyer is sent if they back out.

expires_in_secondsnumber

Session lifetime: 600 (10 minutes) to 604800 (7 days). Default 86400 (24 hours).

chainnumber | string

Optional cross-check: 8453/"base" or 84532/"base-sepolia". Must match the chain implied by your key mode; mismatches return 400.

chain_plan_idstring | number

Subscription mode only: the on-chain billing plan id the buyer subscribes to. Required for subscription mode.

interval_secondsnumber

Subscription 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

GET/api/v1/checkout/sessions/:id

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

GET/api/v1/checkout/sessions
limitnumber

1–100, default 20.

starting_afterstring

Session id cursor for the next page.

statusstring

"open", "completed", or "expired".

customer_referencestring

Exact-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.

The payment object
{
  "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

GET/api/v1/payments/:id

List payments

GET/api/v1/payments
limitnumber

1–100, default 20.

starting_afterstring

Payment id cursor for the next page.

subscriptionstring

Filter to one subscription's charges.

checkout_sessionstring

Filter 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.

The refund object
{
  "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

POST/api/v1/refunds

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.

paymentstringrequired

The payment to refund (pay_…).

tx_hashstringrequired

Hash of the on-chain transfer that returned funds to the payer.

amountstring

Optional. Must equal the on-chain transfer amount; omit to record the full transfer. Cumulative refunds cannot exceed the gross paid.

reasonstring

Optional 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

GET/api/v1/refunds/:id
GET/api/v1/refunds
limitnumber

1–100, default 20.

starting_afterstring

Refund id cursor for the next page.

paymentstring

Filter to one payment's refunds.

Subscriptions

Live mode only
Recurring billing runs against the SubscriptionManager contract on Base mainnet. Creating a subscription-mode checkout session with a test key returns 400.

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.

The subscription object
{
  "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

GET/api/v1/subscriptions/:id

List subscriptions

GET/api/v1/subscriptions
limitnumber

1–100, default 20.

starting_afterstring

Subscription id cursor for the next page.

statusstring

"pending", "active", "past_due", "paused", "canceled", or "expired".

wallet_addressstring

Filter 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.

The payment attempt object
{
  "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

GET/api/v1/payment_attempts
limitnumber

1–100, default 20.

starting_afterstring

Attempt id cursor for the next page.

subscriptionstring

Filter 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

POST/api/v1/wallets
addressstringrequired

The EVM address that will receive settlements.

chain_idnumber

Defaults 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

POST/api/v1/wallets/:id/verify
signaturestringrequired

Hex 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

GET/api/v1/wallets/:id
GET/api/v1/wallets
DELETE/api/v1/wallets/:id

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

POST/api/v1/webhook_endpoints
urlstringrequired

HTTPS 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

POST/api/v1/webhook_endpoints/:id/test
typestring

Event 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

GET/api/v1/webhook_endpoints
DELETE/api/v1/webhook_endpoints/:id

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

GET/api/v1/webhook_events
limitnumber

1–100, default 20.

starting_afterstring

Event id cursor for the next page.

statusstring

"pending", "succeeded", or "failed".

typestring

Filter by event type, e.g. "payment.succeeded".

The webhook event object
{
  "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

POST/api/v1/webhook_events/:id/resend

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.