payable

Stripe Provider

Stripe Treasury, including its independent webhook verifier, is implemented by the separate StripeTreasuryProvider; see Stripe Treasury.

Stripe Tax is also exposed independently through StripeTaxProvider. It implements the generic tax calculation and transaction contracts without adding Stripe-specific methods to PaymentProvider.

Stripe Issuing is exposed through StripeIssuingProvider, with a separate registry and no card data or operations added to the payment provider.

Stripe Identity is exposed through StripeIdentityProvider. Verification sessions remain separate from payment customers and Connect onboarding, and normalized results exclude verified personal data.

StripeProvider (src/infrastructure/providers/stripe/stripe-provider.ts) is the reference implementation of PaymentProvider. It implements the base contract and optional capabilities for charges, subscriptions, invoices, saved payment methods, payment-method setup, disputes, payouts, webhooks, and catalog operations. Its registry name is 'stripe'.

Construction and options

export interface StripeProviderOptions {
  secretKey: string;
  webhookSecret: string;
  logger?: Logger;
}

new StripeProvider(options: StripeProviderOptions, client?: Stripe);
  • secretKey - the Stripe API key used to lazily construct the SDK client.
  • webhookSecret - the signing secret passed to StripeWebhookVerifier.
  • logger (optional) - a Logger forwarded to StripeEventNormalizer.
  • client (optional) - an injected Stripe instance, used in tests. When omitted, the client is created on first use via a dynamic import('stripe'), so the stripe package is only loaded when the provider is actually exercised (zero-peer-dependency guarantee).
const stripe = new StripeProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
});

const payable = createPayable({ providers: { stripe }, /* storage, queue, ... */ });

Stripe Tax provider

import { StripeTaxProvider } from '@akira-io/payable';

const tax = new StripeTaxProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
});

StripeTaxProvider has the registry name stripe-tax and declares the calculations and transactions tax capabilities. Like the payment and Treasury providers, it loads the optional Stripe SDK dynamically when no client is injected.

Stripe Issuing provider

import { StripeIssuingProvider } from '@akira-io/payable';

const issuing = new StripeIssuingProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
});

StripeIssuingProvider has the registry name stripe-issuing and declares cardholders, cards, authorizations, and transactions. It uses the same dynamic optional Stripe SDK boundary.

Stripe Identity provider

import { StripeIdentityProvider } from '@akira-io/payable';

const identity = new StripeIdentityProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
});

StripeIdentityProvider has the registry name stripe-identity and declares only verificationSessions. It lazily loads the optional Stripe SDK and does not add identity methods or results to StripeProvider.

Declared capabilities

capabilities(): ProviderCapabilities {
  return new Set([
    'checkout',
    'charges',
    'subscriptions',
    'trials',
    'refunds',
    'coupons',
    'billingPortal',
    'invoicePdf',
    'webhooks',
    'customers',
    'paymentMethods',
    'paymentMethodSetup',
    'catalog',
    'catalogRead',
    'catalogLifecycle',
    'catalogIdempotency',
    'priceLookupKeys',
  ]);
}

Stripe supports every current Payable provider capability except meteredBilling (absent from the set). It is the only built-in provider that implements ChargeCapable, DirectSubscriptionCapable, InvoiceCapable (listInvoices, downloadInvoicePdf), and PaymentWebhookCapable.

Checkout handling

createCheckoutSession calls checkout.sessions.create. Payable checkout reference is forwarded as Stripe client_reference_id; idempotency still uses Stripe’s request option. Refund reference is forwarded as Stripe refund metadata.reference.

Subscription handling

Subscription operations are delegated to StripeSubscriptions (src/infrastructure/providers/stripe/stripe-subscriptions.ts), constructed with a lazy client getter so it shares the provider’s single SDK instance.

OperationStripe callBehavior
createsubscriptions.createUses input.items when present, otherwise a single item from priceId with quantity ?? 1. Applies trial_period_days and discounts (coupon) when provided.
updatesubscriptions.updateUses the stable provider item ID persisted for the selected local item. Missing mappings fail without selecting items.data[0].
cancelsubscriptions.cancel or subscriptions.updateimmediately: true cancels now; otherwise sets cancel_at_period_end: true.
resumesubscriptions.updateClears the pending cancellation with cancel_at_period_end: false.
pausePaymentCollectionsubscriptions.updateSets pause_collection.behavior and optional pause_collection.resumes_at.
resumePaymentCollectionsubscriptions.updateUnsets pause_collection.

