DigitalTreasury API
A single REST API for payment operations. All money is in cents. All routes are versioned under /v1.
Quick start
# 1. Authenticate every request with your tenant API key curl https://api.digitaltreasury.com/v1/balances \ -H "X-API-Key: dt_live_..."
Authentication
Pass your secret key in the X-API-Key header. Keys are tenant-scoped — DigitalTreasury resolves your tenant, applies isolation, and rejects requests for inactive tenants with 401.
Endpoint reference
Payment Orders
POST /v1/payment-orders GET /v1/payment-orders GET /v1/payment-orders/:id POST /v1/payment-orders/:id/approve POST /v1/payment-orders/:id/cancel
Ledger
POST /v1/ledger-accounts GET /v1/ledger-accounts GET /v1/ledger-accounts/:id GET /v1/ledger-accounts/:id/entries
Virtual Accounts
POST /v1/virtual-accounts GET /v1/virtual-accounts GET /v1/virtual-accounts/:id
Counterparties
POST /v1/counterparties GET /v1/counterparties GET /v1/counterparties/:id PUT /v1/counterparties/:id
Balances
GET /v1/balances
Webhooks
GET /v1/webhooks GET /v1/webhooks/:id POST /v1/webhooks/:id/retry
Code snippets
cURL
curl -X POST .../v1/payment-orders \ -H "X-API-Key: dt_live_..." \ -H "Content-Type: application/json" \ -d '{ "externalId":"inv_1", "direction":"Credit", "rail":"Ach", "amountCents":250000, "counterpartyName":"Acme", "counterpartyAccountNumber":"123", "counterpartyRoutingNumber":"021000021" }'
C#
var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-API-Key", key); await http.PostAsJsonAsync( "/v1/payment-orders", order);
Python
import requests requests.post(url, headers={"X-API-Key": key}, json=order)
Webhook event types
payment_order.pending payment_order.completed payment_order.failed payment_order.returned payment_order.cancelled fuel_tax.remittance_cleared fuel_tax.remittance_returned
Every delivery is signed with HMAC-SHA256 in the X-DigitalTreasury-Signature header — see Verifying webhooks below.
Verifying webhooks
Every webhook we deliver is signed so your endpoint can prove it came from DigitalTreasury and was not altered in transit. Verify the signature before you trust the body.
The signature
We compute HMAC-SHA256 over the raw request body, keyed with your endpoint's signing secret, and send the lowercase hex digest in a header:
X-DigitalTreasury-Signature: 9f86d0818884...
Recompute it with your secret over the bytes exactly as received, and compare in constant time. Parse the JSON only after the signature checks out — re-serializing first can change a byte and break the match.
During a secret rotation the header carries several signatures, comma-separated — one per active secret. Accept the delivery if any of them matches yours.
The event envelope
{ "id": "1f4c…",
"type": "payment_order.completed",
"created_at": "2026-07-17T…Z",
"data": { /* the event */ } }
Deliveries are at-least-once: a retry or a replay reuses the same id, so deduplicate on it and make your handler idempotent.
Verify · Node.js
const crypto = require('crypto'); function verify(rawBody, header, secret) { const a = Buffer.from(crypto .createHmac('sha256', secret) .update(rawBody, 'utf8').digest('hex')); // header may carry several signatures during a rotation return header.split(',').some(sig => { const b = Buffer.from(sig.trim()); return a.length === b.length && crypto.timingSafeEqual(a, b); }); }
Verify · Python
import hmac, hashlib def verify(raw_body, header, secret): expected = hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() # any signature in the header may match during a rotation return any(hmac.compare_digest(expected, s.strip()) for s in header.split(","))
Verify · C#
using System.Security.Cryptography; bool Verify(byte[] body, string header, string secret) { using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var expected = Convert.ToHexString(h.ComputeHash(body)).ToLowerInvariant(); return header.Split(',').Any(sig => CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(sig.Trim()))); }
Delivery & retries
Respond 2xx to acknowledge. Anything else — including your own rejection of a bad signature with 401 — is retried with backoff (1m, 5m, 30m) and dead-lettered after three attempts. Dead-lettered events can be replayed from the dashboard.
Sign your own test event with the Send test event button to confirm your endpoint before going live.
Rotating a secret is lossless: for an overlap window (24h by default) after a rotation, every delivery is signed with both the new secret and the old one — that is the comma-separated header above — so a receiver still holding either keeps verifying. Install the new secret on your endpoint during the window, then let it close, or retire the old one immediately from tenant settings.