# DuckLedger integration context > DuckLedger sends signed financial events to SaaS products and exposes a permission-scoped API for server-to-server automation. ## Canonical endpoints - Web application: `https://duckledger.com` - Developer documentation: `https://duckledger.com/developers` - Interactive API reference: `https://duckledger.com/developers/reference` - OpenAPI 3.1 contract: `https://duckledger.com/openapi.json` - Webhook guide: `https://duckledger.com/developers/guides/webhooks` - Production API: `https://api.duckledger.com/api/v1` - LLM index: `https://duckledger.com/llms.txt` - Webhook debugger: `https://duckledger.com/webhooks/logs` (authenticated) ## API parity status The OpenAPI contract and developer endpoint matrix map 208 Asaas reference operations across 33 capability groups. `x-duckledger-status: available` means the DuckLedger equivalent is callable today. `x-duckledger-status: planned` is a documented parity target and must not be called yet. The generated inventory currently contains 21 available equivalents and 187 planned operations. ## Outbound webhook setup 1. Open Developer → Webhooks → Add endpoint. 2. Enter the receiver's final public HTTPS URL. 3. Optionally scope it to one workspace and select event filters. 4. Store the returned `whsec_...` signing secret. It is shown only once. 5. Verify signatures over the exact raw request body before parsing JSON. 6. Store `event.id` as an idempotency key, enqueue work, and return 2xx quickly. 7. Send a `ping` with the Test action and inspect Developer → Webhook Logs. Registration is also available at `POST /api/v1/webhook-endpoints`: ```json { "url": "https://product.example/webhooks/duckledger", "workspaceId": "97d7c188-e37a-4897-a719-5537cce78af8", "description": "Production receiver", "subscribedEvents": ["payment.received", "payment.refunded"] } ``` Omit `workspaceId` for the whole organization. An empty `subscribedEvents` array receives all events. ## Webhook request contract ```http POST /webhooks/duckledger HTTP/1.1 Content-Type: application/json User-Agent: DuckLedger-Webhooks/1.0 X-DuckLedger-Event: payment.received X-DuckLedger-Delivery: 12465f62-8adf-4c7a-8be9-e84152c029e5 X-DuckLedger-Signature: t=1753208591,v1=6f1c… ``` ```json { "id": "12465f62-8adf-4c7a-8be9-e84152c029e5", "type": "payment.received", "createdAt": "2026-07-22T17:43:11.000Z", "data": { "paymentId": "f7d6a6bc-b9dd-4f59-862d-07a884c9aca3", "externalReference": "pay_order_1842", "customerId": "fb16c0a2-b8f1-4a5f-8ff6-49e58a660e1f", "workspaceId": "97d7c188-e37a-4897-a719-5537cce78af8", "status": "RECEIVED", "amount": "299.00", "netAmount": "290.80", "feeAmount": "8.20", "currency": "BRL", "paidAt": "2026-07-22T17:42:58.000Z", "billingType": "PIX" } } ``` Money values are decimal strings. The payload is self-contained. ## Signature verification Header: `t=,v1=`. Signing material: `.`. Required checks: - Read raw bytes before JSON parsing or body transformations. - Reject a timestamp more than 300 seconds from the receiver clock. - Compute HMAC-SHA256 with the current endpoint `whsec_...` secret. - Compare digests in constant time. ```ts import { createHmac, timingSafeEqual } from 'node:crypto'; export function verifyDuckLedgerSignature( rawBody: string, signatureHeader: string | undefined, signingSecret: string, ): boolean { if (!signatureHeader) return false; const parts = Object.fromEntries( signatureHeader.split(',').map((segment) => { const [key, ...value] = segment.trim().split('='); return [key, value.join('=')]; }), ); const timestamp = Number(parts.t); if (!Number.isFinite(timestamp) || !parts.v1) return false; if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const expected = createHmac('sha256', signingSecret) .update(`${timestamp}.${rawBody}`) .digest('hex'); const provided = Buffer.from(parts.v1, 'utf8'); const expectedBytes = Buffer.from(expected, 'utf8'); return provided.length === expectedBytes.length && timingSafeEqual(provided, expectedBytes); } ``` Next.js App Router: ```ts export async function POST(request: Request) { const rawBody = await request.text(); const signature = request.headers.get('x-duckledger-signature') ?? undefined; if (!verifyDuckLedgerSignature( rawBody, signature, process.env.DUCKLEDGER_WEBHOOK_SECRET!, )) { return Response.json({ error: 'invalid_signature' }, { status: 401 }); } const event = JSON.parse(rawBody); await enqueueIdempotently(event.id, event); return Response.json({ received: true }); } ``` Express: ```ts app.post( '/webhooks/duckledger', express.raw({ type: 'application/json' }), async (request, response) => { const rawBody = request.body.toString('utf8'); const signature = request.header('x-duckledger-signature'); if (!verifyDuckLedgerSignature( rawBody, signature, process.env.DUCKLEDGER_WEBHOOK_SECRET!, )) { return response.status(401).json({ error: 'invalid_signature' }); } const event = JSON.parse(rawBody); await enqueueIdempotently(event.id, event); return response.status(200).json({ received: true }); }, ); ``` Python: ```python import hashlib import hmac import time def verify_duckledger_signature(raw_body, signature_header, signing_secret): if not signature_header: return False parts = dict( segment.strip().split("=", 1) for segment in signature_header.split(",") if "=" in segment ) try: timestamp = int(parts["t"]) provided = parts["v1"] except (KeyError, ValueError): return False if abs(time.time() - timestamp) > 300: return False signed = str(timestamp).encode() + b"." + raw_body expected = hmac.new(signing_secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(provided, expected) ``` ## Event catalog - `payment.created`: a charge was created and is awaiting payment. - `payment.updated`: a charge changed. - `payment.authorized`: a card charge was authorized. - `payment.confirmed`: the provider confirmed payment but settlement is incomplete. - `payment.received`: funds were received; use this event to unlock paid access. - `payment.overdue`: the due date passed without payment. - `payment.refunded`: a payment was fully refunded. - `payment.partially_refunded`: part of a payment was refunded. - `payment.refund_in_progress`: the provider is processing a refund. - `payment.refund_denied`: the provider denied a refund. - `payment.chargeback_requested`: a cardholder requested a chargeback. - `payment.chargeback_dispute`: a chargeback entered dispute. - `payment.canceled`: a charge was canceled. - `payment.failed`: a charge failed. - `subscription.created`: a subscription was created. - `subscription.updated`: a subscription changed. - `subscription.inactivated`: a subscription became inactive. - `subscription.canceled`: a subscription was canceled. - `asaas.*`: all other Asaas event families and future provider events, forwarded losslessly. - `ping`: a synthetic event from the endpoint Test action. ## Delivery semantics - At-least-once delivery; duplicate events are expected. - Any 2xx response marks success. - Non-2xx and network failures are retried up to six attempts with exponential backoff beginning at 10 seconds. - Delivery timeout is 10 seconds. - Redirects are not followed. - An endpoint is disabled after 15 consecutive failures. - URLs are checked against private, loopback, and link-local targets during registration and before each delivery. Persist the envelope `id` or `X-DuckLedger-Delivery` as a unique idempotency key. Return 2xx before slow business logic. ## HTTP API Base URL: `https://api.duckledger.com/api/v1`. Machine authentication: ```http Authorization: Bearer dlk_test_. # production: dlk_live_. ``` Generate keys in Developer → API Keys. Keys own their environment, organization, optional workspace, and permission subset. Test keys can only route to the sandbox Asaas account; live keys can only route to production. DuckLedger ignores `x-organization-id` for API-key requests. The full key is returned once when created. Keep it server-side. ### Billing resources - `POST/GET /customers`: create and list product customers. - `POST/GET /plans`: create reusable billing plans. - `POST /payments`: create a PIX, boleto, or credit-card charge. - `GET /payments/:id/pix`: retrieve QR image and PIX copy-paste data. - `GET /payments/:id/boleto`: retrieve digitable line, barcode, and boleto URL. - `POST /payment-methods/tokenize`: exchange card data for an encrypted provider token. - `GET /payment-methods?customerId=`: list safe card display metadata. - `POST/GET /subscriptions`: create a subscription from a `planId` or explicit terms. - `POST /payments/:id/refund`: full or partial refund. - `POST /payments/:id/cancel`: cancel a charge. Asaas has no standalone plan resource. DuckLedger plans are product templates materialized as Asaas subscriptions. Customers have distinct Asaas IDs in Sandbox and Production. Create a sandbox PIX charge: ```bash curl -X POST https://api.duckledger.com/api/v1/payments \ -H "Authorization: Bearer $DUCKLEDGER_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1842_pix" \ -d '{"customerId":"","billingType":"PIX","amount":"99.90","dueDate":"2026-07-30"}' ``` Success: ```json { "data": {}, "meta": { "requestId": "..." } } ``` Error: ```json { "error": { "code": "SOME_ERROR_CODE", "message": "Human-readable", "details": {}, "requestId": "..." } } ``` Pagination uses `page` and `pageSize`, maximum 100. IDs are UUIDs. Timestamps are ISO-8601 UTC. Money uses decimal strings. ## Debugging Developer → Webhook Logs stores inbound gateway webhooks and outbound product deliveries. It includes sanitized request and response headers and bodies, status, duration, attempt, source, endpoint, event type, and correlation identifiers. Authorization, cookie, API-key, and signature secrets are redacted. Typical failures: - `401` or `403`: verify the exact raw body with the current signing secret and check clock skew. - `404`: confirm the deployed path matches the registered URL. - `307` or `308`: register the final URL; redirects are not followed. - `429`: acknowledge quickly and queue work locally. - `5xx`: inspect receiver logs, fix the error, then use Test or await a retry. - timeout: DuckLedger waits 10 seconds; return 2xx before slow processing. - duplicate side effects: add a unique constraint for `event.id`. - no deliveries: confirm endpoint status, event filters, and workspace scope. ## MCP The repository includes `@duckledger/mcp`, a local read-only stdio server. It does not call the production API and does not handle credentials. Build: ```bash pnpm install pnpm --filter @duckledger/mcp build ``` Portable client configuration: ```json { "mcpServers": { "duckledger": { "command": "node", "args": ["/absolute/path/to/duckledger/apps/mcp/dist/index.js"] } } } ``` Resources: - `duckledger://docs/quickstart` - `duckledger://docs/webhooks` - `duckledger://docs/api` - `duckledger://docs/debugging` Tools: - `duckledger_list_webhook_events` - `duckledger_signature_example` - `duckledger_webhook_handler_example` - `duckledger_debug_webhook` - `duckledger_mcp_config` Prompt: - `integrate-duckledger-webhooks` ## Production checklist - Test `ping`. - Test a changed body, bad secret, stale timestamp, and missing signature. - Deliver the same event twice and confirm side effects happen once. - Return 2xx before slow work. - Register a URL that does not redirect. - Alert on repeated non-2xx responses and local queue failures. - Keep secrets server-side and define a rotation procedure.