Skip to documentation content

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 optionMeaning
apiKeyRequired Carden API key for the intended merchant/environment.
baseUrlRequired absolute Carden deployment origin. Use HTTPS in production.
timeoutMsPositive integer client timeout; enrichment defaults to 5,000 ms.
fetchOptional Fetch-compatible implementation, useful for testing.
Enrichment with request correlation
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.

SDK delivery from a durable worker
// 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 optionAllowed values and defaults
maxRetries0–2 additional delivery attempts; default 2 (up to three total requests).
retryDelayMs0–1,000 ms fixed delay; default 100 ms.
timeoutMs1–5,000 ms per request, including response-body reading; default the smaller of client timeoutMs and 1,000 ms.
signalOptional 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.

Equivalent explicit HTTP delivery
// 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 optionMeaning
reportRequired stable event/attempt IDs, currency, amount, and last-known capture/refund totals, plus optional references. Omit status, source, and errorCode; occurredAt is optional.
selectResultRequired local selector returning only the allowlisted payment observation fields.
selectErrorOptional 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.
reportOptionsOptional report timeout, signal, and bounded retry options from the table above.
onReportErrorOptional callback receiving a CardenError and { report }. The report is a validated immutable report or null if selection/validation failed.
Track a merchant-owned payment operation
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.

Select provider facts
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 / evidenceNormalized observation
requires_actionrequires_action
requires_captureauthorized
succeededcaptured, partially_refunded, or refunded according to actual captured/refunded totals.
canceledcanceled
requires_payment_methodfailed only with last_payment_error code or type; otherwise unknown.
processing, requires_confirmation, or another unmapped stateunknown; 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

ErrorUseful information / action
CardenApiErrorstatus, type, issues, and requestId; distinguish permanent validation/access errors from transient failures.
CardenTimeoutErrortimeoutMs; no acknowledgment reached the client. Retain the report for retry.
CardenConnectionErrorThe client could not reach Carden. Retry report delivery independently of the payment.
CardenPaymentReportValidationErrorA report or selector result failed the allowlisted schema. Correct the selected facts; the error intentionally excludes raw provider values.
CardenErrorBase 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.