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
  1. Create a key at /developers — name it and pick the scopes you need. The sk_… secret is shown once, at creation; store it immediately.
  2. Send it with every request as X-API-Key: sk_… or Authorization: Bearer sk_….
  3. 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 methods

A 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() },
);
FileBytes
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/v1
GET /api/v1/me no scope required

The 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"}
GET /api/v1/listings scope listings:read

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}
POST /api/v1/listings scope listings:write fires listing.created

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.

GET /api/v1/offers scope offers:read

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}
POST /api/v1/offers scope offers:write fires offer.received

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.

POST /api/v1/offers/{id}/accept scope offers:write

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
GET /api/v1/orders scope orders:read

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}
GET /api/v1/wallet scope wallet:read

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"}
StatusWhen
401Missing, unknown, revoked or expired key — or an account that is not active.
Missing API key — send X-API-Key: sk_…
403The key lacks the scope the endpoint needs.
This key lacks the "listings:read" scope.
404No such listing or offer.
409State 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.
422Validation failed — missing title, description under 10 characters, price ≤ 0, unsupported currency.
429Rate 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
ScopeGrants
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-signature

Subscribe 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);
listing.created A listing is published live
offer.received Someone sends you an offer live
order.released Escrow releases on a trade live
ping.test Manual test delivery live

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: