WalletD developers

Authentication

API keys, wallet user tokens and IdP tokens: which credential you send decides both who you are and what you may do.

Every request carries a bearer credential. WalletD verifies it itself, on every call. It never trusts an identity header from a proxy, so nothing in front of the API can vouch for a caller.

Authorization: Bearer <credential>

Three kinds of credential exist. Which one you send decides both who you are and what you may do.

The three credentials

Tenant API key

sk_live_01H8X...ab_9f2c... — issued to your backend, acts for your whole tenant. Format is sk_{env}_{key-id}_{secret}; the env segment is live or sandbox, and the key id lets WalletD find the record without scanning. Only the Argon2id hash is stored, so the secret is shown exactly once at creation. If you lose it, rotate it.

Use it from a server you control.

Never ship a tenant API key to a browser, a mobile app, or anything a user can decompile. It acts for your whole tenant. An app that needs to act for one person holds a wallet user token instead.

Wallet user token

A short-lived RS256 JWT that acts for exactly one user. Your backend mints one by exchanging its API key:

curl -X POST https://api.walletd.example/v1/auth/user_tokens \
  -H "Authorization: Bearer $WALLETD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_external_id": "your-user-42",
    "scopes": ["balances:read", "users:read", "transfers:write"],
    "ttl_seconds": 900
  }'

The token can only ever act on that user's own wallet, and it cannot request a scope your key does not already hold (you get 403 scope_exceeds_key). Keep the TTL short; 15 minutes is the default for a reason. This is the credential your app holds.

IdP access token

If your users sign in through the WalletD identity provider, their access token is their wallet credential. There is no exchange step and no second account system: WalletD verifies the token against the realm's public keys and provisions the wallet on first contact. Consumers get a fixed scope set decided by the platform, not by the token.

Scopes

A scope is resource:action. The credential carries a set; a handler refuses anything outside it with 403 insufficient_scope.

ScopeGrants
users:read, users:writeRead and create wallet users
balances:readRead balances
transfers:writeSend P2P transfers
topups:read, topups:writeRead and start top-ups
payments:read, payments:writeRead, create, capture and void payments
refunds:writeRefund a payment
merchants:read, merchants:writeRead and register merchants
subscriptions:read, subscriptions:writeRead and manage subscriptions
rewards:read, rewards:manage, rewards:convertRead rewards, manage rules, convert points
webhooks:manageManage endpoints and redelivery
ledger:read, audit:readLedger summary and audit log
fees:manage, limits:manage, credit:writeFee plans, tier limits, credit lines
catalog:read, clients:onboardEcosystem discovery and self-serve onboarding

Two rules that surprise people:

  • A user token is confined to its own user regardless of scope. balances:read on a user token does not let it read another user's balance. Self-access is a separate check from the scope check.
  • Some surfaces refuse user tokens entirely, no matter the scope. Credit lines and tier configuration are API-key-or-staff only, because a user must never be able to raise their own limits.

A client, once, in each language

The rest of this guide assumes a small client that does four things: sets the bearer token, sets Content-Type, attaches an Idempotency-Key on money-moving calls, and turns a problem response into an error you can catch. Write it once.

export WALLETD_API="https://api.walletd.example"
export WALLETD_API_KEY="sk_sandbox_..."

# Every example in this guide uses these two variables.
curl -sS "$WALLETD_API/v1/users" -H "Authorization: Bearer $WALLETD_API_KEY"

Keeping keys safe

  • Keep keys in a secret manager or environment, never in source control. A leaked key can move your tenant's money.
  • Use sk_sandbox_... keys everywhere but production. The environment segment is checked against the key record; it does not partition data within a deployment. Use a separate non-production deployment for sandbox work.
  • Issuance and revocation are audited. When something looks wrong, the audit log answers who issued what and when.

An API key expires 365 days after it is issued. The expiry is set at issuance. The key-list response includes created_at, expires_at and last_used_at; the create response does not include expiry. Read the listing to schedule rotation before expires_at, and handle a null expiry on older records explicitly. No automatic expiry notification is established by this API. Rotate before expiry to avoid 401 unauthenticated responses.

Rotation is the same three steps whether you are rotating on schedule or because a key leaked, and the order matters, because issuing a new key does not invalidate the old one:

  1. Create a second key with the same scopes.
  2. Deploy it everywhere the old one is used, and confirm traffic has moved.
  3. Revoke the old key.

Revoking first gives you an outage; revoking last gives you an overlap you control. authsvc and walletd each have a 60-second positive cache. Revocation clears only the receiving authsvc process's cache, so replicas can extend stale acceptance toward two minutes plus request latency. Verify rejection of the old key after that allowance; see Security for integrators.

Next

  • Idempotency, which is the other half of a correct client.
  • Errors for the codes these clients raise.

On this page