Skip to content

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.

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.

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.

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.

FieldActivationRenewalDeactivation
type
telco
product_id
product_name
phone
fee
auto_renewal
date

fee is an integer in the currency’s minor unit.

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');
}
});

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_notification events 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.