Post an ad

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

The API moves real ETH on Robinhood Chain, exactly like the site.

Overview

A JSON API over HTTPS. Your key acts as your account: the same rules, limits and escrow as on the site.

Base URL

Base URL
https://usesettle.cash/api/v1

Bodies and answers are JSON. Public reads (/config, /methods, /market, /ads/{id}) need no key.

Quick start

  1. 1

    Create a key

    On the Developers page, signed in with X. Pick what it may do.
  2. 2

    Call the API

    From your server, with the key in the Authorization header.
  3. 3

    Listen

    Add a webhook, or poll /events, to react when a trade moves.
Terminal
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.

Node
// 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 $25

Authentication

Personal keys, sent as a bearer token. Server to server only.

API keys

Header
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 Origin header is refused (browser_not_allowed).

API keys are for Pro accounts

Creating and using a key needs a Pro stake: 500,000 SETTLE or more, on the Stake page. Unstake below it and your keys pause until the stake is back (403 stake_required).

Scopes

ScopeLets 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

A leaked key with 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:

JSON
{ "error": "insufficient_scope", "message": "This key doesn't have the \"release\" scope…" }
StatusCodes
400Validation, or a trading rule (the message says which).bad_request
401missing_api_key · invalid_api_key · revoked_api_key
403insufficient_scope · stake_required · browser_not_allowed
404not_found
409idempotency_conflict · conflict
413 / 415body_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
TimesISO 8601, UTC

Market

Public: no key needed.

GET/configpublic
Chain id, vault address, fee (1%), trade limits and payment windows. Check deposit calldata against vault.
GET/methodspublic
Every payment method: id, label, category, region.
GET/market?side=sell&method=paypal&amountCents=2500public
Listed ads, best price first. side=sell: people selling (you buy); side=buy: people buying. asset=ETH (default), asset=USDG or asset=SOL.
GET/ads/{id}public
One ad, with its price now.

Account

GET/meread
Your handle, saved wallet, payment accounts, and this key's scopes.
GET/me/adsread
Your ads, with their escrow: availableWei, listed, toListWei.

Ads

POST/adstrade
Post an ad. Sell ads need a saved payee for each method, buy ads a saved wallet (both set on the site).
PATCH/ads/{id}trade
{ "status": "active" | "paused" | "closed" }
GET/ads/{id}/deposit?amountWei=…read
The calldata to lock ETH or USDG in your sell ad's escrow (amount in base units).
POST/ads/{id}/withdrawtrade
Send the ad's free escrow (not committed to open trades) back to your saved wallet.
POST /ads
{
  "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):

Response
{ "deposit": {
  "chainId": 4663,
  "to": "<vault>",
  "value": "10000000000000000",
  "data": "0xee214668…",       // depositFees(routeId, 0x0, 0)
  "routeId": "0x…"
} }

Trades

The flow

  1. 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. 2

    Pay

    The buyer sends the money to trade.payee, then POST /trades/{id}/paid.
  3. 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

GET/trades?status=open|closed|all&limit=50&cursor=…read
Your trades, newest first. Follow nextCursor for more.
GET/trades/{id}read
The whole trade: status, amounts, payee, deadlines, the release legs and their transactions.
POST/tradestrade
{ "adId", "fiatCents", "method" }. Between $10 and $5,000, within the ad's limits.
POST/trades/{id}/paidtrade
Buyer: the payment is sent. Optional { "note" }, like a reference.
POST/trades/{id}/releaserelease
Seller: the money arrived, release the ETH.
POST/trades/{id}/canceltrade
Buyer before paying, or seller before locking.
POST/trades/{id}/disputetrade
{ "reason" }. Once the payment is marked as sent.
GET/trades/{id}/messages?after={messageId}read
The trade chat, oldest first.
POST/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.

GET/events?after={eventId}&limit=100read
Events after after; keep the next you get back for the next call. Kept 30 days.
Node
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:

Headers
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:

Node
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

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

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

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.