Denaro API
One ledger, one set of rails, one place to audit. These docs cover the public surface of the Denaro API used by approved DDG portfolio services.
Overview
Denaro exposes a small set of resources — charges,
refunds, payouts — plus an append-only
ledger that every state transition writes to. Every
mutation is idempotent, every webhook is signed, and every dollar
moved is traceable end-to-end through a request id.
Access & onboarding
Onboarding a new portfolio service is a four-step process:
- Request access via /contact with your service, expected volume, and integration shape.
- Compliance review — we confirm the service is approved within DDG and that your data-handling posture matches the rail you need.
- Sandbox provisioning — you receive a service identifier and a scoped API key pair for the
sandboxenvironment. - Production cutover — once your sandbox integration passes our smoke tests, we issue live keys and add your service to the routing table.
Authentication
All requests use HTTP Bearer authentication. Keys are scoped per service and per environment, and never carry permissions beyond the resources owned by that service.
Authorization: Bearer dn_live_sk_… Content-Type: application/json Denaro-Service: preceptor.network
Keys come in two halves — a publishable identifier
(safe to embed in server logs) and a secret (never logged,
never client-side). The Denaro-Service header is required
on every request and must match the service the key was issued to.
Environments
| Environment | Host | Key prefix | Settles? |
|---|---|---|---|
sandbox | api.sandbox.denaro.money | dn_test_sk_… | Simulated only |
live | api.denaro.money | dn_live_sk_… | Real settlement |
Idempotency
Every mutating request (POST, DELETE) requires
an Idempotency-Key header. Denaro stores the response for
24 hours; a retry with the same key returns the original response
unchanged, even if the underlying processor has since moved on.
POST /v1/charges Idempotency-Key: chg_2026-05-15_ab12cd # Retried within 24h with the same key → identical response, same charge_id. # Retried after 24h → treated as a new request.
Charges
A charge represents money pulled from a customer into
your service's balance. Charges progress through the states
authorized → captured → settled, with terminal failure
states declined and voided.
POST /v1/charges
POST https://api.denaro.money/v1/charges service: "preceptor.network" idempotency_key: "chg_2026-05-15_ab12cd" amount: 4900 # cents, USD currency: "usd" customer_ref: "pn_user_8421" descriptor: "PRECEPTOR · membership" source: "tok_…" 201 charge_id: "ch_01HVZQ…" ledger_id: "le_01HVZQ…" state: "authorized" events_url: "/v1/charges/ch_01HVZQ…/events"
GET /v1/charges/{id}
Returns the canonical state of a charge. Always reflects the latest ledger entry.
Refunds
Refunds are attached to a charge and inherit its ledger lineage. Partial refunds are supported; total refunded amount cannot exceed the captured amount.
POST https://api.denaro.money/v1/refunds service: "preceptor.network" idempotency_key: "ref_2026-05-16_xy09zz" charge_id: "ch_01HVZQ…" amount: 4900 # cents; omit for full refund reason: "customer_request" 201 refund_id: "rf_01HW01…" state: "pending"
Payouts
Payouts move funds out of a service's balance to its designated bank account on a fixed cadence. Manual payouts are available on request.
| Field | Type | Notes |
|---|---|---|
payout_id | string | Stable identifier, prefix po_. |
amount | integer | Net, in cents, after fees. |
arrival_date | date | Expected funds-available date. |
state | string | pending, in_transit, paid, failed. |
Ledger
The ledger is append-only and is the system of record for every state transition. Each entry has a content hash and a reference to its predecessor in the chain, so any tampering is detectable on read.
GET /v1/ledger?service=preceptor.network&cursor=le_01HVZQ… 200 entries: [ { id: "le_01HVZQ…", type: "charge.authorized", amount: 4900, prev_hash: "…", hash: "…" }, … ] next_cursor: "le_01HW01…"
Webhooks
Webhooks are POSTed to a single endpoint per service, with at-least-once delivery and exponential backoff (1s, 5s, 30s, 5m, 30m, 2h). A non-2xx response, or no response within 10s, triggers a retry.
Event types
| Event | Fires when… |
|---|---|
charge.authorized | Funds are reserved on the customer's source. |
charge.captured | Authorization is captured to settle. |
charge.settled | Processor confirms funds have settled. |
charge.failed | Charge cannot be completed. |
refund.created | A refund has been issued. |
refund.settled | Refund settlement is confirmed. |
payout.paid | Payout has landed in the destination account. |
payout.failed | Payout was returned or rejected. |
Signing & verification
Each webhook carries a Denaro-Signature header. The
signature is an HMAC-SHA256 over {timestamp}.{raw_body}
using the webhook signing secret for that service.
Denaro-Signature: t=1715800123,v1=9d3a… # Pseudo-verify: expected = hmac_sha256(secret, f"{t}.{raw_body}") assert hmac.compare_digest(expected, v1) assert abs(now - t) < 300 # reject >5min drift
Error shapes
Errors come back with a consistent envelope. Client errors
(4xx) are deterministic; server errors (5xx)
are safe to retry with the same Idempotency-Key.
402 error: { type: "card_declined", code: "insufficient_funds", message: "The card was declined for insufficient funds.", request_id: "req_01HW0G…" }
Rate limits
Default budget: 100 req/s per service, burst 200.
Limits apply per environment, not globally. Responses include
RateLimit-Remaining and RateLimit-Reset
headers; 429 responses include Retry-After.
Versioning
The API is versioned in the URL (/v1). Breaking changes
land in a new version; additive fields are released into the current
one with a four-week deprecation window for anything we ever remove.
Support
Integration questions go through your service's onboarding channel. Incident or production-impacting issues, page on-call directly. Don't have credentials yet? Request access.