Four resources. One status model.
Customers, routes, quotes and transfers. Everything else is a field on one of them. Build against sandbox, switch the key, go live.
$ curl https://api.locals.money/v1/routes \ -H "Authorization: Bearer $LOCALS_KEY" \ -H "Idempotency-Key: 6f1c0b9e-41d2-4c1a-9e2a-3b7d5e2a4c10" \ -d '{"customerId":"cus_8Hq2Lm","source":{"currency":"AUD"}, "destination":{"currency":"USDC","chain":"polygon"}}'
{
"id": "route_3f9a",
"status": "pending",
"customerId": "cus_8Hq2Lm",
"source": { "currency": "AUD" },
"destination": { "currency": "USDC", "chain": "polygon" },
"depositInstructions": {
"accountName": "Harbour & Co",
"routingNumber": "000-000",
"accountNumber": "12345678"
}
}
route_3f9a
verifiedhttps://api.locals.money/v1Authorization: Bearer …Idempotency-KeyJSON · camelCaseFrom zero to first deposit.
Sandbox has the full API, including a deposit simulator. Nothing you do there moves real money.
# Sandbox and live keys are separate. Keep them on your server. export LOCALS_KEY="<your sandbox key>" # Check the key: list your customers curl https://api.locals.money/v1/customers \ -H "Authorization: Bearer $LOCALS_KEY"
curl https://api.locals.money/v1/customers \ -H "Authorization: Bearer $LOCALS_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "type": "business", "name": "Harbour & Co" }' # 201 Created { "id": "cus_8Hq2Lm", "verification": { "status": "in_progress" } }
curl https://api.locals.money/v1/routes \ -H "Authorization: Bearer $LOCALS_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "customerId": "cus_8Hq2Lm", "source": { "currency": "AUD" }, "destination": { "currency": "USDC", "chain": "polygon" } }' # 201 Created: show depositInstructions to your customer { "id": "route_3f9a", "status": "pending", "depositInstructions": { … } }
curl https://api.locals.money/v1/test/routes/route_3f9a/simulate \ -H "Authorization: Bearer $LOCALS_KEY" \ -d '{ "amount": "12500.00", "currency": "AUD" }' # Then, on your webhook endpoint: route.deposit.received A$12,500.00 transfer.processing trf_91kd transfer.completed 8,137.50 USDC
app.post('/hooks', express.raw({ type: '*/*' }), async (req, res) => { if (!verify(req.body, req.get('Locals-Signature'), SECRET)) return res.sendStatus(400); const event = JSON.parse(req.body); // Retries reuse the same id: processing twice is safe if (await seen(event.id)) return res.sendStatus(200); await ledger.apply(event); res.sendStatus(200); });
# .env - LOCALS_KEY="<your sandbox key>" + LOCALS_KEY="<your live key>" # Same endpoints, same payloads, real rails curl https://api.locals.money/v1/routes/route_3f9a \ -H "Authorization: Bearer $LOCALS_KEY"
Every endpoint on one page.
Base URL https://api.locals.money/v1. JSON in, JSON out. Every write accepts an Idempotency-Key.
Customerscus_…
POST/v1/customersCreate an individual or business customer with their verification.idempotent
/v1/customersList your customers./v1/customers/{id}Customer and verification status.Routesroute_…
POST/v1/routesOpen a virtual account: currency in, stablecoin out.idempotent
/v1/routesList a customer's routes./v1/routes/{id}Route, status and deposit instructions.
GET/v1/routes/{id}/reconciliationEvery deposit on the route, matched to its settlement.
/v1/routes/{id}/whitelistAllow-list the account a customer pays from, where the market requires it.idempotentQuotes and transfersquo_… trf_…
POST/v1/quotesPrice a payout. Quotes expire.idempotent
POST/v1/transfersAccept a quote and get exact funding instructions.idempotent
/v1/transfersList transfers./v1/transfers/{id}Transfer and its status history.Sandboxtest only
POST/v1/test/routes/{id}/simulateSandbox only: simulate an incoming bank transfer.sandbox
Cardsillustrative
/v1/cardsIssue a card to an eligible customer. Shape shown for illustration.previewcard.authorizationAn authorisation against the stablecoin balance, in real time.previewPick an endpoint. Copy the call.
Every example runs against sandbox with your key. The response shows the fields you will build on.
1curl -X POST https://api.locals.money/v1/routes \ 2 -H "Authorization: Bearer $LOCALS_KEY" \ 3 -H "Idempotency-Key: $(uuidgen)" \ 4 -d '{ "customerId": "cus_8Hq2Lm", "source": { "currency": "AUD" }, "destination": { "currency": "USDC", "chain": "polygon" } }'
{
"id": "route_3f9a",
"status": "pending"
}
Know the moment money moves.
Each event is signed and carries an id. Verify, store, then act. Retries use the same id, so processing twice is safe.
customer.verification.updated
approved
requires_action
rejected
route.activated
active
route.deposit.receivedon every deposit
transfer.processing
processing
transfer.completed · failed
complete
failed
import { createHmac, timingSafeEqual } from 'crypto'; // Body must be the raw request bytes export function verify(rawBody, header, secret) { const [ts, sig] = header.split(','); const expected = createHmac('sha256', secret) .update(`${ts}.${rawBody}`) .digest(); return timingSafeEqual(expected, Buffer.from(sig, 'hex')); }
import hashlib, hmac # raw_body must be the raw request bytes def verify(raw_body: bytes, header: str, secret: str) -> bool: ts, sig = header.split(",") expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig)
-
Verify the signatureOver the raw body, before you parse it or credit anything.
-
Store the event idEvents can arrive more than once. Retries carry the same id.
-
Then actUpdate your ledger and answer 200.
Errors that tell you what to do.
Every error has a stable code, a message for humans and, on validation errors, the fields that failed.
{
"statusCode": 422,
"code": "currency_not_enabled",
"message": "AED accounts are not enabled for this customer"
}
Every status. No real money.
Sandbox keys give you the full API. Send a test bank transfer to a route and watch it arrive, convert and settle, with every webhook along the way.
-
Simulated bank transfersOne call on any sandbox route.
-
Real webhooks, signedThe same events and payloads as production.
-
Same code in productionSwap the key. Nothing else changes.
POST /v1/test/routes/route_3f9a/simulate { "amount": "12500.00" }
-
route.deposit.received09:41:07 · ref TEST-0001A$12,500.00
-
transfer.processingtrf_91kd · 1 AUD = 0.6510 USDC8,137.50 USDC
-
transfer.completedtx 0x5e0c…a7d4Completed
What changed, and when.
Breaking changes ship as a new version. Additions land in v1 and are listed here, together with the network updates behind accounts, payouts and cards, marked Network.
-
AddedPayouts
One transfer per quote.
Retrying a transfer with the same quote returns the transfer already created, also after a timeout or a lost response. A quote reused for another customer or payout account is refused with
409.POST /v1/transfers -
LiveAccounts
AUD virtual accounts live.
Routes with source AUD run in production. Australian accounts accept deposits from one allow-listed sending account per route; money from any other account is returned to the sender.
POST /v1/routes · POST /v1/routes/{id}/whitelist -
AddedAccounts
Customers from an existing verification.
Verified individuals become customers without repeating their checks. In the first production cases, approval came back in under a minute.
POST /v1/customers -
AddedPayouts
Quoted transfers.
New
kind: "quoted"transfers for payouts: quote, accept, fund the exact amount before it expires. Deposits and payouts share one status model.POST /v1/quotes · POST /v1/transfers -
NetworkCards
Real-time card funding (beta).
Cards can spend straight from the cardholder’s wallet: only the amount of each purchase is committed when it is authorised. No separate card balance to fund or top up, and the approval can be revoked at any time.
-
NetworkCards
Card programmes in Korean won.
Card programmes can be denominated in won, backed by a won stablecoin, on the same issuing infrastructure.
-
NetworkCards
Scoped cards.
Rules set before a card is issued: a limit per transaction, allowed merchants or categories, and an expiry. A card can close after one purchase, run until a fixed budget is spent, or renew with a recurring budget.
-
NetworkCards
Rewards on card spend.
A points programme under your brand: you set the earning rules and merchant campaigns, cardholders redeem inside your app.
-
NetworkAccounts
EUR and GBP rails.
The banking network now converts EUR and GBP to and from stablecoins, and sends and receives USD worldwide over SWIFT. It is the groundwork for EUR and GBP accounts, coming soon.
-
NetworkCards
Spend controls for AI agents (beta).
Per-agent limits on amounts, merchants, categories and time windows, enforced before a transaction is processed, plus programme-wide caps on active cards and total spend.
-
NetworkPayouts
USDT across more than 20 chains.
USDT now moves natively across more than 20 chains, without wrapped tokens or bridges, in both collections and payouts.
-
NetworkPayouts
Dedicated settlement for Asian corridors.
A dedicated settlement partner backs the USDT corridors across Asia, adding liquidity and redundancy so payouts keep settling through high-volume and volatile periods.
-
NetworkPayouts
Institutional banking rails in Asia.
Conversions between local currencies and stablecoins in key Asian markets can run on institutional banking infrastructure, not only on USDT liquidity.
-
NetworkPayouts
Liquidity for fiat and USDT corridors.
A dedicated liquidity layer serves the fiat-to-USDT corridors, focused on Asia: high-volume payouts settle faster and conversions keep running when markets are volatile.
Go local in every market you serve.
Tell us where your customers are. We will tell you what is live, what is next, and how fast you can launch.