Skip to content

Plans and subscriptions ​

Scopes: subscriptions:write to make and change, subscriptions:read to read. Live only — there is no sandbox counterpart, for the reason below.

What a subscription is here, and what it is not ​

There is no mandate in Tanzanian mobile money. Nothing in this system can debit a customer without that customer acting, and no provider behind our rails offers one. So a subscription is not a standing order that takes money. It is a promise to do three things every period:

  1. issue a bill with its own control number;
  2. text that number to the customer on file;
  3. optionally push a prompt to their handset at an hour you choose.

Every cycle ends with somebody deciding to pay. That is a weaker product than a card mandate and it is the honest one — and it means everything here stands on bills, which you already have: the payment arrives as an ordinary charge.succeeded against the bill, through the same M-Pesa menu or the same hosted checkout.

There is no sandbox. A test subscription would issue nothing, text nobody and ring nobody, because all three happen in a scheduler that reads the live database. A POST that returned 201 and then did none of it would teach an integration that it worked.

POST /v1/plans ​

Body

Field
namerequired2–40 characters. It goes into the SMS beside the control number, which is why it is short.
amount_minorrequiredWhole shillings, TZS 100 – 20,000,000.
intervalrequiredweek, month or year. There is no day: a daily bill is a daily SMS.
interval_countoptional1–12. Default 1. {"interval": "week", "interval_count": 2} is fortnightly.
metadataoptionalYour own object, under 4 kB.

A plan's terms never change. The price, the currency and the interval are frozen the moment the plan exists, and the database refuses an edit — every period is billed at what its subscriber agreed to. To move a price, retire the plan and make another. The name and the metadata may still be corrected, because neither decides what anybody owes.

DELETE /v1/plans/{id} ​

Retires it. Nothing is deleted: the plan closes to new subscribers and every existing subscription on it goes on billing, because the plan is what those people agreed to and it has to go on being readable. It comes back as active: false. A new subscription to a retired plan is PLAN_INACTIVE.

POST /v1/subscriptions ​

Body

Field
customerrequiredcus_… from your address book.
planrequiredpln_…, and it must still be active.
start_onoptionalYYYY-MM-DD, a day in Dar. Defaults to today. Every period is computed from it.
grace_daysoptional0–90, default 7. How long after a period starts before an unpaid bill is called late.
collect_houroptional0–23, in Dar. Pushes a prompt at that hour. Omit for none.

The first bill is issued straight away, then one every period. Not in arrears: a landlord's first month would otherwise go unbilled, and picking that silently is not a decision we should make for you.

start_on is the anchor, and it is kept. A tenancy starting on 31 January bills 31 Jan, 28 Feb, 31 Mar — the clamp to a short month never compounds, because every period is computed from the anchor rather than from the period before it. Most products of this shape quietly move that tenancy to the 28th for ever.

collect_hour needs a customer with a phone number, and is refused without one rather than accepted and silently never sent.

One live subscription per customer per plan. A second is ALREADY_SUBSCRIBED (409): two subscriptions to one plan is two bills every month from one person, for ever, and the mistake is invisible until somebody reads their own roster. Cancel and subscribe again to change the terms.

Response 201

json
{
  "id": "sub_7K2FJ8QW4XM1NPRB93HZ5TVC",
  "object": "subscription",
  "status": "active",
  "customer": "cus_7K2FJ8QW4XM1NPRB93HZ5TVC",
  "plan": "pln_7K2FJ8QW4XM1NPRB93HZ5TVC",
  "amount_minor": 45000000,
  "currency": "TZS",
  "interval": "month",
  "interval_count": 1,
  "start_on": "2026-10-01",
  "next_bill_on": "2026-10-01",
  "grace_days": 7,
  "collect_hour": 9,
  "cancelled_at": null,
  "metadata": {},
  "created": "2026-09-23T09:14:02.118Z"
}

start_on and next_bill_on are dates and not instants, deliberately: a period is a day in Dar, and an ISO timestamp would invite your client to render it in its own zone and show a tenant the wrong month.

POST /v1/subscriptions/{id}/cancel ​

Stops the next period. The bill already issued stays payable — somebody who cancels on the 20th still owes that month, and voiding it would be us deciding a commercial question. Cancel the bill too if you mean to write it off.

Cancelling twice answers with the cancelled subscription rather than an error: you asked for a state and the state is true.

The three states ​

activeBilling. The current period may be unpaid and still inside its grace.
past_dueThe grace ran out with the period unpaid. Set by us, cleared when the bill is settled.
cancelledTerminal. Nothing further is issued.

past_due is about a grace period running out, not a rail refusing — nothing debits anybody, so the only way a subscription payment fails is that nobody paid it. A gym reads it to suspend a membership; a landlord reads it to start a conversation.

The three events ​

subscription.cycle_createdA period's bill was issued. Carries the control number.
subscription.payment_succeededMoney landed against it. paid says whether that cleared the period — a part payment is ordinary here.
subscription.payment_failedThe grace ran out and the period is still owed.

There is deliberately no subscription.created: you made it and hold the reply. See webhooks.

Reconciling ​

Every period is a bill, so everything on Bills applies: GET /v1/bills?customer_ref=… finds a subscriber's slips, and the payment itself arrives as charge.succeeded carrying the bill object. A subscription adds the schedule; it does not add a second way for money to arrive.

Every code, scope and route on this site is rendered from the API's own source.