Reporting delivery
Deliver payment observations reliably with a durable outbox, stable event identities, and provider-independent retries.
On this page
Make delivery a backend responsibility
The merchant backend knows whether it attempted a Stripe or Square payment and what the provider returned. Carden only knows reports it receives. A process crash, network interruption, or key error can interrupt delivery even when payment succeeded.
A transactional outbox stores immutable events beside the merchant's payment-state changes. A separate worker claims unacknowledged events and records Carden acknowledgments. A durable queue can serve the same purpose when its database-to-queue failure boundary is handled.
Separate attempts from observations
| Value | Lifetime | Use |
|---|---|---|
| attemptId | One actual payment attempt | Authorization, capture, refund, cancellation, and failure observations reuse it. |
| eventId | One immutable observation | Delivery retry keeps ID and body; later state gets a new event. |
| providerEventId | One provider event | Links a report to the Stripe or Square event verified by the merchant. |
| enrichmentRequestId | One preparation | Use preparation.id for linked source, request ID for inline when available, or omit for reporting only. |
Deduplication is scoped to organization, integration, and environment. Choose identities that survive restarts and rotation. A duplicate acknowledgment never updates accepted payload.
Save preparation.id, snapshotId, revision, and diagnostic request ID before provider execution. A later source edit or disconnect cannot cause the report worker to re-read the invoice or discard an observed outcome.
Implement the outbox lifecycle
- Persist attempt identity and provider idempotency before payment.
- Execute the provider operation independently of Carden availability.
- Persist actual outcome and immutable report in one local transaction where possible; keep ambiguity unknown.
- Have a worker claim and deliver pending rows with a scoped Carden key.
- Acknowledge only a valid success response, including duplicate: true.
- Retain attempts, next retry time, and safe failure reason; alert on permanent failures and aging backlog.
const response = await fetch(new URL("/api/v1/stripe/payment-reports", baseUrl), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(report),
signal: AbortSignal.timeout(5000),
});
const requestId = response.headers.get("x-carden-request-id");
const payload = await response.json();
if (!response.ok) {
throw new Error(`Carden report failed: ${response.status} (${requestId})`);
}
if (payload?.object !== "stripe.payment_report" ||
payload.api_version !== "v1" ||
typeof payload.data?.id !== "string" ||
typeof payload.data?.duplicate !== "boolean") {
throw new Error("Unexpected Carden report response");
}
await reportOutbox.markDelivered(report.eventId, {
reportId: payload.data.id,
requestId,
});Classify delivery failures
| Result | Action |
|---|---|
| Valid 2xx | Acknowledge; duplicate is delivered. |
| Timeout / connection | Keep pending and retry same event because the server may have accepted it. |
| 408 / 429 / 5xx | Retry with bounded exponential backoff and jitter; honor Retry-After when present. |
| 401 / 403 | Pause affected delivery and repair credentials or permissions. |
| 400 / 422 | Quarantine for factual correction; do not loop indefinitely. |
| 409 | Reconcile immutable identity and known cumulative facts. |
| Malformed success | Keep pending for investigation rather than silently discarding it. |
A retry ceiling routes an event to alert or operational review, never deletion. Monitor pending count, oldest age, permanent rejection, and provider-specific backlog.
Reconcile later and out-of-order states
Merchant webhook handlers verify provider signatures, correlate attempts, and normalize actual state before saving another event with source: merchant_webhook. Use occurredAt for actual provider event time and cumulative amounts.
Reconcile out-of-order events against current provider state rather than letting an older observation erase known capture or refund. Unknown is neither success nor failure and remains until provider evidence resolves it.
Test the failure boundaries
- Crash after the provider accepts a request but before local commit.
- Drop the HTTP response after Carden accepts a report and confirm duplicate acknowledgment.
- Deliver the same event concurrently from two workers.
- Rotate a Carden key and resume old events without changing IDs.
- Deliver provider capture and refund events out of order.
- Keep Carden unavailable while provider payments continue and verify no extra payment operation.