Developer docs · /v1

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.