Developers
Money movement, at the altitude of a REST call.
Five calls take you end to end: create a user, fund a wallet, check the balance, send money, read the history. Ten minutes with a sandbox key. Sandbox keys move no real money.
No SDK lock-in
The API is plain HTTP + JSON. Generate a typed client from the published OpenAPI 3.0.3 spec in your language, or just use your HTTP library.
Idempotent by default
Every money-moving call takes an idempotency key. Retries are safe; the ledger replays the stored result byte-for-byte.
Events you can trust
Webhooks are HMAC-SHA256 signed with a replay window, delivered at least once with dedupe ids, on a worker pool isolated from money paths.
Scoped credentials
Tenant API keys for server-to-server, short-lived signed user tokens for client calls, staff tokens for operations, verified on every request.
Fund a wallet, then send money
Pick your language.
The same request in six languages. Amounts are integer minor units; the ledger does the rest.
# Fund a wallet, then send money. Balances only move on the ledger.
curl -sS -X POST "$WALLETD_API/v1/users" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: usr-42-create" \
-d '{"external_id":"user-42","kind":"consumer","display_name":"Ada Lovelace","handle":"ada"}'
curl -sS -X POST "$WALLETD_API/v1/transfers" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: xfer-ada-grace-001" \
-d '{"from":"user-42","to":"@grace","amount":2500,"currency":"USD"}'client := walletd.New(os.Getenv("WALLETD_API"), os.Getenv("WALLETD_API_KEY"))
var transfer walletd.Transfer
err := client.Do(ctx, http.MethodPost, "/v1/transfers", map[string]any{
"from": "user-42",
"to": "@grace",
"amount": 2500, // minor units, $25.00, never a float
"currency": "USD",
}, &transfer, "xfer-ada-grace-001") // idempotency key
if err != nil {
return fmt.Errorf("transfer: %w", err)
}client = WalletD() # reads WALLETD_API / WALLETD_API_KEY
transfer = client.request(
"POST",
"/v1/transfers",
{
"from": "user-42",
"to": "@grace",
"amount": 2500, # minor units, $25.00
"currency": "USD",
},
idempotency_key="xfer-ada-grace-001",
)
print(transfer["status"]) # "settled"const res = await fetch(`${API}/v1/transfers`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "xfer-ada-grace-001",
},
body: JSON.stringify({
from: "user-42",
to: "@grace",
amount: 2500, // minor units, $25.00
currency: "USD",
}),
});
const transfer = await res.json();$transfer = $walletd->request('POST', '/v1/transfers', [
'from' => 'user-42',
'to' => '@grace',
'amount' => 2500, // minor units, $25.00
'currency' => 'USD',
], idempotencyKey: 'xfer-ada-grace-001');
echo $transfer['status']; // "settled"var body = Map.of(
"from", "user-42",
"to", "@grace",
"amount", 2500, // minor units, $25.00
"currency", "USD");
Transfer transfer = client.post(
"/v1/transfers", body, "xfer-ada-grace-001", Transfer.class);Webhooks
Verify in a few lines.
Each event is signed. Reject anything that does not verify in constant time, and dedupe on the event id. Receiving the same event twice is expected and safe.
All event types// Verify an incoming webhook, HMAC-SHA256, constant-time, 5-minute replay window.
const signed = `${timestamp}.${rawBody}`;
const expected = hmacSha256(endpointSecret, signed);
if (!timingSafeEqual(expected, signatureFromHeader)) {
return res.status(400).send("bad signature");
}
// Idempotent by X-Wallet-Event-Id, safe to receive the same event twice.Early access
Get a sandbox key.
Design partners get a sandbox tenant, the OpenAPI spec, and a direct line to the engineers who built it.