API reference
Settle API
Everything you do on the market, from your own servers: read offers, post and fund ads, open and run trades, and hear about every step through webhooks or an event feed.
Mainnet, real ETH
Overview
A JSON API over HTTPS. Your key acts as your account: the same rules, limits and escrow as on the site.
Base URL
https://usesettle.cash/api/v1Bodies and answers are JSON. Public reads (/config, /methods, /market, /ads/{id}) need no key.
Quick start
- 1
Create a key
On the Developers page, signed in with X. Pick what it may do. - 2
Call the API
From your server, with the key in the Authorization header. - 3
Listen
Add a webhook, or poll /events, to react when a trade moves.
curl https://usesettle.cash/api/v1/market?side=sell&method=paypal
curl https://usesettle.cash/api/v1/me \
-H "Authorization: Bearer $SETTLE_API_KEY"SDK
One file, no dependencies, for Node 20+, Deno and Bun: settle.mjs. It sets idempotency keys, retries safely, refuses to run in a browser and verifies webhook signatures.
// curl -o settle.mjs https://usesettle.cash/sdk/settle.mjs
import { createClient } from "./settle.mjs";
const settle = createClient({ apiKey: process.env.SETTLE_API_KEY });
const { ads } = await settle.market({ side: "sell", method: "paypal" });
const { trade } = await settle.trades.open({ adId: ads[0].id, fiatCents: 2500, method: "paypal" });
console.log(trade.payee); // where to send the $25Authentication
Personal keys, sent as a bearer token. Server to server only.
API keys
Authorization: Bearer settle_sk_<id>_<secret>- A key acts as the account that created it. Up to 5 active keys per account; revoke one any time.
- We store only a hash. The full key is shown once, when you create it.
- Keep it on a server. A request that carries a browser's
Originheader is refused (browser_not_allowed).
API keys are for Pro accounts
403 stake_required).Scopes
| Scope | Lets the key |
|---|---|
readRead your account, ads, trades, messages and events; build deposit calldata. | Always on |
tradePost, pause and close ads, withdraw free escrow, open trades, mark paid, cancel, dispute, chat. | Opt-in |
releaseRelease a trade's ETH to the buyer. Only for a server that checks your payments itself. | Opt-in |
Release is the dangerous one
release can give your ETH away. Release only after your server has seen the money land in your account, never on the buyer's word.What a key can't do
- There is no API to change your wallet or where you get paid. Those are set on the site only.
- Trades use your saved wallet and payees; withdrawals go to your saved wallet.
- Deposits are calldata you sign with your own wallet. Settle never holds your keys.
- No admin powers: disputes are decided by the Settle team, on the site.
Conventions
Errors
A status code, and a body with a stable error code and a human message:
{ "error": "insufficient_scope", "message": "This key doesn't have the \"release\" scope…" }| Status | Codes |
|---|---|
| 400Validation, or a trading rule (the message says which). | bad_request |
| 401 | missing_api_key · invalid_api_key · revoked_api_key |
| 403 | insufficient_scope · stake_required · browser_not_allowed |
| 404 | not_found |
| 409 | idempotency_conflict · conflict |
| 413 / 415 | body_too_large · unsupported_media_type |
| 429Wait Retry-After seconds. | rate_limited |
| 503Price feed or chain RPC down: retry. | unavailable |
Rate limits
60 requests a minute per key, 300 per IP. Over it: 429 rate_limited with a Retry-After header. The market's own limits apply too: at most 20 trades opened a day and 3 open at a time.
Idempotency
Send Idempotency-Key on every POST and PATCH. A retry with the same key and the same request gets the first answer back (header Idempotent-Replayed: true), so a timeout can't open two trades. Keys live 24 hours; the same key with a different request is a 409 idempotency_conflict.
Units
| ETH amounts"5587000000000000" = 0.005587 ETH on Robinhood Chain. | wei, as strings |
| USDG amounts"20000000" = 20 USDG. Every ad and trade says its asset. | 6 decimals, as strings |
| SOL amounts"1500000000" = 1.5 SOL, on Solana. | lamports, as strings |
| Dollar amountsfiatCents: 2500 = $25.00. | integer cents |
| PricesA number, e.g. 2711.64 per ETH, about 1 per USDG. | USD per whole unit |
| Times | ISO 8601, UTC |
Market
Public: no key needed.
/configpublicvault./methodspublic/market?side=sell&method=paypal&amountCents=2500publicside=sell: people selling (you buy); side=buy: people buying. asset=ETH (default), asset=USDG or asset=SOL./ads/{id}publicAccount
/meread/me/adsreadavailableWei, listed, toListWei.Ads
/adstrade/ads/{id}trade{ "status": "active" | "paused" | "closed" }/ads/{id}/deposit?amountWei=…read/ads/{id}/withdrawtrade{
"side": "sell",
"asset": "ETH", // or "USDG" (priced around $1), or "SOL" (on Solana)
"priceType": "market", // or "fixed" with "fixedPriceUsd"
"marginBps": 150, // +1.5% over the market price
"minCents": 1000,
"maxCents": 50000,
"methods": ["paypal", "xmoney", "revolut"],
"paymentWindowMin": 30, // 15, 30, 45, 60
"terms": "PayPal friends & family only"
}Funding a sell ad
A sell ad shows on the market once its escrow covers the minimum trade and the 1% fee (toListWei says how much is missing). Ask for the calldata, check to against /config, then sign and send it from your wallet on Robinhood Chain. For a USDG ad the answer also carries an approve call to send first (the token vault pulls the USDG). A SOL ad answers with an address instead: send amount lamports to to with a plain transfer, from any wallet or exchange (uri is a Solana Pay link):
{ "deposit": {
"chainId": 4663,
"to": "<vault>",
"value": "10000000000000000",
"data": "0xee214668…", // depositFees(routeId, 0x0, 0)
"routeId": "0x…"
} }Trades
The flow
- 1
Open
POST /trades on an ad. On a sell ad you buy: the ETH is already locked. On a buy ad you sell: lock it with trade.deposit. - 2
Pay
The buyer sends the money to trade.payee, then POST /trades/{id}/paid. - 3
Release
The seller checks the money arrived, then POST /trades/{id}/release. The vault sends the ETH to the buyer.
The buyer only sees payee once the ETH is locked. The seller has 30 minutes to lock on a buy ad; the buyer has the ad's payment window to pay.
Endpoints
/trades?status=open|closed|all&limit=50&cursor=…readnextCursor for more./trades/{id}read/tradestrade{ "adId", "fiatCents", "method" }. Between $10 and $5,000, within the ad's limits./trades/{id}/paidtrade{ "note" }, like a reference./trades/{id}/releaserelease/trades/{id}/canceltrade/trades/{id}/disputetrade{ "reason" }. Once the payment is marked as sent./trades/{id}/messages?after={messageId}read/trades/{id}/messagestrade{ "body" }, up to 1,000 characters.Events
Every step of your trades, in order. Poll it, or get the same events by webhook.
/events?after={eventId}&limit=100readafter; keep the next you get back for the next call. Kept 30 days.for await (const event of settle.events.poll({ after: lastSeenId })) {
if (event.type === "trade.paid" && event.data.trade.role === "seller") {
// check your PayPal / bank, then release
}
}Event types
trade.openedA trade was opened: on your ad, or by you.trade.escrowedThe ETH is locked in the vault. The buyer can pay.trade.paidThe buyer marked the payment as sent. Seller: check your account.trade.releasingThe release is on its way to the buyer.trade.completedThe ETH reached the buyer's wallet.trade.disputedSomeone opened a dispute. The ETH stays locked.trade.cancelledThe trade ended without a release.trade.messageA new chat message, yours included (check message.mine).pingA test, sent from the Developers page (webhooks only).Webhooks
One HTTPS endpoint per account. Each event is POSTed as JSON, signed with your secret.
Set up
Add the URL on the Developers page. You get a signing secret (whsec_…) once; send a test ping from there. Each request carries:
Content-Type: application/json
Settle-Event-Id: 1042
Settle-Event-Type: trade.paid
Settle-Signature: t=1791052376,v1=5f1c…Verify the signature
v1 is the hex HMAC-SHA256 of <t>.<raw body> with your secret. Check it on the raw body, in constant time, and refuse a t more than 5 minutes off:
import { verifyWebhook } from "./settle.mjs";
const event = await verifyWebhook({
rawBody, // the exact bytes received
signatureHeader: req.headers["settle-signature"],
secret: process.env.SETTLE_WEBHOOK_SECRET,
});Retries
- Answer 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h, then mark it failed (you can resend it).
- At least once, not in order: dedupe on the event id, and read the trade before acting on it.
- Redirects aren't followed. Private and internal addresses are refused.
Objects
The main shapes. The full schema is in the OpenAPI file.
Ad
{
"id": "d8db34f1-…", "side": "sell", "status": "active",
"priceUsd": 2724.5, "priceType": "market", "marginBps": 150, "fixedPriceUsd": null,
"minCents": 1000, "maxCents": 50000,
"methods": ["paypal", "xmoney"], "paymentWindowMin": 30, "terms": null,
"availableWei": "262181836921485257", "listed": true, "toListWei": null,
"advertiser": { "handle": "nora", "verified": true, "trades": 48, "completion": 1 },
"createdAt": "2026-10-03T17:19:30.000Z"
}Trade
{
"id": "b9c75c9a-…", "adId": "d8db34f1-…", "role": "buyer", "status": "escrowed",
"method": "paypal", "fiatCents": 2000, "priceUsd": 2671.6,
"amountWei": "7485969748709657", "feeWei": "74859697487096",
"buyerWallet": "0x81b1…", "sellerWallet": null,
"payee": "seller@example.com", "giftCard": false,
"escrowRouteId": "0x…", "escrowSource": "ad",
"payDeadline": "2026-10-03T18:03:09.000Z", "paidAt": null,
"buyer": { "handle": "you", … }, "seller": { "handle": "nora", … },
"legs": [], "deposit": null,
"createdAt": "…", "updatedAt": "…", "completedAt": null
}Event
{
"id": "1042",
"type": "trade.paid",
"createdAt": "2026-10-03T18:33:12.000Z",
"data": {
"trade": { "id": "b9c75c9a-…", "role": "seller", "status": "paid", "method": "paypal",
"fiatCents": 2000, "amountWei": "7485969748709657",
"counterparty": { "handle": "you", "avatarUrl": null }, … }
}
}v1New fields may be added to any object; existing ones won't change meaning within v1.