Skip to documentation content

Reporting delivery

Deliver actual payment observations reliably with a durable outbox, stable event IDs, and independent retries.

On this page

Make delivery a backend responsibility

Your backend knows whether it attempted a Stripe payment and what Stripe returned. Carden only knows the reports it receives. A process crash, network interruption, or key error can interrupt that delivery even when the payment succeeded.

A transactional outbox is a database table that stores events alongside your own payment-state changes. A separate worker sends unacknowledged events and records acknowledgments. A persistent queue can serve the same purpose if you handle the database-to-queue failure boundary.

Separate attempts from observations

ValueLifetimeExample use
attemptIdOne actual payment attempt.Authorization, capture, and refund observations refer to the same attempt.
eventIdOne immutable report event.A retry keeps its ID and body; a later status gets a new event ID.
providerEventIdOne provider event when available.Connect the normalized report to the Stripe webhook your backend verified.
enrichmentRequestIdThe preparation request used for the attempt.Associate the actual payment with the commercial details prepared earlier.

Carden deduplicates eventId within organization, integration, and environment. It does not provide a global event namespace across merchants. Choose IDs stable enough to survive worker restarts and credential rotation. Do not generate a fresh random event ID inside each retry.

A duplicate acknowledgment does not update an accepted payload. If you need to report a new observation or correction, persist a new event and retain the relationship to the same attempt.

Implement the outbox lifecycle

  1. Before calling Stripe, persist the stable attempt identity and your own Stripe idempotency information.
  2. Execute the Stripe operation using your existing payment backend. Avoid coupling it to the availability of Carden's report endpoint.
  3. Persist the observed outcome and an immutable report event in one database transaction where possible. On ambiguous provider failures, persist unknown and schedule reconciliation.
  4. Have a worker claim pending outbox rows safely, send reports with a scoped key, and record the response ID and Carden request ID.
  5. Mark an event delivered only after a valid successful response, including duplicate: true.
  6. Retain failed rows with attempt count, next retry time, and a safe failure reason. Alert on permanent failures and aging backlogs.
Report-only worker request
// Run in a server worker. report is the immutable payload saved in your outbox.
// baseUrl is your Carden deployment origin; apiKey is a scoped Carden key.
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) {
  // Record status and requestId; retry this report or alert on permanent errors.
  // Never repeat the Stripe payment because this request failed.
  throw new Error(`Carden report failed: ${response.status} (${requestId})`);
}
if (payload?.object !== "stripe.payment_report" || payload.api_version !== "v1" ||
    typeof payload.data?.id !== "string" || !payload.data.id ||
    typeof payload.data?.duplicate !== "boolean") {
  throw new Error("Unexpected Carden report response; retain the outbox entry");
}
// Mark this outbox entry delivered, including when data.duplicate is true.
// Persist payload.data.id for reconciliation.

Classify delivery failures

ResultAction
Valid 2xx responseAcknowledge the event. A duplicate is also delivered.
Timeout or connection failureKeep the event pending. The server may have accepted it; retry the same ID and body.
5xx responseRetry the report with exponential backoff and jitter; alert if it persists.
429 response, if returnedHonor Retry-After when present; otherwise use bounded backoff.
401 or 403 responsePause affected delivery and repair credentials or permissions; blind retries cannot fix access.
400 or 422 responseQuarantine the invalid event for correction; preserve the original failure and avoid infinite retries.
Malformed success bodyKeep the event pending for investigation; do not silently discard it.

Use a bounded request timeout and jittered backoff to avoid synchronized retries. A retry ceiling should route an event to an operational queue or alert, not delete it. Monitor pending count, oldest pending age, and permanently rejected events.

Reconcile later and out-of-order changes

Stripe can report customer action, authorization, capture, cancellation, and refund changes after the first response. Your merchant webhook handler must verify Stripe signatures, correlate the attempt, and normalize the actual state before persisting another report with source: merchant_webhook.

Use occurredAt for the observation's actual time, not the latest delivery attempt time. Report cumulative captured and refunded amounts for the attempt. When notifications arrive out of order, reconcile against current provider state rather than letting an old event erase a known capture or refund.

A provider timeout remains unknown until verified information resolves it. The unknown state is neither a success nor a failure. Carden's authentication of a reporting key does not constitute direct verification of a Stripe provider event.

Test the failure boundaries

  • Crash after Stripe accepts a request but before your local transaction commits; confirm recovery produces one correct attempt record.
  • Drop the HTTP response after Carden accepts a report; retry and confirm duplicate acknowledgment.
  • Deliver the same report concurrently from two workers; verify one logical event is tracked.
  • Revoke a report key, rotate it, and resume old outbox events without changing event IDs.
  • Deliver capture and refund webhooks out of order; keep observed amounts coherent.
  • Keep Carden unavailable while payments continue; confirm no extra payment calls and no lost reports.