StrimzStrimz
Subscriptions

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:

FailureNext retry after
1st15 minutes
2nd1 hour
3rd3 hours
4th6 hours
5th12 hours
6th and later24 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.outcome says why it failed and charge.attemptNumber which attempt it was.
  • subscription.charged: a retry succeeded and the subscription is active again.
  • 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.

On this page