createSubscription (from DirectSubscriptionCapable) routes to StripeSubscriptions.create; the contract’s updateSubscription, cancelSubscription, and resumeSubscription route to the matching methods. Every call forwards ctx.idempotencyKey to Stripe’s idempotencyKey request option.

Stripe’s stable pause_collection API pauses payment collection, not the subscription lifecycle. Invoices continue to be created and the subscription status is unchanged. Payable therefore exposes this through pausePaymentCollection() and resumePaymentCollection() only, with the documented keep_as_draft, mark_uncollectible, and void behaviors and optional resumes_at timestamp. It does not advertise lifecycle pauseSubscription() support. Stripe’s separate lifecycle-pause API is public preview and is intentionally outside this stable adapter contract.

Official Stripe subscription references:

Catalog lifecycle

StripeProvider implements CatalogCapable, CatalogReadCapable, CatalogLifecycleCapable, and PriceLookupKeyCapable. Product and price list calls map the portable active, limit, and cursor fields to Stripe’s list parameters. Price lists also map providerProductId to Stripe’s product filter and lookupKeys to Stripe’s lookup-key filter. A page with has_more: true returns the final item id as nextCursor; callers pass that cursor back unchanged.

Payable operationStripe callNotes
products().retrieve(id)products.retrieve(id)A missing product maps to PRODUCT_NOT_FOUND.
products().list(input)products.list({ active, limit, starting_after })Defaults are applied by the Payable resource before this call.
products().activate(id)products.update(id, { active: true })Marks the Stripe product active.
products().archive(id)products.update(id, { active: false })Keeps the Stripe product record.
prices().retrieve(id)prices.retrieve(id)A missing price maps to PRICE_NOT_FOUND.
prices().list(input)prices.list({ active, limit, product, lookup_keys, starting_after })Supports product, lookup-key, and active-state filters.
prices().transferLookupKey({ providerPriceId, lookupKey }, options)prices.update(providerPriceId, { lookup_key, transfer_lookup_key: true })Transfers the key atomically at Stripe.
prices().activate(id)prices.update(id, { active: true })Marks the Stripe price active.
prices().archive(id)prices.update(id, { active: false })Keeps the Stripe price record.

Payable exposes no portable delete method for Stripe products or prices. Existing price monetary terms are not updateable through the contract. Create a replacement price and archive the old price when the amount, currency, billing interval, or interval count changes.

Stripe lookup keys are optional provider-native aliases, not Payable price identity. A key contains at most 200 Unicode code points, and one price-list request accepts at most 10 keys. A create with transferLookupKey: true or an explicit transfer moves a Stripe lookup key atomically, but does not archive the former price. The caller manages that price’s lifecycle separately. Payable does not persist lookup keys as local price identity.

If Stripe rejects a duplicate or conflicting lookup-key request, Payable exposes generic provider invalid-request behavior as PROVIDER_REQUEST_INVALID. There is no portable lookup-key collision error type; inspect the provider error context and correct the request before retrying.

Official Stripe references:

Catalog idempotency

Stripe declares Payable’s catalogIdempotency capability. For a keyed product or price mutation, Payable sends a derived payable:catalog:v1:<sha256> value through Stripe’s idempotencyKey request option. The raw caller key is not sent. An engine store is optional for provider forwarding and adds local concurrency control, request-conflict detection, and response replay when configured.

Stripe documents that idempotency keys can be supplied to POST requests, that repeated requests return the saved result, and that parameters are compared when a key is reused. Stripe also limits keys to 255 characters. Payable’s derived key remains within that limit. See Stripe idempotent requests.

Entity mapping

