Node.js SDK
Use the private @carden/node workspace package from your server, with explicit reporting and durable delivery.
On this page
Package availability
@carden/node is currently a private workspace package. It is not documented as a publicly published npm package. Use the package supplied for your integration and verify its version and exports; a public npm install command is intentionally not part of this guide.
The client runs in your Node.js backend with server-held credentials. It uses a Fetch-compatible implementation and timeout/abort support. It does not need your Stripe secret and is not intended for browser bundles.
Configure and prepare enrichment
| Client option | Meaning |
|---|---|
apiKey | Required Carden API key for the intended merchant/environment. |
baseUrl | Required absolute Carden deployment origin. Use HTTPS in production. |
timeoutMs | Positive integer client timeout; enrichment defaults to 5,000 ms. |
fetch | Optional Fetch-compatible implementation, useful for testing. |
import { Carden } from "@carden/node";
const carden = new Carden({
apiKey: process.env.CARDEN_API_KEY!,
baseUrl: process.env.CARDEN_BASE_URL!,
timeoutMs: 5000,
});
// invoice is the complete CanonicalInvoice from your trusted invoice source.
const { enrichment, requestId } = await carden.stripe.createPaymentIntentEnrichmentWithRequestId({
invoice,
options: { taxRepresentation: "line_item", discountRepresentation: "line_item" },
});
// Persist requestId with your attempt; omit enrichmentRequestId if it is null.
// enrichment contains amount_details and payment_details, not a PaymentIntent.
// Merge these fields into your own supported Stripe request on your server.
// Keep Stripe authentication and payment idempotency in your existing backend.
// Persist and deliver the actual outcome separately with the payment-report API.carden.stripe.createPaymentIntentEnrichmentWithRequestId(input, options?) returns { enrichment, requestId }. The ID comes from x-carden-request-id and can be null if a server or proxy omits that header. Save it with your attempt; include enrichmentRequestId in reports only when it is available.
carden.stripe.createPaymentIntentEnrichment(input, options?) remains available and returns just the enrichment fields. Both methods accept signal and timeoutMs request options and execute no Stripe payment.
Report actual payment observations
carden.stripe.reportPayment(report, options?) validates the allowlisted report, posts it to the v1 payment-report endpoint, and returns { id, duplicate }. It throws on final delivery or validation failure. A duplicate acknowledgment means the same immutable observation was already accepted; a conflicting event ID returns 409.
// In your durable outbox worker. carden is the configured server client.
// report is the immutable, validated event previously persisted by your backend.
const receipt = await carden.stripe.reportPayment(report, {
maxRetries: 0, // The durable worker owns backoff and respects rate-limit delays.
timeoutMs: 5000,
});
// receipt is { id: string, duplicate: boolean }, not the HTTP envelope.
// Mark the outbox entry delivered for either duplicate value.
// On error, retain the same eventId and payload; never repeat the Stripe charge.| Report option | Allowed values and defaults |
|---|---|
maxRetries | 0–2 additional delivery attempts; default 2 (up to three total requests). |
retryDelayMs | 0–1,000 ms fixed delay; default 100 ms. |
timeoutMs | 1–5,000 ms per request, including response-body reading; default the smaller of client timeoutMs and 1,000 ms. |
signal | Optional AbortSignal; cancellation stops further retries. |
Built-in report retries cover connection errors, timeouts, and HTTP 408, 429, and 5xx responses. They retain exactly the same serialized body and event ID. Other failures, including invalid successful response envelopes, are surfaced without this retry loop.
// 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.Track an operation without changing its result
carden.stripe.trackPayment(operation, options) calls your supplied operation once, then attempts to report its outcome. It returns the original result or rethrows the original operation error. It awaits bounded report delivery before returning or rethrowing, so reporting can add latency; it is not background delivery. It does not create Stripe credentials or implement Stripe's payment idempotency policy.
| Tracking option | Meaning |
|---|---|
report | Required stable event/attempt IDs, currency, amount, and last-known capture/refund totals, plus optional references. Omit status, source, and errorCode; occurredAt is optional. |
selectResult | Required local selector returning only the allowlisted payment observation fields. |
selectError | Optional selector for definitive provider evidence. An omitted selector produces unknown with last-known amounts. Recognized transport errors force unknown even if this selector says otherwise. |
reportOptions | Optional report timeout, signal, and bounded retry options from the table above. |
onReportError | Optional callback receiving a CardenError and { report }. The report is a validated immutable report or null if selection/validation failed. |
import { paymentObservationFromStripePaymentIntent } from "@carden/node";
// Prerequisites supplied by your backend:
// - carden: a server client with a Stripe-integration Carden key.
// - attempt: a durably stored attempt with stable eventId/attemptId, currency,
// amountMinor, known capturedAmountMinor/refundedAmountMinor, and invoiceReference.
// - executeStripePayment: your existing operation with your Stripe idempotency policy.
// It returns a PaymentIntent with an expanded latest_charge for refund evidence.
// - reportDeliveryMonitor: your application's safe operational alerting adapter.
const result = await carden.stripe.trackPayment(executeStripePayment, {
report: {
eventId: attempt.eventId,
attemptId: attempt.attemptId,
currency: attempt.currency,
amountMinor: attempt.amountMinor,
capturedAmountMinor: attempt.capturedAmountMinor,
refundedAmountMinor: attempt.refundedAmountMinor,
invoiceReference: attempt.invoiceReference,
...(requestId ? { enrichmentRequestId: requestId } : {}),
},
selectResult: (intent) => paymentObservationFromStripePaymentIntent(intent),
reportOptions: { maxRetries: 0, timeoutMs: 1000 },
onReportError: (error, { report }) => {
// Monitoring only: this callback is not a durable-delivery guarantee.
// report is null if selection/validation failed; never log raw Stripe errors.
reportDeliveryMonitor.record({
attemptId: attempt.attemptId,
eventId: report?.eventId ?? attempt.eventId,
errorType: error.name,
});
},
});
// result is the original Stripe result, even when Carden reporting failed.
// Your durable reconciliation/outbox process must recover undelivered reports.
// A provider exception is rethrown unchanged; it is not automatically a failure status.The wrapper adds source: sdk. It uses the supplied occurredAt or the observation time after the operation. Missing provider IDs in a selector do not erase known IDs from the context. A thrown provider operation is not assumed failed; a failed follow-up operation on a captured payment must preserve known funded amounts or use unknown.
Normalize a PaymentIntent locally
paymentObservationFromStripePaymentIntent(intent, options?) is a standalone export from @carden/node, not a method under carden.stripe. It selects allowed fields locally, performs no network call, and does not verify provider signatures.
import { paymentObservationFromStripePaymentIntent } from "@carden/node";
// intent is a verified/retrieved provider snapshot kept in your own backend.
// Prefer an expanded latest_charge with actual amount_captured and amount_refunded.
const observation = paymentObservationFromStripePaymentIntent(intent);
// If latest_charge is not expanded, provide a separately known refund total:
// paymentObservationFromStripePaymentIntent(intent, { refundedAmountMinor: knownRefundTotal });
// Never substitute zero for a refund total you have not established.
// Combine observation with your stable eventId/attemptId, actual occurredAt,
// source: "merchant_webhook", and verified providerEventId where applicable.
// Persist the resulting report first, then deliver it via reportPayment.
// This helper performs no network request and does not verify the Stripe signature.| Stripe state / evidence | Normalized observation |
|---|---|
| requires_action | requires_action |
| requires_capture | authorized |
| succeeded | captured, partially_refunded, or refunded according to actual captured/refunded totals. |
| canceled | canceled |
| requires_payment_method | failed only with last_payment_error code or type; otherwise unknown. |
| processing, requires_confirmation, or another unmapped state | unknown; never inferred success. |
For a captured payment, provide an expanded latest_charge containing amount_captured and amount_refunded, or supply a separately established refundedAmountMinor option. A PaymentIntent alone has no refund total; the helper throws if captured funds exist and neither source supplies that total. It also enforces coherent status/amount combinations.
For merchant Stripe webhooks, verify the provider event in your backend, construct an observation from the appropriate provider state, and persist a new report with source: merchant_webhook, stable attempt identity, actual occurrence time, and providerEventId. Use reportPayment to deliver that event; trackPayment labels its own observations sdk.
Handle client errors
| Error | Useful information / action |
|---|---|
CardenApiError | status, type, issues, and requestId; distinguish permanent validation/access errors from transient failures. |
CardenTimeoutError | timeoutMs; no acknowledgment reached the client. Retain the report for retry. |
CardenConnectionError | The client could not reach Carden. Retry report delivery independently of the payment. |
CardenPaymentReportValidationError | A report or selector result failed the allowlisted schema. Correct the selected facts; the error intentionally excludes raw provider values. |
CardenError | Base class for client/configuration errors; correct configuration rather than silently ignoring it. |
An application crash can happen before an SDK error callback runs. Even an asynchronous reporting error callback is not a substitute for persisting the attempt and an outbox event in your own durable storage.
Test the integration boundary
- Inject a controlled fetch implementation for success, 401, 422, duplicate report, server failure, and timeout scenarios.
- Verify that keys, Stripe secrets, and raw provider objects never appear in request bodies or logs.
- Check that report delivery failure leaves the original Stripe result intact.
- Verify a process restart resumes pending reports with the same event IDs.
- Keep the private package version and the deployed API contract aligned during updates.