diff --git a/README.md b/README.md
index c9ca5b4..c03eb4e 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@
-# @antelopejs/interface-subscriptions
+# `@antelopejs/interface-subscriptions`
-Provider-agnostic subscriptions for AntelopeJS. Bill a saved payment method
-every period — with trials, a billing anchor, cancellation now or at period end,
-and `past_due` reporting — without naming a provider, and without every
-consumer rebuilding a billing cycle of its own.
+The AntelopeJS interface for subscriptions that work with any payment provider.
+Bill a saved payment method every period, with trials, a billing anchor,
+cancellation now or at period end, and `past_due` reporting. Your code names no
+provider, and no consumer has to build a billing cycle of its own.
## Installation
@@ -21,19 +21,20 @@ pnpm add @antelopejs/interface-subscriptions
This package declares the contract and ships no billing logic. Wire a module
that implements it:
-- a provider that fires renewals itself implements it natively (Stripe, PayPal,
- Mollie);
-- for any other `@antelopejs/interface-payment` provider, wire
- `@antelopejs/payment-subscriptions`, which runs the renewal clock itself on
+- A payment module whose provider fires renewals itself implements it natively
+ (Stripe, PayPal, Mollie).
+- For any other `@antelopejs/interface-payment` provider, wire
+ `@antelopejs/payment-subscriptions`. It runs the renewal clock itself, with
saved methods and off-session charges.
-A payment module that cannot do subscriptions simply does not implement this
-interface. There is no half-honoured contract and no `UnsupportedOperationError`
-here.
+A payment module that cannot do subscriptions does not implement this
+interface. No implementation keeps only part of the contract, and the interface
+has no `UnsupportedOperationError`.
-For Stripe, native subscriptions in `@antelopejs/stripe` are recommended.
-`@antelopejs/payment-subscriptions` is an explicit alternative, not an automatic
-fallback. Wire one renewal owner per subscription, never both.
+For Stripe, use the native subscriptions in `@antelopejs/stripe`.
+`@antelopejs/payment-subscriptions` is an alternative you choose explicitly. It
+does not take over automatically. Wire one renewal owner per subscription, never
+both.
## Choose a task
@@ -42,10 +43,10 @@ fallback. Wire one renewal owner per subscription, never both.
- Implement a billing provider: [implementation guide](./docs/4.implementing.md).
- Choose native or generic billing: [provider capabilities](./docs/5.provider-capability-matrix.md).
-Replay protection lasts only for the provider-defined replay window. Persist
-operation keys, subscription IDs and payment IDs. After expiration, applications
-must reconcile uncertain payments with provider records before retrying; reusing
-a key is not indefinite duplicate-charge protection.
+Replay protection lasts only for the replay window that the provider defines.
+Persist operation keys, subscription IDs, and payment IDs. After the window
+expires, reconcile uncertain payments with the provider records before you
+retry. A reused key does not protect against duplicate charges forever.
## What it covers
@@ -56,12 +57,13 @@ a key is not indefinite duplicate-charge protection.
| Recovery | `UpdateSubscriptionPaymentMethod` |
| Events | `SubscriptionEvents` (a registering proxy, not a webhook route) |
-The portable core is small on purpose: amount, interval and interval count,
-billing anchor, trial end, status including `past_due`, cancel now or at period
-end, and the current period. Proration, dunning schedules, tax, coupons, metered
-billing and multiple items per subscription are out, because the generic
-fallback cannot honour them and a contract that only some implementations keep
-is a lie. For those, use the provider's own interface.
+The portable core is small on purpose. It covers the amount, the interval and
+interval count, the billing anchor, the trial end, the status including
+`past_due`, cancellation now or at period end, and the current period.
+Proration, dunning schedules, tax, coupons, metered billing, and multiple items
+per subscription are out. The generic fallback cannot honour them, and a
+contract that only some implementations keep misleads its callers. For those
+features, use the provider's own interface.
## Quick start
@@ -104,7 +106,7 @@ export async function startSignup(
export async function finishSignup(paymentId: string, accountId: string) {
const payment = await GetPayment(paymentId);
if (payment.status !== "succeeded" && payment.status !== "authorized") {
- return undefined; // Still pending/action required, or failed/canceled: do not subscribe.
+ return undefined; // pending, requires_action, failed, or canceled: do not subscribe.
}
if (!payment.customer) throw new Error("Signup payment has no customer");
@@ -125,18 +127,20 @@ export async function finishSignup(paymentId: string, accountId: string) {
}
```
-Use an existing payment-interface customer and a provider-issued method token.
-Persist a unique `signupId` and the returned `paymentId` against the authenticated
-account. Replace the return URL with your own route and send the payer to
-`redirectUrl` when action is required. If the payment is pending, wait for a
-verified `payment.succeeded` or `payment.authorized` webhook before retrying
-`finishSignup`; failed or canceled payments must not create subscriptions.
-
-Call `finishSignup` with the saved payment ID and account ID, using the same
-provider account throughout. The saved-method payment and the first subscription
-period are separate charges (€1 then €19 here); the first period is charged
-off-session before `CreateSubscription` resolves. Adapt those amounts to your
-billing policy. Register an idempotent handler for subsequent lifecycle events:
+Use an existing payment-interface customer and a method token that the
+provider issued. Persist a unique `signupId` and the returned `paymentId`
+against the authenticated account. Replace the return URL with your own route.
+When action is required, send the payer to `redirectUrl`. If the payment is
+pending, wait for a verified `payment.succeeded` or `payment.authorized` webhook
+before you retry `finishSignup`. A failed or canceled payment must not create a
+subscription.
+
+Call `finishSignup` with the saved payment ID and the account ID, and use the
+same provider account throughout. The payment that saves the method and the
+first subscription period are separate charges, €1 then €19 here. The
+implementation charges the first period off-session before `CreateSubscription`
+resolves. Adapt those amounts to your billing policy. Register an idempotent
+handler for later lifecycle events:
```ts
import {
@@ -151,8 +155,9 @@ SubscriptionEvents.register("billing", async (event) => {
});
```
-`grantOrRevokeAccess` stands for your application's access policy, and its `declare` line only gives it a type. Deduplicate
-events by `event.id` as described in [Events](./docs/3.events.md).
+`grantOrRevokeAccess` stands for your application's access policy, and its
+`declare` line only gives it a type. Deduplicate events by `event.id`, as
+[Events](./docs/3.events.md) describes.
## Compatibility
@@ -165,36 +170,40 @@ events by `event.id` as described in [Events](./docs/3.events.md).
-These are the peer requirements in `package.json`. Development uses the same
-payment-interface semver range. Check your implementing module's requirements
-too. `@antelopejs/core` is a development test runner, not a runtime peer.
+The table lists the peer requirements in `package.json`. Development uses the
+same semver range for the payment interface. Check the requirements of your
+implementing module too. `@antelopejs/core` is a test runner for development,
+not a runtime peer.
## Conventions
-- **Built on the payment interface's portable pair**: one on-session payment that
- saves a method, then off-session charges. Customers, methods and payments are
- `@antelopejs/interface-payment` identifiers from the same account.
-- **An idempotency key on every mutating call**, positionally.
+- **Built on the portable pair of the payment interface.** One on-session
+ payment saves a method, then off-session charges use it. Customers, methods,
+ and payments are `@antelopejs/interface-payment` identifiers from the same
+ account.
+- **An idempotency key on every mutating call.** It is a positional argument.
- **A declined renewal is a status, not an exception.** Only a declined _first_
period rejects, with `SubscriptionPaymentError`, and creates nothing.
-- **`past_due` is portable to report; the retry policy is not.**
+- **`past_due` is portable to report.** The retry policy is not portable.
- **`vendorData` is a read-only echo of JSON values.** No request accepts it.
-- **An optional trailing `provider`** selects the payment account, as in the
- payment interface.
+- **An optional trailing `provider` selects the payment account.** The payment
+ interface uses the same argument.
## Documentation
-| Document | Contents |
-| -------------------------------------------------------------------- | --------------------------------------------------------------- |
-| [Introduction](./docs/1.introduction.md) | Why a second interface, the portable core, who fires renewals |
-| [Subscriptions](./docs/2.subscriptions.md) | Creating, periods, trials and anchors, statuses, cancelling |
-| [Events](./docs/3.events.md) | The event set, delivery, and how it relates to payment webhooks |
-| [Implementing](./docs/4.implementing.md) | Native and fallback implementations, the conformance suite |
-| [Provider capability matrix](./docs/5.provider-capability-matrix.md) | Which providers fire renewals themselves, and what that means |
+| Document | Contents |
+| -------------------------------------------------------------------- | ------------------------------------------------------------- |
+| [Introduction](./docs/1.introduction.md) | Why a second interface, who fires renewals, the portable core |
+| [Subscriptions](./docs/2.subscriptions.md) | Creation, periods, trials and anchors, statuses, cancellation |
+| [Events](./docs/3.events.md) | The event set, delivery, and the relation to payment webhooks |
+| [Implementing](./docs/4.implementing.md) | Native and fallback implementations, the conformance suite |
+| [Provider capability matrix](./docs/5.provider-capability-matrix.md) | Which providers fire renewals themselves, and what that means |
## Development
-Use Node.js 24 and pnpm 10.6.5. Install the published payment interface from npm using the frozen lockfile:
+Use the Node.js version in `.nvmrc` and the pnpm version in the `packageManager`
+field of `package.json`. Install the published payment interface from npm with
+the frozen lockfile:
```bash
pnpm install --frozen-lockfile
@@ -204,16 +213,18 @@ pnpm format:check
pnpm knip
```
-CI runs the same checks against the published dependency without Git submodules or workspace overrides.
+CI runs the same checks against the published dependency, without Git
+submodules or workspace overrides.
## Conformance suite
`src/tests/` ships in the package as `dist/tests` and runs against whichever
-module implements the interface. It is much weaker than the payment suite, and
-on purpose: a test cannot wait a month for a renewal, so for a native provider it
-covers creation, reading, cancellation, method changes and request validation —
-not renewals. The generic fallback's own suite drives an injected clock through
-every renewal path and is the executable specification for the rest.
+module implements the interface. It checks much less than the payment suite, on
+purpose. A test cannot wait a month for a renewal. For a native provider, the
+suite covers creation, reading, cancellation, method changes, and request
+validation, but not renewals. The generic fallback has its own suite, which
+drives an injected clock through every renewal path. That suite is the
+executable specification for the rest.
| Variable | Purpose |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
@@ -224,7 +235,10 @@ every renewal path and is the executable specification for the rest.
## Contributing
-Read [CONTRIBUTING.md](./CONTRIBUTING.md) and run the [development checks](#development) before opening a pull request. Local contract tests do not require provider credentials; the provider conformance suite is a separate integration check.
+Read [CONTRIBUTING.md](./CONTRIBUTING.md) and run the
+[development checks](#development) before you open a pull request. The local
+contract tests need no provider credentials. The provider conformance suite is a
+separate integration check.
## License
diff --git a/docs/1.introduction.md b/docs/1.introduction.md
index 60d99a4..6045447 100644
--- a/docs/1.introduction.md
+++ b/docs/1.introduction.md
@@ -1,71 +1,78 @@
-# Introduction
-
-`@antelopejs/interface-subscriptions` defines provider-agnostic recurring
-billing for AntelopeJS applications. A backend written against it bills a saved
-payment method every period and learns what happened — paid, failed, ended —
-without naming a provider, and without running a billing cycle of its own.
+---
+title: Introduction
+description: Why recurring billing is its own interface, who fires each renewal, and what the portable core covers.
+mode: explanation
+---
+
+`@antelopejs/interface-subscriptions` defines recurring billing for AntelopeJS
+applications that works with any payment provider. A backend written against it
+bills a saved payment method every period. It learns whether each period was
+paid, failed, or ended. It names no provider and runs no billing cycle of its
+own.
## Why a second interface
-Every call in `@antelopejs/interface-payment` is request and response against
-the provider. A subscription is different in kind: it is a long-lived object
-that must be acted on **when nobody is calling**. It needs two things the
-payment contract never needed:
+Every call in `@antelopejs/interface-payment` is a request to the provider and
+its response. A subscription is a different kind of thing. It is a long-lived
+object that something must act on **when nobody is calling**. It needs two
+things the payment contract never needed:
-1. **Durable state** — the subscription, its current period, its status.
-2. **The passage of time** — something must fire at each renewal.
+1. **Durable state.** The subscription, its current period, and its status.
+2. **The passage of time.** Something must fire at each renewal.
A provider with native subscriptions supplies both. A provider without them
-leaves the merchant to fire each charge, and then something has to supply both
-on the merchant's side. That something is a module with a database and a clock,
-which is a subscriptions module's business and not a payment module's — so the
-contract is its own interface, and:
+leaves the merchant to fire each charge, so something on the merchant's side has
+to supply both. That something is a module with a database and a clock, which
+belongs in a subscriptions module and not in a payment module. For that reason
+the contract is its own interface, with these results:
-- a payment module whose provider cannot do subscriptions does not implement it,
- rather than implementing it badly;
-- the generic fallback, `@antelopejs/payment-subscriptions`, can depend on a
- database and a scheduler without forcing either on every payment consumer;
-- consumers that do not bill recurrently do not carry the surface.
+- A payment module whose provider cannot do subscriptions does not implement the
+ interface. It does not implement it badly either.
+- The generic fallback, `@antelopejs/payment-subscriptions`, can depend on a
+ database and a scheduler without forcing either on every payment consumer.
+- Consumers that do not bill on a schedule do not carry the extra calls.
## Who fires the renewal
-The question that sorts providers is not "is there a subscription object" but
-"who fires each renewal charge". That is exactly what the fallback has to
-supply.
+The question that sorts providers is "who fires each renewal charge". Whether
+the provider has a subscription object does not decide it. The fallback has to
+supply exactly that renewal charge.
| The provider fires renewals | The merchant fires them |
| --------------------------- | ----------------------------- |
| Stripe, PayPal, Mollie | Adyen, Checkout.com, Mangopay |
-The split is even, so the fallback is half the providers rather than a rare edge.
-[The capability matrix](./5.provider-capability-matrix.md) has the detail.
+The split is even, so the fallback serves half the providers. It is not a rare
+edge case. [The provider capability matrix](./5.provider-capability-matrix.md)
+has the detail.
## The portable core
-Expressible everywhere, including by the fallback:
+Every implementation can express these, the fallback included:
-- an amount, an interval and an interval count;
-- a billing anchor (when the first paid period starts) or a trial end;
-- a status, including `past_due`;
-- cancelling now, or at the end of the period paid for;
-- the current period's start and end.
+- An amount, an interval, and an interval count.
+- A billing anchor, which is when the first paid period starts, or a trial end.
+- A status, including `past_due`.
+- Cancellation now, or at the end of the period paid for.
+- The start and the end of the current period.
-Deliberately not modelled, because the fallback cannot honour them and a
-contract only some implementations keep is a lie: proration, dunning schedules,
-tax, coupons and discounts, metered billing, and more than one item per
-subscription. A backend that needs one of these uses the provider's own
-interface (`@antelopejs/interface-stripe`) alongside this one.
+The contract leaves some features out on purpose. The fallback cannot honour
+them, and a contract that only some implementations keep misleads its callers.
+The features left out are proration, dunning schedules, tax, coupons and
+discounts, metered billing, and more than one item per subscription. A backend
+that needs one of these uses the provider's own interface, such as
+`@antelopejs/interface-stripe`, alongside this one.
## Built on the payment interface
A subscription charges a method saved through `@antelopejs/interface-payment`,
-off-session. That pair — one on-session payment that saves a method, then any
-number of off-session charges against it — is the payment interface's portable
-recurring primitive, and this interface adds nothing underneath it.
+off-session. The payment interface supports recurring charges portably with one
+pair. One on-session payment saves a method, then any number of off-session
+charges use it. This interface adds nothing underneath that pair.
-Identifiers cross the two interfaces unchanged: a subscription's `customer`,
-`paymentMethod` and `latestPayment` are payment-interface identifiers from the
-same account, and `provider` selects that account in both.
+Identifiers cross the two interfaces unchanged. The `customer`, `paymentMethod`,
+and `latestPayment` of a subscription are payment-interface identifiers from the
+same account. `provider` selects that account in both interfaces.
## Installation
@@ -77,22 +84,23 @@ Wire a module that implements it alongside a payment module.
## Conventions
-**Amounts are the payment interface's `Amount`**: integer minor units and an
-uppercase ISO-4217 code.
+**Amounts are the `Amount` of the payment interface.** An amount is an integer
+of minor units and an uppercase ISO-4217 code.
-**Times are Unix epoch milliseconds**, and periods are counted in UTC.
+**Times are Unix epoch milliseconds.** Implementations count periods in UTC.
-**Every mutating call takes an idempotency key**, positionally. Replaying it
-returns the original outcome within the provider-defined replay window,
-including rejections where the provider retains them. After expiration,
-applications must reconcile uncertain payments before retrying; replay
-protection is not indefinite.
+**Every mutating call takes an idempotency key.** It is a positional argument.
+Within the replay window that the provider defines, a replayed key returns the
+original outcome. The outcome includes a rejection when the provider retains
+it. After the window expires, reconcile uncertain payments before you retry.
+Replay protection does not last forever.
**A failed renewal is a status, not an exception.** The subscription goes
-`past_due` and a `subscription.payment_failed` event is raised. Only a declined
-_first_ period rejects, because then there is no subscription to put in a state.
-
-**`vendorData` is a read-only echo.** An implementation may surface what its
-provider said; no request accepts provider-specific settings, because a
-`Record` that compiles against every provider and works on one
-hides exactly the coupling this interface exists to prevent.
+`past_due`, and the implementation raises a `subscription.payment_failed` event.
+Only a declined _first_ period rejects, because then no subscription exists to
+put in a state.
+
+**`vendorData` is a read-only echo.** An implementation may expose what its
+provider said. No request accepts settings for one provider. A
+`Record` that compiles against every provider but works on only
+one would hide the coupling that this interface exists to prevent.
diff --git a/docs/2.subscriptions.md b/docs/2.subscriptions.md
index 7eb0154..3e72327 100644
--- a/docs/2.subscriptions.md
+++ b/docs/2.subscriptions.md
@@ -1,23 +1,31 @@
-# Subscriptions
+---
+title: Subscriptions
+description: How a subscription is created, how its periods, trials and anchors work, what each status means, and how cancellation and errors behave.
+mode: reference
+---
## Creating one
-The payer is not present when a subscription charges, so the mandate is taken
+The payer is not present when a subscription charges, so you take the mandate
first, through the payment interface. Create an on-session payment with
-`savePaymentMethod: true`, `customer` and `returnUrl`, and persist its ID.
-If it returns `requires_action`, send the payer to `redirectUrl`. Do not list
-saved methods or start the subscription while the payment is still pending or
-requires action, or if it failed or was canceled.
+`savePaymentMethod: true`, `customer`, and `returnUrl`, and persist its ID. If
+it returns `requires_action`, send the payer to `redirectUrl`. Do not list saved
+methods or start the subscription in these cases:
-After a verified `payment.succeeded` or `payment.authorized` event, or a server
-read with `GetPayment` reporting one of those states, use `ListPaymentMethods`
-to find the saved method matching that payment's `paymentMethod`. Do not select
-an unrelated method just because it appears first. The [README quick start](../README.md#quick-start)
-shows the checkout and reconciliation steps separately.
+- The payment is still `pending` or `requires_action`.
+- The payment is `failed` or `canceled`.
-`savePaymentMethod` cannot be combined with `offSession` — a mandate can only be
-taken from a payer who is there. That first payment is an ordinary one-off; the
-subscription starts after it, and its first period is a separate charge.
+Wait for a verified `payment.succeeded` or `payment.authorized` event, or for a
+server read with `GetPayment` that reports one of those states. Then use
+`ListPaymentMethods` to find the saved method that matches the `paymentMethod`
+of that payment. Do not select an unrelated method because it appears first. The
+[README quick start](../README.md#quick-start) shows the checkout and the
+reconciliation as separate steps.
+
+`savePaymentMethod` cannot be combined with `offSession`, because only a payer
+who is present can give a mandate. That first payment is an ordinary one-off
+payment. The subscription starts after it, and its first period is a separate
+charge.
```ts
import type { PaymentMethod } from "@antelopejs/interface-payment";
@@ -42,45 +50,47 @@ export async function subscribe(
}
```
-Without a trial or a billing anchor, the first period is charged off-session
-before `CreateSubscription` resolves:
+Without a trial or a billing anchor, the implementation charges the first period
+off-session before `CreateSubscription` resolves:
| First charge | Result |
| ------------------------ | --------------------------------------------------------------------------------------------- |
| Succeeded | An `active` subscription |
-| Still settling (a debit) | An `active` subscription; if the charge later fails it goes `past_due` |
+| Still settling (a debit) | An `active` subscription. If the charge later fails, it goes `past_due`. |
| Declined | Rejects with `SubscriptionPaymentError`, carrying the declined `Payment`. Nothing is created. |
-Replaying the idempotency key within the provider-defined replay window returns
-the saved outcome, including a rejection where retained by the provider.
-After expiration, applications must reconcile uncertain payments and subscription
-creation before retrying; the same key can create another charge.
+Within the replay window that the provider defines, a replayed idempotency key
+returns the saved outcome. The outcome includes a rejection when the provider
+retains it. After the window expires, reconcile uncertain payments and
+subscription creation before you retry, because the same key can create another
+charge.
## Periods
-A period is `intervalCount` × `interval`, where the interval is `day`, `week`,
-`month` or `year`. Every implementation accepts periods up to a year; a longer
-one may be refused with `InvalidSubscriptionRequestError`.
+A period is `intervalCount` times `interval`. The interval is `day`, `week`,
+`month`, or `year`. Every implementation accepts periods up to a year, which
+`MAX_PORTABLE_PERIOD_DAYS` sets at 366 days. An implementation may refuse a
+longer period with `InvalidSubscriptionRequestError`.
-Periods are counted from the billing anchor in calendar units, in UTC, and never
-from the end of the previous period. A monthly subscription anchored on 31
-January renews on 28 February (29 in a leap year), then on 31 March — not on the
-28th of every month from then on.
+Implementations count periods from the billing anchor in calendar units, in UTC,
+and never from the end of the previous period. A monthly subscription anchored
+on 31 January renews on 28 February, or 29 February in a leap year. It then
+renews on 31 March, not on the 28th of every month from then on.
`currentPeriodStart` and `currentPeriodEnd` describe the period most recently
-billed for. The next charge is made at `currentPeriodEnd`.
+billed for. The next charge happens at `currentPeriodEnd`.
## Trials and billing anchors
Both defer the first charge, and they are mutually exclusive.
-- **`trialEnd`**: the subscription is `trialing` until then, nothing is charged,
- and the first paid period starts at the trial end.
-- **`billingAnchor`**: the subscription is `active` until then — it exists and is
- in good standing — but nothing is charged until the anchor, where the first paid
- period starts.
+- **`trialEnd`.** The subscription is `trialing` until then, and nothing is
+ charged. The first paid period starts at the trial end.
+- **`billingAnchor`.** The subscription is `active` until then. It exists and is
+ in good standing, but nothing is charged until the anchor. The first paid
+ period starts at the anchor.
-In both cases the period before the first charge runs from creation to that
+In both cases, the period before the first charge runs from creation to that
time, so `currentPeriodEnd` is the trial end or the anchor.
## Statuses
@@ -92,23 +102,24 @@ time, so `currentPeriodEnd` is the trial end or the anchor.
| `past_due` | Charging the current period failed. The implementation is retrying under its own policy. |
| `canceled` | Ended. Terminal. `endedAt` and `endReason` (`requested` or `payment_failed`) say when and why. |
-When a renewal fails, the period still advances: the new period is the current
-one, and `past_due` says it is unpaid. When a retry succeeds the subscription is
-`active` again in the same period; the cycle does not move.
+When a renewal fails, the period still advances. The new period is the current
+one, and `past_due` says that it is unpaid. When a retry succeeds, the
+subscription is `active` again in the same period. The cycle does not move.
### `past_due` is portable to report, not to predict
-What happens after a failure is the implementation's: a native provider follows
-the retry schedule configured in its dashboard, and the generic fallback follows
-its documented policy (three retries, one, three and seven days apart, then the
-subscription ends with `endReason: "payment_failed"`). Two subscriptions in the
-same state may therefore behave differently on different providers. Decide access
-from the status; do not schedule anything on the assumption of a retry date.
+The implementation decides what happens after a failure. A native provider
+follows the retry schedule configured in its dashboard. The generic fallback
+follows its documented policy. By default, it retries three times, one, three,
+and seven days apart. When every retry has failed, the subscription ends with
+`endReason: "payment_failed"`. Two subscriptions in the same state can therefore
+behave differently on different providers. Decide access from the status. Do not
+schedule anything on the assumption of a retry date.
## Recovering a `past_due` subscription
-If the saved method will not work again — an expired card — take a new mandate
-on-session and switch to it:
+If the saved method will not work again, as with an expired card, take a new
+mandate on-session and switch to it:
```ts
import {
@@ -129,7 +140,7 @@ export async function switchMethod(
}
```
-Nothing is charged by the switch. The next scheduled attempt uses the new method.
+The switch charges nothing. The next scheduled attempt uses the new method.
## Cancelling
@@ -142,30 +153,32 @@ export async function cancel(id: string) {
}
```
-Use distinct keys for distinct operations. Reuse a key only when retrying the
-same request; reusing the period-end key for immediate cancellation replays the
-first outcome instead of applying the change.
+Use distinct keys for distinct operations. Reuse a key only to retry the same
+request. If you reuse the period-end key for immediate cancellation, the call
+replays the first outcome and does not apply the change.
- `period_end` sets `cancelAtPeriodEnd`. The status does not change until the
- period ends, at which point the subscription ends instead of renewing. Asking
- again is not an error.
-- `now` ends it at once. Nothing is refunded; refunds are `RefundPayment` on the
- payment interface. A subscription set to end at period end can still be ended
- now.
-- Either on a subscription that has already ended raises
+ period ends. Then the subscription ends instead of renewing. Asking again is
+ not an error.
+- `now` ends the subscription at once. Nothing is refunded. A refund is a
+ `RefundPayment` call on the payment interface. A subscription set to end at
+ period end can still be ended now.
+- Either timing on a subscription that has already ended raises
`InvalidSubscriptionStateError`, unless the idempotency key is a replay.
## Errors
-| Error | When |
-| --------------------------------- | ---------------------------------------------------------------------------- |
-| `SubscriptionNotFoundError` | No subscription has that identifier under that `provider` |
-| `InvalidSubscriptionRequestError` | A request outside the contract: a non-positive amount, a past trial end, ... |
-| `InvalidSubscriptionStateError` | A change to a subscription that has ended |
-| `SubscriptionPaymentError` | The first period was charged and declined; nothing was created |
-| `SubscriptionError` | The base class; also the call could not be made at all |
-
-Each carries `providerCode` where the provider gave one.
-`InvalidSubscriptionStateError.status` is an optional `SubscriptionStatus`
-describing the actual state that blocked the operation. Its constructor is
+| Error | When |
+| --------------------------------- | --------------------------------------------------------------------------------- |
+| `SubscriptionNotFoundError` | No subscription has that identifier under that `provider` |
+| `InvalidSubscriptionRequestError` | A request outside the contract, such as a non-positive amount or a past trial end |
+| `InvalidSubscriptionStateError` | A change to a subscription that has ended |
+| `SubscriptionPaymentError` | The first period was charged and declined. Nothing was created. |
+| `SubscriptionError` | The base class. Also raised when the call could not be made at all. |
+
+Each error carries `providerCode` when the provider gave one.
+`InvalidSubscriptionStateError.status` is an optional `SubscriptionStatus` that
+describes the actual state that blocked the operation. Its constructor is
`new InvalidSubscriptionStateError(message, status?, providerCode?)`.
+`SubscriptionNotFoundError` is also raised for an identifier that cannot be
+parsed at all.
diff --git a/docs/3.events.md b/docs/3.events.md
index 95d4a6c..9cd7840 100644
--- a/docs/3.events.md
+++ b/docs/3.events.md
@@ -1,7 +1,11 @@
-# Events
+---
+title: Events
+description: The four subscription events, how they are delivered, and how they relate to payment webhooks.
+mode: reference
+---
A subscription changes while nobody is calling, so a backend learns of renewals,
-failures and endings from events.
+failures, and endings from events.
## The event set
@@ -9,17 +13,18 @@ failures and endings from events.
| -------------------------------- | ---------------------------------------------------------------------------- |
| `subscription.created` | The subscription exists |
| `subscription.payment_succeeded` | A period was paid for, including the first |
-| `subscription.payment_failed` | Charging a period failed; the status is `past_due`, or the retries are spent |
-| `subscription.canceled` | It ended, for any reason |
+| `subscription.payment_failed` | Charging a period failed. The status is `past_due`, or the retries are spent |
+| `subscription.canceled` | The subscription ended, for any reason |
-An event carries the subscription as it was when the event happened, and, for
-the two payment events, the payment-interface identifier of the charge in
+An event carries the subscription as it was when the event happened. The two
+payment events also carry the payment-interface identifier of the charge in
`payment`. Read the charge with `GetPayment` for the amount or the decline
reason. A `subscription.payment_failed` has no `payment` when the provider
-refused to make the charge at all — the saved method had been removed, say.
+refused to make the charge at all, for example because the saved method had
+been removed.
-Changing the payment method and scheduling a cancellation at period end raise no
-event: the caller made those changes and already knows.
+A change of payment method and a cancellation scheduled for period end raise no
+event. The caller made those changes and already knows about them.
## Receiving them
@@ -38,40 +43,42 @@ SubscriptionEvents.register("billing", async (event) => {
});
```
-`SubscriptionEvents` is a registering proxy, not a webhook route. Handlers are
-registered under an identifier unique within your module, and unregistered
-automatically when your module unloads.
+`SubscriptionEvents` is a registering proxy, not a webhook route. You register
+each handler under an identifier unique within your module. AntelopeJS
+unregisters the handler when your module unloads.
-Register handlers when your module starts. An event is delivered to the
-handlers registered at the time; one raised while none are registered reaches
-nobody and is not kept.
+Register handlers when your module starts. The implementation delivers an event
+to the handlers registered at that time. An event raised while no handler is
+registered reaches nobody, and the implementation does not keep it.
Delivery is **at least once**:
-- A handler that throws or rejects has not handled the event, and it is delivered
- again later — to every handler, so de-duplicate on `event.id`.
-- A crash between a change and its delivery means the event is delivered when the
- implementation next looks at that subscription.
-- In a cluster, each event is delivered by one instance, to the handlers
- registered in that instance.
+- A handler that throws or rejects has not handled the event. The implementation
+ delivers the event again later, to every handler, so deduplicate on
+ `event.id`.
+- After a crash between a change and its delivery, the implementation delivers
+ the event the next time it looks at that subscription.
+- In a cluster, one instance delivers each event, to the handlers registered in
+ that instance.
-Events may arrive late, and should not be read as more recent than state you
-already hold. `GetSubscription` is always current.
+Events can arrive late. Do not treat an event as more recent than state you
+already hold. `GetSubscription` always returns the current state.
## How implementations learn of them
-That is each implementation's business, and invisible to a consumer:
+Each implementation decides how it learns of events, and a consumer cannot see
+the difference:
-- the generic fallback raises them from its own renewal clock, recording each in
- its database with the change that caused it;
-- a native provider raises them from the provider's webhooks.
+- The generic fallback raises them from its own renewal clock. It records each
+ event in its database with the change that caused it.
+- A native provider raises them from the provider's webhooks.
## Relation to payment events
Every subscription charge is an ordinary payment-interface payment, so its
-`payment.*` events reach your payment webhook route as well. They carry
-`SUBSCRIPTION_METADATA_KEY` (`antelopeSubscription`) in their metadata, set to
-the subscription's identifier:
+`payment.*` events reach your payment webhook route as well. Their metadata
+carries `SUBSCRIPTION_METADATA_KEY`, which is `antelopeSubscription`, set to the
+identifier of the subscription:
```ts
import {
@@ -83,11 +90,11 @@ import { SUBSCRIPTION_METADATA_KEY } from "@antelopejs/interface-subscriptions";
export async function handlePaymentWebhook(request: WebhookRequest) {
const event = await VerifyWebhook(request);
if (event?.payment?.metadata?.[SUBSCRIPTION_METADATA_KEY]) {
- return; // a subscription charge: handled by the subscription event
+ return; // The subscription event handles this charge.
}
}
```
-Handle a subscription charge in one place. The subscription event is the one
-that says what the charge meant — which period it paid for, whether the
-subscription is now `past_due`.
+Handle a subscription charge in one place. The subscription event says what the
+charge meant, such as which period it paid for and whether the subscription is
+now `past_due`.
diff --git a/docs/4.implementing.md b/docs/4.implementing.md
index bfb45a7..b64e4f2 100644
--- a/docs/4.implementing.md
+++ b/docs/4.implementing.md
@@ -1,70 +1,85 @@
-# Implementing the interface
+---
+title: Implementing the interface
+description: Implement the subscriptions interface natively in a payment module, or wire the generic fallback, and run the conformance suite.
+mode: how-to
+---
-There are two kinds of implementation, and both are expected from the start.
+An implementation is one of two kinds, and the interface expects both from the
+start.
## Native: the provider fires renewals
-A payment module whose provider has first-class subscriptions (Stripe, PayPal,
-Mollie) implements this interface beside `@antelopejs/interface-payment`, in the
-same module, mapping each call onto the provider's subscription object. It owns
-no clock and no database.
+A payment module whose provider has first-class subscriptions, such as Stripe,
+PayPal, or Mollie, implements this interface beside
+`@antelopejs/interface-payment`, in the same module. It maps each call onto the
+subscription object of the provider. It owns no clock and no database.
-What it must do that the provider will not do for it:
+The module must do these things, which the provider does not do for it:
- **Validate with `ValidateSubscriptionRequest`** before anything reaches the
- provider, so every implementation enforces the portable request shape. Defer
- relative date validation to a provider that checks its idempotency cache
- first; a replay must not fail because its original date has passed. An
- unknown customer or payment method is an `InvalidSubscriptionRequestError`
- too, never a `SubscriptionNotFoundError`, which is reserved for the
- subscription itself.
-- **Report only its own subscriptions.** A provider account may hold
- subscriptions made elsewhere — a hosted checkout, the dashboard. They were not
- created through this interface, have no `reference`, and must not be listed,
- read or raised as events.
+ provider, so every implementation enforces the portable request shape. If the
+ provider checks its idempotency cache first, leave relative date validation
+ to the provider. A replay must not fail because its original date has passed.
+ An unknown customer or payment method is also an
+ `InvalidSubscriptionRequestError`. It is never a `SubscriptionNotFoundError`,
+ which is reserved for the subscription itself.
+- **Report only its own subscriptions.** A provider account can hold
+ subscriptions made elsewhere, such as in a hosted checkout or in the
+ dashboard. They were not created through this interface and have no
+ `reference`. Do not list them, read them, or raise events for them.
- **Charge the first period at creation, or refuse.** A declined first charge
- rejects with `SubscriptionPaymentError` and leaves nothing behind. On Stripe
+ rejects with `SubscriptionPaymentError` and leaves nothing behind. On Stripe,
this is `payment_behavior: "error_if_incomplete"`.
- **Count periods from the anchor**, in UTC calendar units. Providers that bill
from an anchor already do.
-- **Map the provider's statuses onto four.** Anything that means "a charge
- failed and the provider is retrying" is `past_due`; anything that has ended or
- will never charge again is `canceled`. Where the provider can hold a
- subscription that will never charge again without ending it — Stripe's
- `unpaid` — say so, and tell the merchant to configure it to cancel instead.
+- **Map the statuses of the provider onto four.** A status that means "a charge
+ failed and the provider is retrying" is `past_due`. A status that has ended or
+ will never charge again is `canceled`. Some providers can hold a subscription
+ that will never charge again without ending it, such as Stripe's `unpaid`.
+ Document that case, and tell the merchant to configure the provider to cancel
+ instead.
- **Raise the four events** from the provider's webhooks. A module that already
- normalises webhooks for the payment interface can raise subscription events from
- the same deliveries.
+ normalises webhooks for the payment interface can raise subscription events
+ from the same deliveries.
- **Mark charges** with `SUBSCRIPTION_METADATA_KEY` when it normalises the
underlying payments, so a consumer can tell them from one-off payments.
-- **Echo, never accept, `vendorData`.** It is a `JsonObject`: put in it
- strings, numbers, booleans, `null`, arrays and plain objects, not a `Date`,
- a `Buffer` or a function. A value typed as an `interface` (a provider SDK
- object, for instance) and a `readonly` array are rejected by the compiler:
- build the object from a `type` alias or a literal, and spread a read-only
- array into a mutable one. `NaN` and `Infinity` compile but serialize to
- `null`.
+- **Echo, never accept, `vendorData`.** It is a `JsonObject`. Put strings,
+ numbers, booleans, `null`, arrays, and plain objects in it, not a `Date`, a
+ `Buffer`, or a function. The compiler rejects a value typed as an `interface`,
+ such as a provider SDK object, and a `readonly` array. Build the object from a
+ `type` alias or a literal, and spread a read-only array into a mutable one.
+ `NaN` and `Infinity` compile, but `JSON.stringify` writes them as `null`.
## Fallback: the merchant fires renewals
For every provider that leaves each charge to the merchant, wire
`@antelopejs/payment-subscriptions`. It implements this interface on top of any
-payment-interface provider, keeps every subscription in
-`@antelopejs/interface-database`, and fires renewals from its own clock — the
-Redis scheduler when one is wired, polling the database when not.
+payment-interface provider and keeps every subscription in
+`@antelopejs/interface-database`. It fires renewals from its own clock. That
+clock is the Redis scheduler when one is wired. Otherwise it polls the
+database.
-A payment module should not implement this interface by embedding a clock of its
-own: that would put a mandatory database and scheduler into a payment module.
+Do not implement this interface in a payment module by embedding a clock in it.
+That would make a database and a scheduler mandatory for the payment module.
## Wiring an implementation
-The calls are declared directly as the public names, so the whole module is the
-declaration:
+Declare the calls under their public names, so the whole module is the
+declaration. The `declare` lines below stand for your own implementations:
-```ts fragment
+```ts
import * as subscriptions from "@antelopejs/interface-subscriptions";
+import type { SubscriptionEventHandler } from "@antelopejs/interface-subscriptions";
import { ImplementInterface } from "@antelopejs/interface-core";
+declare const CreateSubscription: typeof subscriptions.CreateSubscription;
+declare const GetSubscription: typeof subscriptions.GetSubscription;
+declare const ListSubscriptions: typeof subscriptions.ListSubscriptions;
+declare const CancelSubscription: typeof subscriptions.CancelSubscription;
+declare const UpdateSubscriptionPaymentMethod: typeof subscriptions.UpdateSubscriptionPaymentMethod;
+declare function register(id: string, handler: SubscriptionEventHandler): void;
+declare function unregister(id: string): void;
+
export function construct(): void {
ImplementInterface(subscriptions, {
CreateSubscription,
@@ -77,19 +92,19 @@ export function construct(): void {
}
```
-`SubscriptionEvents` is a registering proxy: its implementation is a `register`
+`SubscriptionEvents` is a registering proxy. Its implementation is a `register`
and `unregister` pair that keep the handlers your module delivers to.
-List the interface in `antelopeJs.implements`, and the payment interface in
+List the interface in `antelopeJs.implements`, and list the payment interface in
`dependencies`.
## `ValidateSubscriptionRequest`
`ValidateSubscriptionRequest(request: SubscriptionRequest, now?: number): void`
checks the portable request rules synchronously. Supply `now` to also check
-that trial and billing dates are in the future. It is Unix epoch
-**milliseconds**, such as `Date.now()`, not seconds. Pass your injected clock
-in tests or in an implementation that controls time.
+that trial and billing dates are in the future. `now` is in Unix epoch
+**milliseconds**, such as `Date.now()`, not seconds. Pass your injected clock in
+tests or in an implementation that controls time.
```ts docs-check
import {
@@ -102,56 +117,59 @@ export function validateBeforeCreation(request: SubscriptionRequest): void {
}
```
-The function returns `undefined` on success and throws
-`InvalidSubscriptionRequestError` with a message naming the first failed rule:
+The function returns `undefined` on success. Otherwise it throws
+`InvalidSubscriptionRequestError` with a message that names the first failed
+rule:
-- `reference`, `customer` and `paymentMethod` must be non-empty.
-- `amount.value` must be a positive integer of minor units; `amount.currency`
+- `reference`, `customer`, and `paymentMethod` must be non-empty.
+- `amount.value` must be a positive integer of minor units. `amount.currency`
must contain exactly three uppercase letters. The helper checks the format,
not membership in an ISO currency registry.
-- `interval` must be `day`, `week`, `month` or `year`; an explicit
+- `interval` must be `day`, `week`, `month`, or `year`. An explicit
`intervalCount` must be a positive integer.
-- `trialEnd` and `billingAnchor` are mutually exclusive. Each, if supplied,
- must be finite. When `now` is supplied, each must also be strictly greater
- than `now` (equality is invalid).
-
-An implementation may omit `now` only when its provider validates future dates
-after resolving idempotent replays. Stripe uses this form: a retry with the
-original key and parameters can return the saved subscription after its trial
-or billing anchor has passed. A new request with a past date still fails at
-the provider. Implementations that own their idempotency storage must resolve
-replays before applying relative date validation themselves.
-
-This helper does not contact a provider, verify customer or saved-method
-existence, or enforce provider-specific maximum periods. Implementations must
-perform those checks separately and also report invalid requests with
-`InvalidSubscriptionRequestError`.
+- `trialEnd` and `billingAnchor` are mutually exclusive. Each one, if supplied,
+ must be finite. When `now` is supplied, each one must also be strictly
+ greater than `now`. Equality is invalid.
+
+Omit `now` only when your provider validates future dates after it resolves
+idempotent replays. Stripe uses this form. A retry with the original key and
+parameters can return the saved subscription after its trial or billing anchor
+has passed. A new request with a past date still fails at the provider. If your
+implementation owns its idempotency storage, resolve replays before you apply
+relative date validation yourself.
+
+The helper does not contact a provider. It does not check that the customer or
+the saved method exists, and it does not enforce the maximum periods of a
+provider. Make those checks separately, and report invalid requests with
+`InvalidSubscriptionRequestError` too.
## The conformance suite
-`dist/tests/*.test.js` ships in the package. `ajs module test` runs it
-automatically against any module that lists this interface in
-`antelopeJs.implements`.
-
-It is weaker than the payment interface's suite, and the reason matters before
-anyone reads a green run as a promise: **a test cannot wait a month.** Stripe has
-test clocks, but Mollie and PayPal do not, so a portable suite cannot pin a
-renewal, a trial ending, a failed retry or a period end. What it covers:
-
-- creating a subscription that charges at once, a trial, a billing anchor;
-- the first charge's shape and its metadata marker;
-- idempotent replay of creation and cancellation;
-- reading back, listing newest first and per customer;
-- cancelling now and at period end, and refusing to change one that has ended;
-- switching the payment method;
-- unknown identifiers and every invalid request;
-- a declined first period, when the provider can supply a method that saves and
- then declines;
-- events, when they can reach the test process.
-
-Everything time-driven is pinned instead by the generic fallback's own suite,
-which drives an injected clock. That suite is the executable specification a
-native implementation's renewal behaviour is checked against by eye.
+`dist/tests/*.test.js` ships in the package. `ajs module test` runs the suite
+against any module that lists this interface in `antelopeJs.implements`.
+
+The suite checks less than the suite of the payment interface. Know why before
+you read a green run as a promise: **a test cannot wait a month.** Stripe has
+test clocks, but Mollie and PayPal do not. A portable suite therefore cannot pin
+a renewal, a trial end, a failed retry, or a period end. The suite covers these
+cases:
+
+- Creating a subscription that charges at once, one with a trial, and one with a
+ billing anchor.
+- The shape of the first charge and its metadata marker.
+- Idempotent replay of creation and cancellation.
+- Reading back, and listing newest first and per customer.
+- Cancelling now and at period end, and refusing to change a subscription that
+ has ended.
+- Switching the payment method.
+- Unknown identifiers and every invalid request.
+- A declined first period, when the provider can supply a method that saves and
+ then declines.
+- Events, when they can reach the test process.
+
+The generic fallback has its own suite, which drives an injected clock and pins
+everything that depends on time. That suite is the executable specification. Check the
+renewal behaviour of a native implementation against it by eye.
| Variable | Purpose |
| --------------------------------------------------- | --------------------------------------------------------------------------- |
@@ -160,5 +178,5 @@ native implementation's renewal behaviour is checked against by eye.
| `ANTELOPE_SUBSCRIPTIONS_TEST_EVENTS` | Set when the implementation's events reach the test process during the run. |
| `ANTELOPE_PAYMENT_TEST_CURRENCY` | ISO-4217 code to run against. Defaults to `EUR`. |
-With nothing implementing the interface, the calls queue and the suite times
-out; it cannot report green by accident.
+When nothing implements the interface, the calls queue and the suite times
+out. It cannot report green by accident.
diff --git a/docs/5.provider-capability-matrix.md b/docs/5.provider-capability-matrix.md
index f3f4329..871547e 100644
--- a/docs/5.provider-capability-matrix.md
+++ b/docs/5.provider-capability-matrix.md
@@ -1,9 +1,13 @@
-# Provider capability matrix
+---
+title: Provider capability matrix
+description: Which payment providers fire renewals themselves, which implementation each one needs, and where native and fallback behaviour differ.
+mode: reference
+---
-Which providers fire renewals themselves decides which implementation a
-deployment needs. Checked against each provider's documentation on 2026-09-18;
-Stripe and PayPal were not re-checked then, both having first-class subscription
-objects that bill on their own schedule.
+Whether a provider fires renewals itself decides which implementation a
+deployment needs. The matrix was checked against the documentation of each
+provider on 2026-09-18. Stripe and PayPal were not checked again then, because
+both have first-class subscription objects that bill on their own schedule.
| Provider | Who fires renewals | Implementation |
| ------------ | ------------------ | ------------------------------------------------------------- |
@@ -18,35 +22,36 @@ objects that bill on their own schedule.
for you at the interval you specified."_
- **Adyen**: subscriptions are _"recurring transactions you initiate at regular
intervals"_, against a stored token and `shopperReference`.
-- **Checkout.com**: no subscription or billing-plan resource at all. Recurring
- is a stored instrument plus `payment_type: Recurring` and
+- **Checkout.com**: no subscription or billing-plan resource at all. A
+ recurring charge is a stored instrument plus `payment_type: Recurring` and
`merchant_initiated: true` on an ordinary payment.
-- **Mangopay**: the Recurring PayIn Registration stores frequency, dates and
- amounts, and handles the authentication chain between charges, but _"the
- platform can initiate payments"_ — Mangopay does not fire them. The fallback
- owns the clock and can record the schedule with Mangopay through its provider
- schedule seam.
-
-Implemented: the generic fallback, and Stripe natively in `@antelopejs/stripe`,
-which passes the conformance suite against live Stripe and is further tested on
-Stripe test clocks — renewal, trial end, `past_due` and cancellation at period
-end — which the portable suite cannot pin. PayPal and Mollie are plans.
-
-Times are kept at each provider's precision, which the contract only promises
-to the second: Stripe stores seconds. Mollie's subscription `startDate` is a
-calendar date, so on Mollie a trial end or billing anchor lands on a day rather
-than an instant — a native Mollie implementation has to say so, or charge the
-first period itself. Unverified against Mollie.
+- **Mangopay**: the Recurring PayIn Registration stores frequency, dates, and
+ amounts, and handles the authentication chain between charges. But _"the
+ platform can initiate payments"_, so Mangopay does not fire them. The fallback
+ owns the clock. It can record the schedule with Mangopay through its provider
+ schedule interface (`ProviderSchedule`).
+
+Two implementations exist: the generic fallback, and Stripe natively in
+`@antelopejs/stripe`. `@antelopejs/stripe` passes the conformance suite against
+Stripe in test mode. Its tests on Stripe test clocks also cover what the portable
+suite cannot pin: renewal, trial end, `past_due`, and cancellation at period end.
+PayPal and Mollie are plans.
+
+Implementations keep times at the precision of each provider, and the contract
+promises only the second. Stripe stores seconds. The subscription `startDate` of
+Mollie is a calendar date, so on Mollie a trial end or a billing anchor lands on
+a day, not an instant. A native Mollie implementation has to document that, or
+charge the first period itself. This is not verified against Mollie.
## Where native and fallback differ, and the contract allows it
-| Behaviour | Native | Fallback |
-| ----------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
-| What happens after `past_due` | The provider's configured retry schedule | Three retries, one, three and seven days apart, then ends |
-| When a renewal is charged | The provider's scheduler, near the time | At the time with Redis wired; within one poll otherwise |
-| `vendorData` | What the provider said | Absent |
-| Events | From the provider's webhooks | From its own clock, recorded with each change |
-| An event's subscription | Stripe invoice events: read back at delivery, so a redelivery may show a later state | As it was when the event happened |
+| Behaviour | Native | Fallback |
+| ----------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| What happens after `past_due` | The provider's configured retry schedule | By default three retries, one, three, and seven days apart, then the subscription ends |
+| When a renewal is charged | The provider's scheduler, near the time | At the time with Redis wired; within one poll otherwise |
+| `vendorData` | What the provider said | The fallback's own renewal state: `reconciliationRequired`, `attemptStartedAt`, `attemptReplayUntil`, `phase` |
+| Events | From the provider's webhooks | From its own clock, recorded with each change |
+| An event's subscription | Stripe invoice events read it back at delivery, so a redelivery can show a later state | As it was when the event happened |
## Out of scope, and why
@@ -54,8 +59,8 @@ first period itself. Unverified against Mollie.
| ------------------------------ | ------------------------------------------------------------------------------------ |
| Proration | Arithmetic every provider does differently, and the fallback would have to invent it |
| Dunning schedules | Native providers configure retries in their dashboard; only the status is portable |
-| Tax | A tax engine is a product of its own |
+| Tax | A tax engine is a separate product |
| Coupons and discounts | Provider objects with provider semantics |
| Metered and usage billing | Needs usage reporting the fallback has no source for |
| Several items per subscription | Needs proration to change any one of them |
-| Write-through `vendorData` | Compiles everywhere, works on one provider: invisible coupling |
+| Write-through `vendorData` | It compiles everywhere but works on one provider, which hides the coupling |
diff --git a/package.json b/package.json
index 19f16e9..5f2255d 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,9 @@
"test": "src/antelope.test.ts",
"skills": [
"./skills"
- ]
+ ],
+ "docs": {
+ "standard": true
+ }
}
}