Subscribe
secret field is the only place the signing secret ever appears):
secret immediately — Caratuva encrypts it at rest and cannot return it again. If you lose it, rotate it.
Event types
Two parallel namespaces:payment_intent.*— fires only for invoices created viaPOST /v1/payment-intents. Subscribe to these if you’re a B2B integrator. You will not see noise from any dashboard activity.invoice.*— fires for every invoice (both API-created and dashboard-created). Subscribe to these only if you also operate the dashboard.
payment_intent.* events:
| Event | When |
|---|---|
payment_intent.created | Intent created via API; buyer not yet engaged. |
payment_intent.buyer_kyc_approved | Buyer’s KYC submission cleared identity verification. |
payment_intent.buyer_kyc_rejected | KYC rejected. Intent terminates at failed. |
payment_intent.on_chain_confirmed | Cross-border transfer confirmed on Caratuva’s settlement layer. |
payment_intent.settled | PIX payout completed; funds in the seller’s BRL account. Terminal happy state. |
payment_intent.failed | Terminal failure (any phase). data carries the reason. |
payment_intent.cancelled | Cancelled via POST /v1/payment-intents/:id/cancel or by ops. |
payment_intent.expired | expiresAt reached without payment. |
Delivery format
Signature verification
The signature header ist=<unix_ts>,v1=<sha256_hex>. The signed payload is ${t}.${rawBody} (the raw HTTP body bytes, not a re-serialized JSON object). Always verify against the raw bytes — re-serializing changes whitespace and key order and breaks the MAC.
Steps
- Parse
tandv1from theX-Caratuva-Signatureheader. - Reject the request if
|now - t| > 300(5-minute replay window). - Compute
expected = HMAC-SHA256(secret, "${t}.${rawBody}")and compare hex-encoded againstv1with a constant-time comparator. - Use
X-Caratuva-Delivery-Idas an idempotency key on your side — replays with the same id should be no-ops.
Node.js example
Python example
Retries and backoff
Caratuva attempts immediate delivery, then retries on any non-2xx or transport error with exponential backoff:| Attempt | Delay since previous |
|---|---|
| 1 | (immediate) |
| 2 | 30s |
| 3 | 2m |
| 4 | 10m |
| 5 | 1h |
| 6 | 6h |
| 7 | 24h |
dead_letter and Caratuva stops retrying. Use GET /v1/webhook-subscriptions/:id/deliveries to inspect failed deliveries and replay them manually if needed.
List subscriptions
Inspect deliveries
attempt, status (pending | delivered | failed | dead_letter), the upstream responseStatus, and the next nextAttemptAt. In practice the values you’ll see today are pending, delivered, and dead_letter — a row stays pending between retries and flips to dead_letter after the final attempt; failed is reserved.
Rotate the secret
secret exactly once. Rotation is immediate and atomic — the API stores only one secret per subscription, so the old secret stops verifying on the very next delivery and there is no dual-secret overlap window. Deploy the new secret to your verifier first (or atomically with the rotate call); any in-flight or retried deliveries are re-signed with the new secret when they’re sent. Don’t configure your verifier to accept “either” secret expecting an overlap — there isn’t one.
Delete (deactivate)
active=false). No further deliveries fire; historical deliveries rows remain queryable.