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.