
Developers / API reference
One API. Direct settlement.
Create hosted stablecoin checkouts, verify settlement, and keep your application in sync with signed webhooks. Every route below is relative to your Settle domain.
Shipping physical goods
- Enable Collect a shipping address on your product or payment link.
- After payment, open Dashboard → Shipping to see orders waiting to ship.
- Select orders to print 4 x 6 address labels and optional packing slips. Your return address stays in your browser.
- Hand the parcel to your carrier, then mark the order shipped. Add a carrier and, for an individual order, tracking details. Optionally email the customer.
Postage is not included. Settle does not buy postage, insure parcels or monitor delivery. Test orders stay in Test mode and never send customer emails. Owners, admins and support can mark orders shipped; viewers have read-only access.
Authentication
Public endpoints authenticate with your publishable key (pk_live_… or pk_…), found in Dashboard →︎ Developers. The secret key (sk_live_…) is reserved for future server-side endpoints — never expose it in a browser or mobile app.
POST /api/checkout
{
"api_key": "pk_live_…",
"amount": 10
}Create a checkout
POST/api/checkout
Creates a pending invoice and returns a hosted checkout URL plus the exact on-chain payment details. Bill either a custom amount or a plan_id from Dashboard →︎ Plans.
{
"api_key": "pk_live_…",
"amount": 49.00, // or "plan_id": "uuid"
"currency": "USDC", // USDC | USDT | DAI | ETH | SOL (plans use their own)
"chain": "base", // optional: an enabled merchant payout network
"customer_email": "buyer@your-domain.com", // required when subscribe is true
"subscribe": false, // true with plan_id →︎ start a subscription
"discount_code": "LAUNCH20" // optional, validated + redeemed
}{
"id": "3f9c…", // invoice id
"url": "https://your-site/checkout/3f9c…",
"pay_to": "0x7A9c…3F21", // merchant wallet (funds go here directly)
"chain": "base",
"currency": "USDC",
"amount": 49.000371, // EXACT on-chain amount to send
"nominal_amount": 49,
"expires_at": "2026-09-08T18:04:00Z",
"test_mode": false
}The customer must send exactly amount of the chain's token contract to pay_to. A small matching adjustment distinguishes this invoice: below 0.01 token for stablecoins and at most 0.000009 ETH or SOL. It goes to the merchant, not Settle. Always display and send the returned exact amount; never round it.
Currency and network must match
Set chain in the JSON body or call /api/checkout?chain=solana. A non-empty body value takes precedence. Without an override, checkout uses the merchant's primary wallet. The selected network must be enabled in Settings. SOL requires Solana; ETH is only accepted where it is the native coin (Ethereum, Base, Arbitrum, Optimism), not on Polygon. Stablecoin availability also varies by network. Settle never swaps tokens or bridges funds.
HTTP 400
{ "error": "SOL is not payable on ethereum" }
HTTP 409
{ "error": "Merchant does not accept payments on that chain" }An incompatible currency/network is rejected before an invoice, subscription, or discount redemption is created. Missing, disabled, or unconfigured payout networks return 409. Plan creation also requires an enabled wallet supporting the plan's currency. An unrecognized currency for a custom charge returns 400, rather than silently using USDC. Renewals use the subscription's network when it is still enabled, otherwise another enabled network supporting that currency.
Check status
GET/api/checkout/status?id={invoice_id}
Returns the invoice status (pending, paid, expired, …) and, when paid, the transaction hash. While an invoice is pending, the poll also runs a log-scan settlement fallback (at most once every few seconds per invoice), so QR payments settle even without webhooks — the hosted checkout polls this every 4 seconds.
Confirm a wallet payment
POST/api/checkout/confirm
After a wallet payment mines, submit the transaction hash. Settle fetches the receipt on-chain and requires a matching transfer — an ERC-20 Transfer on EVM chains, or an SPL / native transfer on Solana — of the invoice's exact amount, token, and recipient. An unrelated hash settles nothing. If the transaction is not yet visible on-chain, the route answers 202 — retry it shortly after.
{
"invoice_id": "3f9c…",
"tx_hash": "0x9f2c…d41a"
}Idempotency
Send an Idempotency-Key header (any unique string, e.g. your order id) with POST /api/checkout. Replaying the same key returns the original invoice instead of creating a duplicate — safe for retries, timeouts, and double-clicks.
curl -X POST https://your-site/api/checkout \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234" \
-d '{"api_key":"pk_live_…","amount":49}'Test mode
Use your test key (pk_test_…) to create checkouts flagged test_mode. They run the exact same flow but are excluded from your analytics, and the payment simulator stays available for them in production. Roll the test key any time from Dashboard →︎ Developers.
Webhooks
Register HTTPS endpoints in Dashboard →︎ Developers. Every event is HMAC-SHA256-signed in the X-Settle-Signature header (hex digest of the raw body, using the endpoint's whsec_… secret). Failed deliveries retry with backoff for up to 12 attempts and can be replayed manually from the event log.
When a subscriber stops paying
Renewals are invoices the customer pays from their wallet, so nothing fails silently. You get subscription.past_due when the due date passes unpaid (reminders keep going), then — after the last reminder in your schedule (Settings → Billing) — subscription.canceled or subscription.paused with "reason": "unpaid". That is the signal to revoke access. A manual stop carries "customer_request" or "merchant_request" instead.
{
"event": "subscription.canceled",
"livemode": true,
"data": {
"id": "sub_…",
"status": "canceled",
"customer_email": "jane@example.com",
"canceled_by": "dunning",
"reason": "unpaid",
"livemode": true
}
}{
"event": "invoice.paid",
"livemode": true,
"created": 1789012345,
"data": {
"id": "3f9c…",
"amount": 49,
"currency": "USDC",
"tx_hash": "0x9f2c…d41a",
"customer_email": null,
"net_amount": 49,
"livemode": true
}
}data.amount is the invoice's nominal price — the buyer actually sent the unique on-chain amount (nominal + a sub-cent delta). customer_email is null for anonymous checkouts. livemode is false for test-mode invoices, which the public payment simulator can settle — check it before fulfilling anything. Retries and replays can deliver an event more than once, so handle them idempotently on data.id.
import crypto from "node:crypto";
const expected = crypto
.createHmac("sha256", process.env.SETTLE_WEBHOOK_SECRET)
.update(rawBody) // the RAW request body string
.digest("hex");
const given = String(req.headers["x-settle-signature"] ?? "");
const valid =
given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (valid) {
const { event, livemode, data } = JSON.parse(rawBody);
if (livemode && event === "invoice.paid") fulfil(data.id);
}import hmac, hashlib
expected = hmac.new(
SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
if hmac.compare_digest(expected, request.headers.get("X-Settle-Signature", "")):
payload = json.loads(raw_body)
if payload["livemode"] and payload["event"] == "invoice.paid":
fulfil(payload["data"]["id"])Links, products & storefronts
/l/{link_id}— a payment link from Dashboard →︎ Payment Links. Append?discount=CODEto apply a discount at entry./p/{product_id}— buy button for a catalog product; also accepts?discount=CODE./u/{slug}— the merchant's public storefront page (Dashboard →︎ Storefront).
Reconciliation & scheduling
GET/api/cron/reconcile
Platform operators schedule this endpoint approximately every five minutes. It scans open invoices for matching on-chain payments, including transfers made after a customer closes checkout, and lists transfers to payout wallets that match no invoice in the dashboard inbox for review. It is a fallback for provider webhooks, not a merchant API.
GET /api/cron/reconcile Authorization: Bearer <CRON_SECRET> // JSON response includes ok and reconciliation counters. // Missing or invalid authorization returns HTTP 401.
Keep CRON_SECRET on the server and in the scheduler only; publishable and merchant secret keys cannot authorize this endpoint. The separate hourly GET /api/cron/billing job issues renewal invoices and reminders, applies each plan's unpaid-renewal rule, expires invoices, retries outbound webhooks, and also reconciles payments. Both jobs must be scheduled externally.
Confirmation timing depends on the network, RPC provider, and scheduler. A checkout timeout is not proof that no funds were sent.
Errors & limits
| Status | Meaning |
|---|---|
| 202 | Confirm accepted, transaction not visible on-chain yet — retry |
| 400 | Invalid request — missing api_key or bad params (see error) |
| 401 | Invalid api_key |
| 403 | Simulator disabled, or viewers attempting writes |
| 404 | Unknown invoice / plan / product |
| 409 | Merchant not configured, or slug already taken |
| 429 | Rate limited — slow down (Retry-After header) |
Public endpoints are rate limited per IP / key (60 checkouts per minute per key) to protect the unique-amount space.
Ready to build?
Explore the API now. Create an account to get your keys and run your first checkout in minutes.
Create an account