Skip to documentation content

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

ValueLifetimeUse
attemptIdOne actual payment attemptAuthorization, capture, refund, cancellation, and failure observations reuse it.
eventIdOne immutable observationDelivery retry keeps ID and body; later state gets a new event.
providerEventIdOne provider eventLinks a report to the Stripe or Square event verified by the merchant.
enrichmentRequestIdOne preparationUse 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

  1. Persist attempt identity and provider idempotency before payment.
  2. Execute the provider operation independently of Carden availability.
  3. Persist actual outcome and immutable report in one local transaction where possible; keep ambiguity unknown.
  4. Have a worker claim and deliver pending rows with a scoped Carden key.
  5. Acknowledge only a valid success response, including duplicate: true.
  6. Retain attempts, next retry time, and safe failure reason; alert on permanent failures and aging backlog.
Report-only worker request
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

ResultAction
Valid 2xxAcknowledge; duplicate is delivered.
Timeout / connectionKeep pending and retry same event because the server may have accepted it.
408 / 429 / 5xxRetry with bounded exponential backoff and jitter; honor Retry-After when present.
401 / 403Pause affected delivery and repair credentials or permissions.
400 / 422Quarantine for factual correction; do not loop indefinitely.
409Reconcile immutable identity and known cumulative facts.
Malformed successKeep 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.