PayFanout API reference
    Preparing search index...

    The Stripe server adapter: PaymentIntents, Refunds, Customers and Billing over the Stripe Node SDK, confirm-on-client.

    Amounts cross it in PayFanout's minor units (core's getCurrencyExponent, which follows ISO 4217) and are converted where Stripe's API reads a currency in other units (see "Currencies with Stripe-specific units" in the setup guide). ISK is sent multiplied by 100 and reported divided by 100, as Stripe takes it as a two-decimal value whose decimals are always 00; an ISK amount Stripe reports that is not a multiple of 100 cannot be reported and fails the read. MGA, a zero-decimal currency at Stripe, is sent divided by 100 and reported multiplied by 100, and an MGA amount that is not whole ariary (a multiple of 100) is refused with invalid_request before any request. Where the caller names the currency (session creation, an update naming amount and currency, saved-method charges, native subscriptions), three-decimal amounts must be multiples of 10, a rule from an earlier version of Stripe's currencies page that the adapter keeps.

    UGX is refused, as Stripe's currencies page gives it both units: a call that would send a UGX amount rejects with invalid_request before the request that would carry it; reads of a UGX record reject with unsupported_operation, as do the answers of calls that went through without sending an amount (a capture, cancellation or refund with no amount, a metadata-only update, a subscription cancellation), marked outcomeUnknown; a list page holding one fails whole, its raw naming each such record and carrying the page's nextCursor; and webhook and polled events in UGX carry no amount. A zero-amount session is a SetupIntent, which carries no amount or currency, and is left alone in every currency. UGX is declared in unsupportedCurrencies, so the router skips Stripe for a UGX session and PaymentService refuses one with unsupported_operation, zero-amount sessions included (from the @payfanout/server release that reads the field).

    Earlier releases sent UGX amounts, and MGA amounts that are not whole ariary, unconverted, so a retry under the same key after the upgrade may meet money they already moved. Their refusal is therefore marked outcomeUnknown on chargeSavedPaymentMethod and createNativeSubscription, whose earlier attempt the adapter cannot read back; on capturePayment, unless the PaymentIntent it reads is still requires_capture; on refundPayment, unless the PaymentIntent's refund list, read on the way to the refusal, is complete and holds only failed or canceled refunds; and on updatePaymentSession when the update names UGX or sends an amount for a UGX PaymentIntent (a currency change away from a UGX PaymentIntent, whose amount cannot be kept, is refused final). These refusals, final or not, ask the host to check the Stripe Dashboard for that request under the idempotency key before sending another.

    updatePaymentSession, capturePayment and refundPayment read the PaymentIntent first whenever the currency of the amount they send is not in the call (see each method), one more request each time.

    Implements

    Index
    pspName: "stripe" = STRIPE_PSP_NAME
    • Resolves "canceled" only when the PSP confirms it. Under getCapabilities().modificationOutcome "asynchronous" the PSP merely acknowledges the request and the outcome arrives by webhook, so the returned status is "processing" — never an unconfirmed terminal state.

      Parameters

      • pspPaymentId: string
      • idempotencyKey: string

      Returns Promise<PaymentInfo>

    • A capture that states amount first reads the PaymentIntent (one more request) to send it in Stripe's units for the payment's currency, and refuses one Stripe cannot be sent (see the class notes) before the capture. An earlier release sent such captures unconverted, so the refusal of a UGX amount, or of an MGA amount that is not whole ariary, is marked outcomeUnknown unless the PaymentIntent read is still requires_capture, which shows nothing was captured; either way it asks the host to check the Stripe Dashboard under the key before sending another. Without amount, Stripe captures its own amount_capturable and nothing is read first.

      Parameters

      • pspPaymentId: string
      • amount: number | undefined
      • idempotencyKey: string

      Returns Promise<PaymentInfo>

    • POST /v1/subscriptions against customer + default_payment_method (the vaulted token — it must belong to that customer, so pspCustomerId is required here). planId (when given) is an existing Stripe Price id; without one the adapter builds the price inline via items[].price_data, which requires an existing Product — created on the fly (name merchantRefNum, falling back to "Subscription"; it shows on customer invoices) under a derived idempotency key so replays reuse it. off_session: true + payment_behavior: "error_if_incomplete": a first invoice that cannot be paid rejects with the mapped card error instead of leaving an incomplete subscription behind — server-only creation has no customer present to finish authentication. startAt maps to a future billing_cycle_anchor with proration_behavior: "none", making the span until the anchor free and the first full invoice due at startAt.

      Returns Promise<NativeSubscriptionRecord>

    • A refund that states amount first reads the PaymentIntent (one more request) to send it in Stripe's units for the payment's currency, and refuses one Stripe cannot be sent (see the class notes) before the refund. An earlier release sent such refunds unconverted, so on the way to refusing a UGX amount, or an MGA amount that is not whole ariary, the adapter lists the PaymentIntent's refunds (GET /v1/refunds, one request more): the refusal is final only when that list is complete and holds nothing but failed or canceled refunds, and is marked outcomeUnknown otherwise, including when the list cannot be read. Either way it asks the host to check the Stripe Dashboard under the key before sending another. A refund that is sent lists nothing. Without amount, Stripe refunds what remains and nothing is read first.

      Parameters

      Returns Promise<RefundResult>

    • Cart total changed? Stripe PaymentIntents update in place — same pspSessionId, same clientSecret, no client remount needed (the Payment Element re-fetches the amount on confirm).

      Stripe's units for the amount depend on the currency, so an update that carries amount without currency, or currency without amount, first reads the PaymentIntent (GET /v1/payment_intents/:id, one more request): the first to learn the currency the amount is sent in, the second to keep the session's amount, in PayFanout's minor units, across the change. A currency change always sends the kept amount converted for the new currency, so its parameters depend only on the state it asks for and a retry under the same key sends the same request: a session of amount 1000 moved from ISK to USD still has amount 1000 (USD 10.00), where Stripe alone would keep its 100000 (USD 1,000.00). An update that names both, or neither, reads nothing first. Refusals of the amount (UGX, MGA amounts that are not whole ariary, and, when the update names both amount and currency, three-decimal amounts that are not multiples of 10) come before the update is sent. A UGX refusal, whether the update names UGX or sends an amount for a UGX PaymentIntent, is marked outcomeUnknown, as an earlier release sent such updates unconverted under the same key.

      A currency change without an amount writes the amount it read, so an amount update landing between that read and its write is overwritten: send the updates of one session one at a time.

      Parameters

      Returns Promise<PaymentSession>

    • Webhook handling — same adapter, separate concerns. Both async: future PSPs may need remote key retrieval; the contract stays uniform. MUST operate on the RAW request body bytes/string — re-serializing a parsed body breaks signatures (the conformance suite tests this).

      Parameters

      • rawBody: string
      • headers: Record<string, string>

      Returns Promise<boolean>