Note: These notifications apply to the new recurring monthly plans only. If your organisation is still on the previous engine, see "If you are still on the previous engine" at the end of this article.
When a monthly plan renews, we send a notification to your callback URL at each stage of the cycle. There are six in total, covering everything from a renewal being scheduled to a renewal ending.
All six use the same payload shape, so you only need to write one parser and switch on the alertType field.
To receive these, you need a callback URL configured against your organisation. See Callback URL.
The six notifications
| Notification | What it means | What you need to do |
| BundleRenewalScheduled | The next renewal has been dated. Tells you when we will debit your balance, and how much | Nothing. Use it to forecast committed spend |
| BundleRenewalUpcoming | A renewal is due. Sent at 48 hours, 24 hours and 1 hour beforehand | Check action.required. If it says top_up, fund your balance |
| BundleRenewalAwaitingTopUp | The charge was attempted and your balance did not cover it | Top up before the deadline in action.byTime |
| BundleRenewalSucceeded | The renewal completed and your balance was debited | Nothing. Reconcile against your ledger |
| BundleRenewalFailed | The renewal could not be delivered. The charge has been refunded | Nothing financially. Raise it with us |
| BundleRenewalLapsed | The renewal window closed unfunded. Auto-renewal has ended for this eSIM | Mark the arrangement inactive. Re-enabling needs a new arrangement |
What a normal month looks like
Each month, you’ll get five notifications:
BundleRenewalUpcoming 48 hours before
BundleRenewalUpcoming 24 hours before
BundleRenewalUpcoming 1 hour before
BundleRenewalSucceeded charged
BundleRenewalScheduled next renewal dated
The succeeded and scheduled notifications arrive within minutes of each other. The three reminders are spread across the two days before.
BundleRenewalSucceeded does not carry the new expiry date. The operator confirms the new bundle's window shortly afterwards, which is what BundleRenewalScheduled reports. If you display an expiry date to your customers, hold the previous value until the scheduled notification arrives rather than blanking it.
The renewal timeline
The charge is taken shortly before the current bundle expires, not at the moment it expires so if there is a funding problem, it appears while your customer still has service.
For a bundle expiring at 09:20:
| Time | What happens |
| 09:00 | The charge is attempted, 20 minutes before expiry |
| 09:19:50 | The new bundle is assigned |
| 09:20 | The old bundle expires |
| 15:20 | If the charge could not be taken, the top-up window closes |
The reminder leads are measured from the charge time, not the expiry. For the renewal above, the 1 hour reminder is sent at 08:00.
When your balance is short
If your balance will not cover the charge when a reminder is built, that reminder carries action.required: "top_up", along with the shortfall and the deadline. If you top up before the charge, the renewal simply goes through.
If the charge is attempted and your balance still doesn’t cover it, you receive BundleRenewalAwaitingTopUp. You then have until action.byTime - the bundle expiry plus six hours - to fund the account. We keep re-checking your balance quietly during that window, so you are told once rather than on every retry.
If the deadline passes unfunded, you receive BundleRenewalLapsed and auto-renewal ends for that eSIM. Topping up afterwards does not restart it. You will need to create a fresh arrangement through the management API.
You will always get either a BundleRenewalSucceeded or a BundleRenewalLapsed to close out an AwaitingTopUp.
We recommend you build these notifications into your own alerting and ensure you set a minimum amount on your eSIM Go account.
Expect these in bulk
Your balance is held at organisation level, not per eSIM. So a single funding gap will affect all renewals due while the balance is short, and each one sends its own notification - potentially hundreds within minutes.
Aggregate before you alert. A burst of BundleRenewalAwaitingTopUp might be frustrating but it’s better than lots of customers losing service.
When a renewal fails on our side
BundleRenewalFailed means the renewal could not be delivered. The charge has already been refunded before you receive the notification, so nobody is out of pocket. Service stops when the current bundle expires.
There are two outcome codes, both with charge.status: "refunded":
- delivery_failed - the bundle could not be assigned
- charge_orphaned - the charge was taken but stranded before delivery, and has since been refunded
The distinction is diagnostic. Neither requires you to move any money. Suppress any rebilling of your own for that cycle and raise it with us - if you see a run of these across multiple eSIMs, please report it to us straight away.
Reading the payload
json
{
"alertType": "BundleRenewalAwaitingTopUp",
"iccid": "8944538532008160000",
"renewal": {
"id": "9c1f4e2a-2f4d-4f0e-9a3b-6d0c5c9a1e77",
"number": 4,
"total": 6,
"remaining": 3,
"occurredAt": "2026-08-09T09:00:11Z",
"dueAt": "2026-08-09T09:00:00Z"
},
"charge": {
"amount": "9.99",
"minorUnits": 999,
"currency": "GBP",
"status": "not_taken"
},
"bundle": {
"name": "domp_1GB_ULSMS_ULMIN_1M_V1"
},
"outcome": {
"code": "insufficient_balance",
"message": "Organisation balance insufficient to renew this bundle."
},
"action": {
"required": "top_up",
"byTime": "2026-08-09T15:20:00Z",
"shortfallAtCheck": { "amount": "5.01", "minorUnits": 501, "currency": "GBP" }
}
}
| Field | Description |
| alertType | Which of the six notifications this is |
| iccid | The eSIM being renewed |
| renewal.id | Unique ID for this renewal. Use it to deduplicate |
| renewal.number | Position of this renewal in the sequence |
| renewal.total | The renewal cap, or null if the plan renews indefinitely |
| renewal.remaining | Renewals still to come, or null if uncapped |
| renewal.occurredAt | When this event happened (UTC) |
| renewal.dueAt | The renewal's scheduled charge time (UTC) |
| charge.amount | The amount debited from your balance, as a decimal string |
| charge.minorUnits | The same amount in minor units |
| charge.currency | Your organisation's currency |
| charge.status | charged, refunded, not_taken or pending |
| reminder.leadHours | On BundleRenewalUpcoming only. 48, 24 or 1 |
| bundle.name | The bundle being renewed into |
| outcome.code | Machine-readable outcome. Branch on this |
| outcome.message | Human-readable text for your logs. Do not branch on it |
| action.required | none or top_up |
| action.byTime | The top-up deadline (UTC). Only when action.required is top_up |
| action.shortfallAtCheck | How short the balance was. Advisory |
Expected values by notification
| alertType | outcome.code | charge.status | action.required |
| BundleRenewalScheduled | scheduled | pending | none |
| BundleRenewalUpcoming | upcoming | pending | none or top_up |
| BundleRenewalAwaitingTopUp | insufficient_balance | not_taken | top_up |
| BundleRenewalSucceeded | renewed | charged | none |
| BundleRenewalFailed | delivery_failed or charge_orphaned | refunded | none |
| BundleRenewalLapsed | payment_missed | not_taken | none |
outcome.code stays upcoming on a reminder even when your balance is short. The demand is in action.required, not in the outcome.
Things that catch people out
charge.amount is what we debit you. It has no relationship to what you charge the person holding the eSIM. Nothing in these notifications is written for your end users - outcome.message is operator text for your logs and support tooling. What you tell your customer is yours to write.
Amounts are strings, not JSON numbers. This prevents float rounding across the boundary. Use minorUnits for integer arithmetic.
remaining is not total minus number. The cap counts renewals actually delivered, whereas the sequence number can advance without one being delivered. Where the two disagree, trust remaining.
occurredAt and dueAt are different clocks. dueAt is when the renewal was scheduled to be charged; occurredAt is when this particular event happened. On an AwaitingTopUp they usually differ by a few seconds.
Three reminders about the same shortfall are not duplicates. They carry the same renewal.id because they concern the same renewal cycle. Deduplicate on renewal.id together with alertType and, for reminders, reminder.leadHours.
If you are still on the previous engine
Organisations on the previous engine continue to receive AutoRenewScheduled and AutoRenewAttempt. Those notifications are unchanged and continue to work as they always have.
An organisation moved onto the new engine receives the BundleRenewal* notifications instead. You will never receive both families for the same eSIM.
| Previously | Now |
| AutoRenewScheduled | BundleRenewalScheduled |
| AutoRenewAttempt with success: true | BundleRenewalSucceeded |
| AutoRenewAttempt with success: false | BundleRenewalAwaitingTopUp, followed by either BundleRenewalSucceeded or BundleRenewalLapsed - or BundleRenewalFailed where the charge was taken and refunded |
The single failure case has been split into three because the three want opposite responses from you. A short balance needs funding, a refunded delivery failure needs nothing moved at all, and a lapse means the arrangement is over.
Because both engines run side by side during migration, the notification names are distinct rather than reused. You can route the whole new family on a single BundleRenewal prefix match.