stripe-mappers.ts converts Stripe SDK objects into domain DTOs. Key behaviors:

  • Money is always reconstructed via Money.of(amount, currency.toUpperCase()). Stripe currencies are lower-cased on the wire; the engine normalizes to upper-case currency codes.
  • stripeAmount / stripeMoney (stripe-amounts.ts) rescale between the engine’s currency precision and Stripe’s per-currency exponent (zero-decimal currencies like JPY, three-decimal like KWD). When a downscale would drop significant digits, the rescale throws PayableError (PROVIDER_CURRENCY_EXPONENT_MISMATCH) rather than silently losing precision.
  • toPriceDTO resolves the unit amount from unit_amount, falling back to an integer unit_amount_decimal. A non-integer decimal throws PayableError (PROVIDER_PRICE_AMOUNT_UNRESOLVABLE).
  • toSubscriptionDTO maps the Stripe status through isSubscriptionStatus, defaulting unknown values to incomplete. currentPeriodEnd is read from the first item’s current_period_end (falling back to the subscription-level field) and converted from Unix seconds. trialEndsAt comes from trial_end.
  • PAYMENT_STATUS exhaustively covers the Stripe SDK’s PaymentIntent.Status union and translates it into the domain PaymentStatus. REFUND_STATUS translates known refund states into RefundStatus. Runtime-unmapped states default to pending.

Event normalization

StripeEventNormalizer (stripe-event-normalizer.ts) maps raw Stripe event types to the engine’s NormalizedEventName. The full map:

Stripe event typeNormalized name
checkout.session.completedcheckout.completed
checkout.session.async_payment_succeededcheckout.completed
checkout.session.async_payment_failedpayment.failed
charge.succeededpayment.succeeded
charge.failedpayment.failed
payment_intent.succeededpayment.succeeded
payment_intent.payment_failedpayment.failed
customer.createdcustomer.created
customer.updatedcustomer.updated
customer.subscription.createdsubscription.created
customer.subscription.updatedsubscription.updated
customer.subscription.deletedsubscription.cancelled
customer.subscription.resumedsubscription.resumed
invoice.createdinvoice.created
invoice.paidinvoice.paid
invoice.payment_succeededinvoice.paid
invoice.payment_failedinvoice.payment_failed
charge.refundedrefund.succeeded
refund.createdrefund.created
refund.failedrefund.failed

Unmapped types normalize to null. The provider keeps the raw type alongside normalizedType, so an unrecognized event is still persisted, just not reconciled.

Disputes, payouts, setup intents, payment-method lifecycle, Connect, Treasury, Terminal, Issuing, Identity, and Financial Connections events remain intentionally unmapped because no approved generic webhook reconciliation flow consumes them.

Saved payment methods

StripeProvider implements PaymentMethodCapable for customer-scoped saved methods. Use listPaymentMethods({ providerCustomerId, limit }) to retrieve normalized display fields, or deletePaymentMethod({ providerCustomerId, providerPaymentMethodId }, ctx) to detach one. Deletion first retrieves the method through the customer-scoped Stripe endpoint, then forwards ctx.idempotencyKey to paymentMethods.detach.

Only generic display data is returned: provider id, type, card brand, last four digits, and expiry. Fields that do not apply to a payment method type are null. Collection and attachment use the separate payment-method setup capability.

Payment method setup

StripeProvider implements PaymentMethodSetupCapable through Setup Intents. Creation forwards the customer, usage, optional payment method types, return URL, opaque reference metadata, and operation idempotency key. The normalized currency field is accepted but not sent because Stripe Setup Intents do not take a currency.

Retrieve and cancel return the same normalized setup DTO. Stripe states that require further input or confirmation map to requires_action; processing, succeeded, and canceled states map directly. The result exposes only the client secret and resulting payment method ID needed by the setup lifecycle.

Disputes

StripeProvider implements DisputeCapable with listDisputes, retrieveDispute, and acceptDispute. Accepting maps to Stripe disputes.close and forwards ctx.idempotencyKey; it is irreversible and acknowledges the dispute as lost.

Disputes expose normalized status, amount, reason, creation time, response deadline, and the related PaymentIntent id, falling back to the Charge id. Evidence submission is not generalized because its shape and upload lifecycle differ across providers.

Payouts

StripeProvider implements PayoutCapable with listPayouts and retrievePayout. List operations use Stripe auto-pagination with a default page size and result limit of 100; a larger requested limit continues across pages without requesting more than Stripe’s per-page maximum.

Payouts map provider id, normalized lifecycle status, amount, creation time, and expected arrival time. Creating, canceling, and reversing payouts remain outside the read-only generic capability.

Tax calculations and transactions

StripeTaxProvider creates and retrieves Stripe Tax calculations. It maps multiple line items, shipping cost, customer and shipping addresses, tax behavior, product tax codes, and customer tax IDs into the generic tax contracts. Customer tax IDs use the portable type:value form, for example eu_vat:DE123456789, because Stripe requires both values.

