Payment reports
Record actual Stripe payment observations with stable identities and deduplicated delivery.
On this page
Endpoint
POST /api/v1/stripe/payment-reportsSubmit an actual attempt or later status your backend observed. Authorization scopes the record to a merchant organization, integration, and environment. The API records the report; it does not execute or retry a payment.
Request fields
| Field | Required | Meaning |
|---|---|---|
eventId | Yes | Stable identity of this immutable observation. Keep it unchanged on delivery retries. |
attemptId | Yes | Your stable payment-attempt identity; later observations reuse it. |
paymentIntentId / chargeId | No | Actual Stripe object IDs when known. |
stripeAccountId | No | Stripe account context when needed for traceability; not authorization to another merchant. |
status | Yes | One of the normalized statuses below. |
currency | Yes | Exactly three lowercase letters, such as usd. All amounts use this currency. |
amountMinor | Yes | Attempt amount in nonnegative integer minor units. |
capturedAmountMinor | Yes | Cumulative amount actually captured for this attempt, not an authorization amount. |
refundedAmountMinor | Yes | Cumulative amount actually refunded for this attempt. |
occurredAt | Yes | ISO 8601 timestamp of the observed event, including timezone; not the latest retry time. |
enrichmentRequestId | No | x-carden-request-id from the preparation request, when available. |
invoiceReference | No | Authoritative invoice or mapped reference associated with the attempt. |
errorCode | No | Sanitized lowercase code, starting with a letter and containing only letters, digits, and underscores; maximum 80 characters. |
providerEventId | No | Provider event identifier for a later observation, if available. |
source | Yes | sdk, merchant_webhook, or api; describes the reporting path, not proof of provider verification. |
Report objects are strict: extra fields and raw provider objects are rejected. References must be nonempty, at most 160 characters, and free of control characters, credentials, and card data. Provider object IDs must use the expected pi_, ch_, or acct_ prefix followed by letters and digits. Supply optional fields only when known, rather than null.
Keep amounts internally consistent with the observed attempt. Refunds cannot represent more than the captured amount, and a report must not turn an unknown authorization into a confirmed capture. If a provider timeout leaves the captured amount uncertain, preserve the last known amounts and the unknown status while your backend reconciles.
Status meanings
| Status | Meaning |
|---|---|
requires_action | The customer must complete an additional action; payment is not complete. |
authorized | Funds were authorized, but capture has not been reported. |
captured | Capture was observed; use the actual cumulative captured amount. |
partially_refunded | Some, but not all, captured funds were refunded. |
refunded | The captured payment has been fully refunded. |
failed | A definitive failure was observed, rather than inferred from a timeout. |
canceled | Cancellation was observed. |
unknown | The provider result remains ambiguous or unresolved; it is not a success. |
All monetary fields are nonnegative safe integers, with capturedAmountMinor no greater than amountMinor and refundedAmountMinor no greater than capturedAmountMinor. requires_action, authorized, failed, and canceled require zero capture/refund totals. captured requires positive capture with zero refunds; partially_refunded requires a positive refund smaller than capture; refunded requires a positive capture fully refunded. unknown may retain previously known amounts, but those amounts must still reconcile.
These are Carden report statuses, not a direct copy of every Stripe status string. In your merchant backend, normalize provider objects with their capture and refund evidence. For example, a provider processing state should remain unresolved rather than being labeled captured without evidence.
Example request
The IDs and timestamp below are synthetic event data, not a release date or a real payment. Replace them with the stable identifiers and occurrence time stored by your backend.
{
"eventId": "report_attempt_1042_captured_1",
"attemptId": "attempt_1042_1",
"paymentIntentId": "pi_example",
"chargeId": "ch_example",
"status": "captured",
"currency": "usd",
"amountMinor": 11300,
"capturedAmountMinor": 11300,
"refundedAmountMinor": 0,
"occurredAt": "2026-01-01T12:00:00.000Z",
"invoiceReference": "INV-1042",
"source": "api"
}For a later partial refund, preserve attemptId, amount, and capture total; create a new eventId, use partially_refunded, and report the cumulative refunded amount and actual event time.
Response and deduplication
{
"object": "stripe.payment_report",
"api_version": "v1",
"data": {
"id": "report_example",
"duplicate": false
}
}On repeat delivery of the same event in the same organization, integration, and environment, data.duplicate is true. Treat that as acknowledgment, not as a failure or another payment. Preserve the returned ID and response request ID in your delivery record.
A 409 indicates the event ID was already accepted with a different payload, or the report conflicts with a previously recorded currency, Stripe account, or PaymentIntent identity for that attempt. Fix the producer's event identity or reconcile the facts instead of retrying blindly. Invalid fields or an enrichment reference outside the same integration/environment produce 400; oversized reports can produce 413. An event timestamp more than five minutes in the future is rejected.
Accepted observations are immutable history. The current tracked-payment view is a projection of those observations: later provider IDs can link an initially unknown attempt to an existing payment without deleting its report history. Older reports and reports that reduce known cumulative capture/refund amounts do not replace the current financial state. A successful receipt confirms acceptance, not that the report became the latest displayed state.
Explicit HTTP delivery
Use this explicit HTTP example or carden.stripe.reportPayment(report, options?) in the private Node.js package. report is the immutable event stored by your application; configure baseUrl and apiKey in your backend.
// 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.The surrounding worker must catch network exceptions and retain the outbox entry. Never turn this error into a retry of the Stripe request. Use an operational queue for permanent validation failures and a backoff policy for transient delivery failures.
Later provider events
Your own Stripe webhook handler verifies provider signatures and reports normalized later statuses with a Carden key. Include source: merchant_webhook and a providerEventId where available. Carden does not receive Stripe credentials or directly attest that provider event.