v1 · Documentation

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.

Not a public payments API. Denaro is internal infrastructure for the DDG portfolio. Access is granted per service after a compliance and integration review — request access.

Access & onboarding

Onboarding a new portfolio service is a four-step process:

  1. Request access via /contact with your service, expected volume, and integration shape.
  2. Compliance review — we confirm the service is approved within DDG and that your data-handling posture matches the rail you need.
  3. Sandbox provisioning — you receive a service identifier and a scoped API key pair for the sandbox environment.
  4. 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

EnvironmentHostKey prefixSettles?
sandboxapi.sandbox.denaro.moneydn_test_sk_…Simulated only
liveapi.denaro.moneydn_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.
Pick keys you can reproduce. A good idempotency key ties to the upstream event you're reacting to (an order id, a webhook delivery id) so a retry — even from a different process — lands on the same key.

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.

FieldTypeNotes
payout_idstringStable identifier, prefix po_.
amountintegerNet, in cents, after fees.
arrival_datedateExpected funds-available date.
statestringpending, 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

EventFires when…
charge.authorizedFunds are reserved on the customer's source.
charge.capturedAuthorization is captured to settle.
charge.settledProcessor confirms funds have settled.
charge.failedCharge cannot be completed.
refund.createdA refund has been issued.
refund.settledRefund settlement is confirmed.
payout.paidPayout has landed in the destination account.
payout.failedPayout 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
Always verify the raw bytes — deserialize-then-reserialize will change whitespace and break the signature. Capture the body before any middleware reshapes it.

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.