All line items and shipping in one calculation must use the same currency. A mixed-currency request is rejected before the SDK call with PROVIDER_TAX_CURRENCY_MISMATCH. Successful Stripe Tax calculation responses map to complete; Stripe returns an API error instead of an asynchronous calculation object when calculation cannot complete.

commitTaxTransaction creates a Stripe Tax transaction from a completed calculation and forwards the operation idempotency key. reverseTaxTransaction currently performs a full transaction reversal. Partial reversals remain outside the generic contract.

Issuing

StripeIssuingProvider creates and retrieves cardholders, creates virtual and physical cards, updates card status, and provides bounded reads for cards, authorizations, and transactions. Stripe requires a billing address for cardholders and a currency for cards. Physical cards also require a generic shipping contact. Missing required fields are rejected before the SDK call.

A spendingLimit maps to a per-authorization Stripe spending limit. active, inactive, and canceled card states are supported; the generic blocked state is rejected because Stripe does not provide an equivalent card state. Card mappers return only brand, last four, expiry, status, form, provider identifiers, and creation time. PAN, CVC, PIN, shipping details, and expanded provider objects are never returned.

Authorization reads normalize Stripe lifecycle and decision state. respondIssuingAuthorization uses Stripe’s approve and decline REST methods and forwards idempotency. Stripe marks those methods deprecated in favor of responding directly to real-time authorization webhooks, so new applications should treat this method as a compatibility path until a generic synchronous webhook-response contract is approved.

Connect marketplace

StripeMarketplaceProvider is an independent marketplace provider named stripe-connect. It advertises the accounts, onboarding, transfers, transferReversals, and payouts capabilities. It implements seller accounts, hosted onboarding, platform-to-seller transfers and reversals, and connected-account payouts without adding Stripe Connect fields to payment DTOs or coupling marketplace operations to StripeProvider.

import { Money, StripeMarketplaceProvider } from '@akira-io/payable';

const marketplace = new StripeMarketplaceProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
});
const transfer = await marketplace.createMarketplaceTransfer(
  {
    destinationProviderAccountId: 'acct_123',
    amount: Money.of(10_000, 'USD'),
    groupReference: 'order-123',
    sourceReference: { type: 'charge', providerChargeId: 'ch_123' },
  },
  { correlationId: 'order-123', idempotencyKey: 'transfer-order-123' },
);

await marketplace.createMarketplaceTransferReversal(
  {
    providerTransferId: transfer.providerTransferId,
    amount: 2_500,
    reference: 'order-123-partial-reversal',
  },
  { correlationId: 'order-123', idempotencyKey: 'reverse-order-123-1' },
);

Connected accounts use Stripe’s current controller configuration instead of the deprecated account type parameter. The provider creates accounts with Stripe-managed requirement collection, Express Dashboard access, application-paid Stripe fees, and application liability for payment losses. individual and business map to Stripe’s individual and company business types; the generic contract does not expose Stripe account-type vocabulary.

Account status is derived from charges_enabled, payouts_enabled, submitted details, and requirements.currently_due. Disabled accounts map from requirements.disabled_reason. Account listing uses bounded auto-pagination and applies the generic status filter after normalization.

Onboarding links use account_onboarding and forward ctx.idempotencyKey. Transfers move funds from the platform balance to the destination connected account and also forward idempotency. Stripe does not expose a transfer lifecycle status, so unreversed transfers normalize to completed and fully reversed transfers normalize to reversed.

Transfer and reversal constraints:

  • groupReference is an association label and does not change settlement behaviour.
  • sourceReference accepts a Charge ID. A PaymentIntent consumer must resolve latest_charge first.
  • Source-linked transfer funds become available when the source charge funds become available.
  • Omitting reversal amount reverses the full remaining amount; a positive integer performs a partial reversal.
  • The destination connected account must have sufficient available balance unless connected account reserves cover the reversal.
  • Refunding a charge does not automatically reverse related transfers.
  • An asynchronous payment failure does not automatically reverse a transfer; the application must reconcile it.
  • Separate charges and transfers have provider-specific regional and cross-border restrictions.

Payout create, list, and retrieve requests pass the seller account ID only through Stripe’s stripeAccount request option. This makes payouts operate on the connected account balance while account, onboarding, and transfer calls remain platform requests. pending and in_transit Stripe payouts normalize to pending; paid, failed, and canceled retain their meaning.

