Subscription lifecycle
Premium content products bill on a recurring cycle. The operator runs that billing, and relays each event to us; we relay it to your webhook.
Three events, all POSTed to the URL you configure under Settings → Webhook.
Activation
Section titled “Activation”A subscriber successfully subscribed.
{ "type": "activation_notification", "telco": "mtn", "product_id": "103", "product_name": "games_daily", "phone": "2348020000000", "fee": 20000, "auto_renewal": false, "date": "2026-07-14 23:50:37"}Grant access to the product on this event — not when you receive the inbound keyword. The keyword is an intent to subscribe; the activation is confirmation that the operator actually billed them.
Renewal
Section titled “Renewal”The subscription billed for another cycle. This is your revenue event — it’s what most partners reconcile against.
{ "type": "renewal_notification", "telco": "mtn", "product_id": "103", "product_name": "games_daily", "phone": "2348020000000", "fee": 20000, "auto_renewal": true, "date": "2026-07-14 23:50:37"}Extend their access period and record the revenue.
Deactivation
Section titled “Deactivation”They cancelled, or the operator terminated the subscription.
{ "type": "deactivation_notification", "telco": "mtn", "product_id": "103", "product_name": "games_daily", "phone": "2348020000000", "auto_renewal": false, "date": "2026-07-14 23:50:37"}Note there is no fee on this event — nothing was billed. Revoke access.
Fields
Section titled “Fields”| Field | Activation | Renewal | Deactivation |
|---|---|---|---|
type | ✅ | ✅ | ✅ |
telco | ✅ | ✅ | ✅ |
product_id | ✅ | ✅ | ✅ |
product_name | ✅ | ✅ | ✅ |
phone | ✅ | ✅ | ✅ |
fee | ✅ | ✅ | ❌ |
auto_renewal | ✅ | ✅ | ✅ |
date | ✅ | ✅ | ✅ |
fee is an integer in the currency’s minor unit.
Handling them
Section titled “Handling them”One endpoint, branch on type:
app.post('/webhooks/9bits/vas', async (req, res) => { const evt = req.body; res.sendStatus(200); // acknowledge first, always
// De-duplicate — the same event can arrive more than once. const id = `${evt.type}:${evt.phone}:${evt.product_id}:${evt.date}`; const fresh = await cache.set(id, 1, { NX: true, EX: 7 * 86400 }); if (!fresh) return;
switch (evt.type) { case 'activation_notification': await grantAccess(evt.phone, evt.product_id); await recordRevenue(evt); break;
case 'renewal_notification': await extendAccess(evt.phone, evt.product_id); await recordRevenue(evt); // your revenue event break;
case 'deactivation_notification': await revokeAccess(evt.phone, evt.product_id); break;
default: logger.warn({ type: evt.type }, 'unknown VAS event'); }});Reconciliation
Section titled “Reconciliation”The operator’s billing is the source of truth. Your ledger is a copy, and copies drift.
- Reconcile daily against your operator statement. Don’t wait for month-end to discover a webhook was quietly failing for three weeks.
- Alert on silence. A day with zero
renewal_notificationevents for an active product is far more likely to be a broken webhook than genuine churn. Zero is a suspicious number — page someone. - Keep the raw payloads. Store every event verbatim, forever. When a revenue dispute comes, the raw JSON is what settles it.
Remember the retry schedule: after four attempts across an hour, an event is dropped permanently. If your endpoint is down for a morning, those renewals are gone and there is no replay API. Daily reconciliation is how you catch what you lost — see Webhooks.