Recovery
When a charge fails, Strimz retries with backoff for up to 48 hours past the period end before the subscription lapses.
When a charge fails, the subscription moves to at_risk and the scheduler retries it on a backoff schedule. If a retry succeeds, the subscription goes back to active. If the 48-hour grace window runs out first, it lapses.
What triggers recovery
The contract skips a charge with one of these outcomes:
insufficient_funds: the payer's balance is below the charge amount. The most common cause.revoked_approval: the payer's allowance to the subscriptions contract is too low, often because they set it to zero on purpose.transfer_failed: the token transfer failed, for example because the token blocklisted the payer's address.merchant_inactive: the merchant is not active in the registry.
The contract emits SubscriptionChargeSkipped instead of SubscriptionCharged. The indexer writes a failed SubscriptionCharge row, fires subscription.charge_failed, and flips the subscription to at_risk. It only does that when the period has no successful charge yet (see the race-condition guard). A first charge that fails at the end of a trial is handled the same way: the subscription goes from trialing to at_risk.
The retry schedule
When the indexer records a failed charge, it sets the next retry time:
| Failure | Next retry after |
|---|---|
| 1st | 15 minutes |
| 2nd | 1 hour |
| 3rd | 3 hours |
| 4th | 6 hours |
| 5th | 12 hours |
| 6th and later | 24 hours |
The sweeper runs every 15 minutes, so a retry fires on the first run after its time. About seven attempts fit in the grace window.
A retry after a balance or allowance failure reuses the same attempt id, so it updates the same charge: charge.attemptNumber goes up by one and charge.outcome shows the latest reason.
The grace window
gracePeriodHours is 48 on every subscription. It is not configurable through the API, SDK or dashboard.
An hourly scheduler cron moves every at_risk subscription to lapsed once currentPeriodEndAt + 48 hours has passed, and fires subscription.lapsed. In practice the schedule above fits about six retries inside the window.
The recoveryGracePeriodHours setting on the Agents page does not change this window. It only controls when recovery emails are sent.
What fires on your webhook endpoint
subscription.charge_failed: a charge attempt failed. It is sent for every failed retry. The payload is{ subscription, charge };charge.outcomesays why it failed andcharge.attemptNumberwhich attempt it was.subscription.charged: a retry succeeded and the subscription isactiveagain.subscription.lapsed: the grace window ran out. Use this to revoke access.subscription.recovery_attempt: the recovery agent emailed the customer. The payload is{ subscription, attemptNumber }. It is only sent when the recovery agent is on and the email went out.
Emailing the payer
The first charge_failed (charge.attemptNumber 1) is the one to act on. Email the payer and ask them to top up their wallet. You can send it yourself from your webhook handler, or turn on the recovery agent on the Agents page, which emails the customer if Strimz has their email on file.
What it costs to retry
Each retry is an on-chain call. Strimz's scheduler pays the gas. You are not billed for failed retries.
There is no call to retry a charge on demand. The next retry follows the schedule above.
After a lapse
Lapsed is terminal. Even if the payer tops up, Strimz will not charge the old subscription again.
To come back, the payer subscribes again on the plan's checkout link, which creates a new subscription with a new id. A payer can hold only one live subscription per plan; a lapsed one does not count, so they can re-subscribe straight away. The old row stays for audit.