Marketplace transfers do not create customer payments and are not automatically reversed when an unrelated payment fails. The application remains responsible for choosing the payment flow, waiting for asynchronous funds when required, and reconciling transfer reversals.

Official Stripe references:

Terminal

StripeTerminalProvider is an independent server-driven Terminal provider. It lists and retrieves registered Readers and processes card-present PaymentIntents without introducing browser, mobile, Bluetooth, or hardware SDK dependencies.

import { StripeTerminalProvider } from '@akira-io/payable';

const terminal = new StripeTerminalProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
});

Device listing supports Stripe location filters and bounded auto-pagination. A Reader with an active action maps to busy; otherwise Stripe’s online and offline states retain their meaning. Device DTOs include the Reader ID, label, location ID, serial number, and device type. IP addresses, networking details, metadata, and device secrets are not returned.

createTerminalPayment creates a PaymentIntent with payment_method_types: ['card_present'], then hands it to the selected Reader through terminal.readers.processPaymentIntent. Automatic capture is the supported mode. captureMethod: 'manual' is rejected before creating Stripe resources because the current generic Terminal contract has no capture operation. PaymentIntents encountered in requires_capture normalize to pending, never succeeded. Both write calls receive distinct deterministic keys derived from ctx.idempotencyKey. Long Payable keys are hashed so every forwarded key remains within Stripe’s 255-character limit. When Payable does not provide a key, the adapter omits Stripe idempotency options instead of deriving a shared fallback.

Stripe Reader actions do not have independent identifiers. The provider therefore returns an opaque, versioned providerTerminalPaymentId that identifies both the Reader and PaymentIntent. providerPaymentId contains the PaymentIntent ID. Retrieval loads that exact PaymentIntent and only uses Reader action state when the action belongs to the same payment. The provider records the Reader ID in PaymentIntent metadata and rejects identifiers whose Reader and PaymentIntent do not match. Legacy Reader-only identifiers cannot identify a payment safely and are rejected. Persist the versioned identifier returned by createTerminalPayment after upgrading.

cancelTerminalPayment returns PROVIDER_OPERATION_UNSUPPORTED before calling Stripe. Stripe’s server-driven cancel_action endpoint targets the Reader’s current action, not a specific PaymentIntent, so it cannot safely fulfill a payment-specific cancellation contract when actions change concurrently. If handoff fails after PaymentIntent creation, retrying with the same idempotency key reuses the same Stripe write results.

Identity verification

createIdentityVerification creates a Stripe Identity VerificationSession and forwards the opaque application reference as both client_reference_id and metadata.reference. The reference must not contain a name, email address, phone number, national identifier, or other PII. returnUrl maps to Stripe’s return_url, and every create, cancel, and redact request forwards ctx.idempotencyKey.

Supported check combinations are:

Generic checksStripe configuration
documenttype: document
document, selfiedocument with require_matching_selfie
document, id_numberdocument with require_id_number
document, selfie, id_numberdocument with both options
id_numbertype: id_number

selfie without document, empty check lists, address, and phone fail before an SDK request with PROVIDER_OPERATION_UNSUPPORTED. Stripe exposes address and phone checks through restricted flows, not through the generic session configuration implemented here.

Stripe requires_input, processing, verified, and canceled states map directly to the generic lifecycle. Redaction processing and validated remain processing; only Stripe redaction state redacted maps to redacted. Stripe does not expose a verification-completion timestamp on the session, so verifiedAt is null.

The returned DTO contains only the session ID, opaque reference, requested checks, normalized status, short-lived client secret and verification URL, and creation time. It never returns or expands verified_outputs, last_verification_report, last_error, provided_details, document images, selfies, biometric data, national identifiers, or VerificationReports. Applications must not log or persist the client secret or verification URL and remain responsible for consent, retention, access control, and regulatory compliance.

Cancellation is irreversible and Stripe permits it only while a session requires input. Redaction is also irreversible, can take several days, erases metadata, and eventually emits Stripe’s redacted event. Poll retrieveIdentityVerification until the normalized status is redacted when completion matters to the application.

Provider webhook management

StripeProvider implements remote webhook endpoint create, list, retrieve, update, and delete through ProviderWebhookEndpointManagementCapable. Write operations forward ctx.idempotencyKey; list uses bounded auto-pagination.

Stripe returns a signing secret only when an endpoint is created, so signingSecret is normally null on list, retrieve, and update responses. Signing-secret rotation is not exposed because Stripe does not provide an equivalent operation.

Payment webhook reconciliation

StripeProvider implements PaymentWebhookCapable so the generic webhook pipeline can reconcile local payment rows after signature verification. reconcilePayment(verified) is synchronous and pure. It returns null for non-payment events or malformed payment payloads.

Supported payment reconciliation sources:

Stripe event typeLocal payment idDomain status
payment_intent.succeededPaymentIntent.idsucceeded
payment_intent.payment_failedPaymentIntent.idfailed
payment_intent.canceledPaymentIntent.idcanceled
payment_intent.processingPaymentIntent.idprocessing
payment_intent.amount_capturable_updatedPaymentIntent.idprocessing
charge.succeededCharge.payment_intentsucceeded
charge.failedCharge.payment_intentfailed
charge.pendingCharge.payment_intentprocessing
checkout.session.completed with payment_status: 'paid'Checkout.Session.idsucceeded
checkout.session.async_payment_succeededCheckout.Session.idsucceeded
checkout.session.async_payment_failedCheckout.Session.idfailed
checkout.session.expiredCheckout.Session.idcanceled

The checkout-session id is used because Payable records redirect-checkout pending payments under the session id before the browser leaves the application. The pipeline still gates the update through PaymentStateMachine, so stale Stripe events cannot move a final local payment into an invalid state.

Webhook verification

StripeWebhookVerifier (stripe-webhook-verifier.ts) wraps the SDK’s async signature check:

async verify(stripe: Stripe, payload: string, signature: string): Promise<Stripe.Event> {
  try {
    return await stripe.webhooks.constructEventAsync(payload, signature, this.secret);
  } catch (error) {
    throw new InvalidWebhookSignatureError('stripe', { cause: error });
  }
}

The raw request body (string payload) and the Stripe-Signature header value are passed to constructEventAsync with the configured webhookSecret. The signature must be computed over the exact raw bytes, so the adapter must hand over the unparsed body. On any failure the verifier throws InvalidWebhookSignatureError with provider: 'stripe' and the original error as cause.

verifyWebhook then returns a VerifiedWebhook with providerEventId (event.id), the raw type, the normalizedType, and event.data.object as data.

Failure scenarios and recovery

ScenarioSymptomRecovery
Invalid webhook signatureInvalidWebhookSignatureError (provider: 'stripe') from verifyWebhookConfirm webhookSecret matches the Stripe endpoint’s secret and that the adapter forwards the raw, unmodified body. Stripe retries the delivery.
Stripe API errorThe underlying SDK error propagates from the called methodRetry a keyed operation with the same key and input. Reconcile an unkeyed mutation before retrying.
Price has no integer amountPayableError PROVIDER_PRICE_AMOUNT_UNRESOLVABLE from toPriceDTOUse integer minor-unit prices; non-integer unit_amount_decimal is rejected.
Currency exponent mismatchPayableError PROVIDER_CURRENCY_EXPONENT_MISMATCH from stripeAmount / stripeMoneyThe amount cannot be rescaled to Stripe’s currency exponent without precision loss; use an amount that fits the currency’s minor-unit granularity.
Invoice has no PDFPayableError INVOICE_PDF_UNAVAILABLE from downloadInvoicePdfWait until Stripe finalizes the invoice; draft invoices have no invoice_pdf.
Invoice PDF URL not httpsPayableError INVOICE_PDF_UNTRUSTED_URL from downloadInvoicePdfStripe returned a non-https:// invoice_pdf; the download is refused.
Invoice PDF download failsPayableError INVOICE_PDF_DOWNLOAD_FAILED with { status }Transient; retry the download. The status code is in the error context.
Invoice PDF too largePayableError INVOICE_PDF_TOO_LARGE with { bytes }The PDF exceeds the 10 MB cap (checked against content-length and the streamed body).

Configuration example

import { createPayable } from '@akira-io/payable';
import { StripeProvider } from '@akira-io/payable';

const stripe = new StripeProvider({
  secretKey: process.env.STRIPE_SECRET_KEY!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
});

const payable = createPayable({
  providers: { stripe },
  // storage, queue, events, clock ...
});

// Charge a customer (ChargeCapable):
await payable
  .customer({ billableType: 'User', billableId: '1', email: 'jane@example.com' })
  .charge({ amount: Money.of(1500, 'USD'), reference: 'order-1' });