PayFanout API reference
    Preparing search index...

    Class SubscriptionManager

    Index
    • Immediate cancel stops everything now (the rest of the paid window is forfeit — no refund is initiated; use refundPayment separately if owed). atPeriodEnd lets the paid window run out, then ends without charging. A renewal charge still awaiting a definitive answer stays on the canceled record (renewalAttempt.replay) for reconciliation.

      Parameters

      • id: string
      • options: { atPeriodEnd?: boolean } = {}

      Returns Promise<SubscriptionRecord>

    • THE cron entry point — run it every few minutes/hours from the host's scheduler. Idempotent and crash-safe: renewal idempotency keys are derived from (subscription id, period, attempt), so a crashed run that re-charges after the store missed an update dedupes at the PSP.

      Prefers store.listDue (host-indexed batches) when implemented, falling back to per-status list() scans (active, trialing, past_due); either way every candidate's due-ness is re-checked here before any charge.

      Overlapping runs that send the same renewal converge on one PSP charge. When one sends another request under the same key (a card or plan set between their reads), the charge is pinned rather than moved to a new key wherever the PSP's refusal leaves the outcome open, as the Stripe and Paysafe adapters' refusals do while the key's charge may have gone through; two such requests reaching Paysafe before either is filed can both be charged. Events are at-least-once (see onEvent). Hosts wanting single-run semantics should hold a lock around this call.

      Parameters

      • Optionalat: string | Date

      Returns Promise<ChargeDueResult>

    • Halts billing without ending the subscription: the cron skips paused records and dunning stops (nextRetryAt is cleared; failedAttempts, any pendingRenewal and any renewalAttempt survive untouched — an unresolved renewal still resolves via resolvePendingRenewal, but a paused record is never re-charged). Pausing a paused record is a no-op; canceled records cannot pause.

      Parameters

      • id: string

      Returns Promise<SubscriptionRecord>

    • Applies the final outcome of a renewal charge whose result the manager does not know. Two kinds exist:

      • A charge that resolved as "processing" (async rails, pendingRenewal). Wire it to the host's payment webhook ingress (payment.succeeded / payment.failed with a matching pspPaymentId). Until it runs the subscription is frozen: chargeDueSubscriptions never charges on top of an unresolved renewal.
      • A charge that ended without a definitive answer (renewalAttempt.replay). The cron replays it by itself while that is safe, then leaves it frozen. Settle it with the key the charge was sent under: every renewal charge carries it in its metadata as payfanout_renewal_key, so a payment webhook names its own attempt. A failure webhook without that key cannot settle a pin: the payment it names may be an earlier attempt's.

      Replay-safe for re-delivered webhooks: a success whose payment is still lastPaymentId is a no-op, and so is a failure whose idempotencyKey names an attempt of the current period that already failed. Anything else that settles nothing, a failure re-delivered without its key included, is refused with invalid_request. A settled failure counts as a failed attempt for dunning. Resolving never reactivates: a record paused (or canceled) in the meantime keeps its status — a success still advances the paid-through window (the money moved), a failure is recorded without entering dunning.

      Parameters

      • id: string
      • outcome: {
            error?: { code?: UnifiedErrorCode; message?: string };
            idempotencyKey?: string;
            pspPaymentId?: string;
            status: "succeeded" | "failed";
        }
        • Optionalerror?: { code?: UnifiedErrorCode; message?: string }
        • OptionalidempotencyKey?: string

          The renewal key the charge was sent under (its payfanout_renewal_key metadata). Settles a charge without a definitive answer when it equals renewalAttempt.replay.idempotencyKey. Not consulted while a pendingRenewal is awaited — pspPaymentId guards that one.

        • OptionalpspPaymentId?: string

          From the webhook — guards against applying a different payment's outcome. Required to settle a charge without a definitive answer as succeeded: it becomes lastPaymentId, and is taken on trust.

        • status: "succeeded" | "failed"

      Returns Promise<SubscriptionRecord>

    • Reactivates a paused subscription. Still paid through -> just "active", nothing is charged. Lapsed -> ONE immediate charge (occurrence "recurring", the caller's idempotencyKey — retry a storage failure with the SAME key so the PSP replays instead of re-charging) re-anchors the billing cycle at the resume instant. A failed charge leaves the record paused with lastError — no dunning — and throws. A charge resolving as "processing" freezes the still-paused record under pendingRenewal: resolve it, then resume again (paid through by then, so no second charge). A record paused over a renewal charge without a definitive answer (renewalAttempt.replay) cannot resume until resolvePendingRenewal settles it: that charge may have paid the period, and a replay long after the fact may reach a PSP that no longer holds its key.

      Parameters

      • id: string
      • options: { idempotencyKey: string }

      Returns Promise<SubscriptionRecord>

    • Plan/instrument changes apply from the NEXT period (no proration — the already-paid window stands). Changing the token also clears dunning: a fresh card deserves a fresh chance at the next renewal. The period's attempt numbers carry on, so the new card is charged under a key the period has not used, and a renewal charge still awaiting a definitive answer (renewalAttempt.replay) is replayed with its original card and amount before the new ones are charged. Emits "subscription.updated" (renewals never do).

      Parameters

      • id: string
      • updates: {
            metadata?: Record<string, string>;
            plan?: SubscriptionPlan;
            savedPaymentMethodToken?: string;
        }

      Returns Promise<SubscriptionRecord>