Skip to content

Subscriptions and instalments

Wave and Orange Money can’t debit a customer automatically. So each billing period, Xaalis issues an invoice with a payment link; the customer approves it in their wallet like any payment. Xaalis tracks the state; you send the link (by SMS, WhatsApp or email) when you receive invoice.created.

The same feature gives instalment plans (paiement en plusieurs fois): a fixed number of payments, goods delivered after the last one. No credit is involved.

Terminal window
curl -s -X POST http://localhost:4000/v1/subscriptions \
-H "Authorization: Bearer $XAALIS_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: abo:customer_77" \
-d '{"customer":{"phone":"+221771234567","name":"Awa Ndiaye"},"amount":5000,"interval":"month","description":"Abonnement Premium"}'
Field Rules
customer { phone: "+2217XXXXXXXX", name, email? }
amount integer XOF, 100 – 100 000 000, per period
interval, interval_count day · week · month · year; count 1–12 (default 1). Monthly billing keeps the day of the month, clamped to short months (31 Jan → 28/29 Feb → 31 Mar), in Dakar time
billing_cycles null (until canceled) or 2–24 for an instalment plan
days_until_due 1–30, default 3 — after this the invoice is overdue
cancel_after_days 1–60, default 14 — grace after the due date before the subscription is canceled
description, client_reference, metadata as on payments

The response is the subscription with its first invoice (latest_invoice) already issued.

Each invoice has a stable hosted_invoice_url (…/v1/public/invoices/{id}?cs=…). It redirects the customer to a payment for that period. If an attempt fails or expires, opening the same link creates a new attempt — the link never changes, so you can send it once and remind with the same URL. It stays payable until grace_ends_at.

Treat it like a password for that customer: don’t log it or send it to analytics.

stateDiagram-v2
    [*] --> active: POST /v1/subscriptions (invoice 0 issued)
    active --> active: invoice paid → next period's invoice
    active --> past_due: due_at passed, unpaid (invoice.overdue)
    past_due --> active: paid during grace
    past_due --> canceled: grace ended unpaid (invoice uncollectible)
    active --> canceled: POST …/cancel
    active --> completed: last instalment paid
  • One invoice per period, never two, and no new invoice while one is unpaid — a customer is never asked for two periods at once.
  • Paying an invoice is an ordinary payment: same fee, same balance credit, same payment.succeeded event.
  • cycles_paid counts paid invoices; an instalment plan becomes completed when it reaches billing_cycles — ship on subscription.completed.
Terminal window
curl -s -X POST http://localhost:4000/v1/subscriptions/$SUB_ID/cancel \
-H "Authorization: Bearer $XAALIS_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: cancel:$SUB_ID" -d '{"at_period_end": false}'
  • at_period_end: false stops now: an open invoice with no payment in progress is voided and its link stops working. If the customer is paying at that very moment, that payment is allowed to finish (and is credited).
  • at_period_end: true keeps the current invoice payable through its grace period and issues no next one.

invoice.created (carries hosted_invoice_url — send it to the customer) · invoice.paid · invoice.overdue · subscription.past_due · subscription.canceled · subscription.completed. The payment.* events of each attempt still fire. See Webhooks.

GET /v1/subscriptions/{id} · GET /v1/subscriptions?status= · GET /v1/invoices/{id} · GET /v1/invoices?subscription=&status= — cursor pagination like payments.

POST /v1/subscriptions/{id}/test_advance (test keys only, Idempotency-Key required) moves the subscription’s clock to its next billing event — the next period, the due date, or the end of grace — so you can test months in seconds. One advance runs at a time (a concurrent one gets 409 invalid_state); a canceled or completed subscription can’t be advanced.

Before the first instalment, tell the customer what happens to instalments already paid if they stop paying — that’s your policy. Refunds through Xaalis are full refunds of each payment.