Developers
SwapR API — reference
Connect your app, shop or automation to SwapR: REST over HTTPS, JSON in and out, keys you issue yourself.
Connect in three steps
60 requests/minute per key- Create a key at /developers — name it and pick the scopes you need. The sk_… secret is shown once, at creation; store it immediately.
- Send it with every request as X-API-Key: sk_… or Authorization: Bearer sk_….
- Subscribe webhooks at /developers so your endpoint hears about events — each delivery is signed with your whsec_… secret.
Retries: send Idempotency-Key on POST /api/v1/listings and a retry replays the first answer instead of creating a duplicate. Machine-readable spec: /api/openapi.json.
curl -s https://www.myswapr.com/api/v1/me \ -H "X-API-Key: sk_YOUR_KEY"
Base URL for every endpoint on this page: https://www.myswapr.com. Responses are JSON; keys authenticate the member who issued them, so the API acts as you.
TypeScript SDK
generated · 11 typed methodsA typed client generated from the same OpenAPI document this page is built from, so it can never describe a route the API does not serve. Every response has a real interface — no any. Regenerate with php bin/sdk.php.
import { SwapRClient } from '@swapr/api-client';
const swapr = new SwapRClient({ apiKey: process.env.SWAPR_API_KEY! });
const me = await swapr.getMe();
const { listings } = await swapr.listListings({ limit: 10 });
await swapr.createListing(
{ title: 'iPhone 15 Pro', description: 'Sealed, 256GB.', price: 1199 },
{ idempotencyKey: crypto.randomUUID() },
);
| File | Bytes |
|---|---|
| types.ts | 3,582 |
| client.ts | 11,886 |
| index.ts | 247 |
| swapr.ts | 15,250 |
| package.json | 533 |
| README.md | 5,523 |
One method per endpoint below: getMe(), listListings(), createListing(), listOffers(), createOffer(), acceptOffer(), declineOffer(), withdrawOffer(), counterOffer(), listOrders(), getWallet(). The single file swapr.ts has no imports at all — drop it in and go.
Endpoints
v1 · all under /api/v1The account behind the key — id, name, email, username, role, verification state, market and currency.
{"id":"85916a4a-…","name":"Ife BNPL Test","email":"you@example.com","username":"you",
"role":"USER","verified":true,"kyc_tier":1,"market":"US","currency":"USD",
"member_since":"2026-09-24 00:02:11"}
Browse listings. Query parameters: limit (1–50, default 20), offset (default 0), mine=1 (only your own listings instead of the public active feed).
curl -s "https://www.myswapr.com/api/v1/listings?limit=2&mine=1" \
-H "X-API-Key: sk_YOUR_KEY"
{"listings":[{"id":"b4cf8a8f-…","title":"Camera lens","description":"Clean glass…",
"price_cents":3000,"currency":"USD","condition":"GOOD","type":"SALE","status":"ACTIVE",
"created_at":"2026-09-24 00:05:40","owner_name":"Ife BNPL Test","image":null}],
"count":1,"limit":2,"offset":0}
Create a listing (JSON body; form fields also accepted). Same monthly plan allowance as the web form — the Free plan allows 1/month.
curl -s -X POST https://www.myswapr.com/api/v1/listings \
-H "X-API-Key: sk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"title":"Camera lens","description":"Clean glass, no fungus",
"price":30.00,"currency":"USD","condition":"GOOD","type":"SALE",
"category":"electronics"}'
201 → {"created":true,"id":"…","url":"/listing/…"}
Fields: title (required), description (10+ characters), price (> 0, in money units — stored as cents), currency (default USD, must be a supported market currency), condition (NEW · LIKE_NEW · GOOD · FAIR, unknown falls back to GOOD), type (SALE · SWAP · BOTH, unknown falls back to SALE), category (electronics · fashion · home · collectibles · sports · other, unknown falls back to other).
Idempotency-Key (header, optional but recommended): send a unique value per create — replaying the same key with the same body returns the original 201 response byte-for-byte (Idempotency-Replayed: true); the same key with a different body is refused with 409.
Your last 30 offers — sent and received — with listing title, price and status.
{"offers":[{"id":"2362a4eb-…","status":"ACCEPTED","created_at":"2026-09-22 18:11:03",
"title":"Camera lens","price_cents":3000,"currency":"USD"}],"count":1}
Send an offer on a listing — the same state machine as the website. JSON body: listing_id (required), amount (money units; required for BUY), message (≤ 1000 chars), kind (BUY · SWAP), swap_listing_id (your own active listing — required when kind=SWAP).
curl -s -X POST https://www.myswapr.com/api/v1/offers \
-H "X-API-Key: sk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"listing_id":"b4cf8a8f-…","amount":25.50,"message":"Cash today?"}'
201 → {"created":true,"id":"…","status":"PENDING","listing_id":"…","amount_cents":2550}
Answers: 404 no listing · 409 listing not active, swap-only/sale-only mismatch, or your own listing (403) · 422 amount ≤ 0 or missing swap_listing_id.
Answer a pending offer. accept (seller only — creates the escrow order and reserves the listing, 201), decline (seller), withdraw (buyer), counter (either side; body needs amount, optional message). Sibling paths: /api/v1/offers/{id}/decline · /api/v1/offers/{id}/withdraw · /api/v1/offers/{id}/counter.
201 → {"offer_id":"…","status":"ACCEPTED","order_id":"…","url":"/order/…"}
403 wrong side · 409 offer no longer PENDING / listing gone · 422 counter without amount
Escrow orders where the key's account is buyer or seller. Query parameters: limit (1–50, default 20), offset (default 0). Each row carries side (BUY/SELL), status, amounts in cents and the counterparty.
{"orders":[{"id":"9c1f…","status":"FUNDED","side":"BUY","amount_cents":3000,
"fee_cents":75,"payout_cents":0,"listing_title":"Camera lens","counterparty":"Ada"}],
"count":1,"limit":20,"offset":0}
Balances for the account behind the key. Read-only by design — this endpoint can never move money.
{"available_cents":107800,"held_cents":0,"currency":"USD",
"note":"Ledger settles in USD cents; display conversion is a client concern."}
Errors
always JSON: {"error": "message"}| Status | When |
|---|---|
| 401 | Missing, unknown, revoked or expired key — or an account that is not active. Missing API key — send X-API-Key: sk_… |
| 403 | The key lacks the scope the endpoint needs. This key lacks the "listings:read" scope. |
| 404 | No such listing or offer. |
| 409 | State conflict: the account used its plan's monthly listing allowance, an Idempotency-Key was reused with a different body (or its first try is still in flight), or an offer is no longer actionable. Your plan allows 1 listing(s) per month and you have used them all. |
| 422 | Validation failed — missing title, description under 10 characters, price ≤ 0, unsupported currency. |
| 429 | Rate limit — 60 requests/minute per key. The response carries a Retry-After header: seconds until one request ages out of the sliding window. |
Authentication
Send the key either way — both are equivalent:
X-API-Key: sk_YOUR_KEY Authorization: Bearer sk_YOUR_KEY
- Format
- sk_ + 60 hex
- Rate limit
- 60 req/min per key
- Expiry
- never · 90 days · 1 year
- Revocation
- immediate — deleted, not disabled
Every call is logged; watch it under Recent usage on /developers.
Scopes
grant per key| Scope | Grants |
|---|---|
| listings:read * | Browse listings (mine + public feed) |
| listings:write | Create listings on your behalf |
| offers:read | Read offers you sent or received |
| offers:write | Send and respond to offers on your behalf |
| orders:read | Read escrow orders you bought or sold |
| wallet:read | Read wallet balances (never moves money) |
* listings:read is the default when you create a key. /api/v1/me needs any valid key, no scope.
Webhooks
signed · x-swapr-signatureSubscribe endpoints at /developers. Each delivery is a POST with this JSON envelope:
{"id":"evt_9f2c…","event":"listing.created",
"created_at":"2026-09-24T00:12:03+00:00",
"data":{"id":"…","title":"Camera lens","price":30,
"currency":"USD","type":"SALE","condition":"GOOD",
"url":"/listing/…"}}
Verifying the signature — recompute the hex HMAC-SHA256 of the raw body with your endpoint's whsec_… secret and compare in constant time:
$raw = file_get_contents('php://input');
$want = $_SERVER['HTTP_X_SWAPR_SIGNATURE'] ?? '';
$ok = hash_equals(hash_hmac('sha256', $raw, $whsec), $want);
First attempt fires immediately (4s connect / 8s total); a 2xx marks it delivered. Anything else is retried automatically — 1m → 5m → 30m → 2h — then parked as FAILED so you can fix the endpoint and Ping. Retries reuse the same evt_… id, so receivers can dedupe. Queued and failed deliveries show as badges on /developers. Webhooks only receive events of the account that created them.
Try it now
Without a key you get the honest 401 — which is also the fastest way to see the error shape: