API reference
Invoices and payments
Billing periods, their invoices, and the payments made against them, and how to list and read invoices.
A subscription bills one billing period at a time. Each period has one invoice, and each attempt to pay it is a payment. You read all three when you retrieve a subscription. To browse invoices across every subscription, list invoices. To send a customer to a hosted page to pay an open invoice, use Pay an invoice.
The billing period object
Creating a subscription returns its first billing period as entry. Abuna creates the next one when a paid period ends, and issues its invoice.
Attributes
idstringUnique identifier for the billing period.subscription_idstringThe subscription the period belongs to.price_idstringThe price the period bills.pay_tokenstringThe token in the period's pay link.starts_attimestampWhen the period starts, in Unix seconds.ends_attimestampWhen the period ends, in Unix seconds.period_indexintegerThe period's place in the subscription, starting at0.grace_period_ends_attimestampThe date to pay by: the app's grace period after the period starts, but never after it ends. If it passes unpaid, Abuna cancels the subscription.paid_attimestampnullableWhen the period was paid.paused_attimestampnullableReserved for paused periods. Always null.in_forcebooleanfalsefor an upgrade's period while it waits for payment, and for good if the upgrade lapses. Such a period doesn't count as one of the subscription's periods, and the next period reuses itsperiod_index.created_attimestampWhen the period was created, in Unix seconds.
{
"id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"pay_token": "3a7e1c9f5b2d8a4e6c0f7b3d9a1e5c8f2b6d",
"starts_at": 1727600000,
"ends_at": 1730192000,
"period_index": 0,
"grace_period_ends_at": 1727686400,
"paid_at": null,
"paused_at": null,
"in_force": true,
"created_at": 1727600000
}The invoice object
An invoice keeps the amount, buyer, and seller as they were when Abuna issued it. Later changes don't alter it.
Attributes
idstringUnique identifier for the invoice.app_idstringThe app that issued the invoice.entry_idstringThe billing period the invoice bills.subscription_idstringThe subscription the invoice belongs to.numberstringThe invoice number.statusstringopenuntil paid, thenpaid.voidwhen the subscription was canceled before it was paid, or a plan change replaced it.kindstringamountintegerThe amount due, in the currency's smallest unit.creditintegerThe unused time of the previous period taken off the price.0unlesskindisplan_change.currencystringXAForXOF.item_labelstringThe price's name.buyer_namestringnullableThe customer's name.buyer_emailstringnullableThe customer's email address.seller_namestringThe app's name.period_starttimestampWhen the billed period starts.period_endtimestampWhen the billed period ends.due_attimestampThe date to pay by.created_attimestampWhen the invoice was issued, in Unix seconds.paid_attimestampnullableWhen the invoice was paid.voided_attimestampnullableWhen the invoice was voided.
{
"id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
"entry_id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"number": "01J9ZQ7N1P2Q3R4S5T6V7W8X9Y",
"status": "open",
"kind": "period",
"amount": 5000,
"credit": 0,
"currency": "XAF",
"item_label": "Monthly",
"buyer_name": "Jane Doe",
"buyer_email": "jane@example.com",
"seller_name": "My store",
"period_start": 1727600000,
"period_end": 1730192000,
"due_at": 1727686400,
"created_at": 1727600000,
"paid_at": null,
"voided_at": null
}The payment object
Attributes
idstringUnique identifier for the payment.app_idstringThe app the payment belongs to.entry_idstringThe billing period the payment is for.driverstringThe payment provider, likepawapay.sandboxfor a simulated Test payment.providerstringThe network the provider charged, likeMTN_MOMO_CMR.provider_referencestringnullableThe provider's own ID for the charge.amountintegerThe amount charged, in the currency's smallest unit.currencystringXAForXOF.statusstringpending,succeeded, orfailed.messagestringnullableThe failure code, likeinsufficient_funds, or null when the payment hasn't failed.created_attimestampWhen the payment started, in Unix seconds.feeintegerThe provider's fee, in the currency's smallest unit.session_idstringnullableThe session used to make this payment, or null for a payment made another way.method_labelstringThe network's name, in words.
{
"amount": 5000,
"app_id": "01M4047ET949K8JPEZ25NMS0T2",
"created_at": 1791005801,
"currency": "XAF",
"driver": "sandbox",
"entry_id": "01M4047ETRRSRY6EY5FH0X9XCG",
"fee": 100,
"id": "01M4047ETZM9J6NNBFK50EYY8Z",
"message": null,
"method_label": "Successful payment (test)",
"provider": "test",
"provider_reference": "test_08abc6ec9bdd456b8bbc",
"session_id": null,
"status": "succeeded"
}The invoice summary object
The invoice endpoints return invoices in this shape. It adds the customer and whether the invoice is overdue, and leaves out app_id, entry_id, and seller_name.
Attributes
idstringUnique identifier for the invoice.numberstringThe invoice number.statusstringopenuntil paid, thenpaid.voidwhen the subscription was canceled before it was paid, or a plan change replaced it.overduebooleantruewhen the invoice isopenand itsdue_athas passed. Abuna works this out each time you ask. It is not a status.kindstringamountintegerThe amount due, in the currency's smallest unit.creditintegerThe unused time taken off the price, as on the invoice object.currencystringXAForXOF.item_labelstringThe price's name.buyer_namestringnullableThe customer's name.buyer_emailstringnullableThe customer's email address.subscription_idstringThe subscription the invoice belongs to.customer_idstringThe subscription's customer.period_starttimestampWhen the billed period starts.period_endtimestampWhen the billed period ends.due_attimestampThe billing period's date to pay by (grace_period_ends_at), as it stands now. It can differ from thedue_aton the invoice object, which is set when the invoice is issued.created_attimestampWhen the invoice was issued, in Unix seconds.paid_attimestampnullableWhen the invoice was paid.voided_attimestampnullableWhen the invoice was voided.
{
"id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"number": "01J9ZQ7N1P2Q3R4S5T6V7W8X9Y",
"status": "open",
"overdue": false,
"kind": "period",
"amount": 5000,
"credit": 0,
"currency": "XAF",
"item_label": "Monthly",
"buyer_name": "Jane Doe",
"buyer_email": "jane@example.com",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"period_start": 1727600000,
"period_end": 1730192000,
"due_at": 1727686400,
"created_at": 1727600000,
"paid_at": null,
"voided_at": null
}List invoices
GET/v1/apps/{appID}/invoices
Lists the app's invoices, newest first, one page at a time. Test mode and Live mode have separate invoices, so a list shows only the mode of the app you ask about. All parameters are optional, and a blank one is ignored.
Parameters
statusstringopen,paid,void, oroverdue.openincludes the overdue invoices.overduereturns the open invoices whose date to pay by has passed.searchstringPart of the invoice number, buyer name, or buyer email. Ignores case.fromtimestampOnly invoices issued at or after this time, in Unix seconds.totimestampOnly invoices issued before this time, in Unix seconds.limitintegerHow many invoices to return, from 1 to 100. Defaults to 25.cursorstringThenext_cursorfrom the previous page. Send the same filters with it.
Response attributes
itemsarrayThe page's invoices. Empty when nothing matches.next_cursorstringnullablePass it as cursor to get the next page. It is null on the last page. Treat it as opaque.
Returns 200. A value that isn't allowed returns 422 with code invalid and the parameter's name in errors.
curl "https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/invoices?status=overdue&limit=25" \
-H "Authorization: Bearer sk_test_..."{
"items": [
{
"id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"number": "01J9ZQ7N1P2Q3R4S5T6V7W8X9Y",
"status": "open",
"overdue": false,
"kind": "period",
"amount": 5000,
"credit": 0,
"currency": "XAF",
"item_label": "Monthly",
"buyer_name": "Jane Doe",
"buyer_email": "jane@example.com",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"period_start": 1727600000,
"period_end": 1730192000,
"due_at": 1727686400,
"created_at": 1727600000,
"paid_at": null,
"voided_at": null
}
],
"next_cursor": "MTcyNzYwMDAwMDAwMDAwMC4wMUo5WlE3VjFXMlgzWTRaNUE2QjdDOEQ5RQ"
}Retrieve an invoice
GET/v1/apps/{appID}/invoices/{id}
Retrieves an invoice with its links and its history, from when it was issued to when it was paid or voided.
Response attributes
invoiceobjectpay_urlstringnullableThe pay link for the billing period. It is null once the invoice is paid or void, or when the subscription is canceled.invoice_urlstringA link to the customer's invoice page, which also shows paid and void invoices. It expires after 7 days.timelinearrayWhat happened to the invoice, oldest first. Events in the same second come in the order below.Show child attributesHide child attributes
typestringissued: Abuna issued the invoice.reminder: Abuna queued a reminder.notice: a message to the customer about the invoice.payment: an attempt to pay, whether it succeeded, failed, or is still pending.paid: the invoice was paid.voided: the invoice was voided.
attimestampWhen it happened, in Unix seconds. For a notice, when it was sent, or created if it is not sent yet.noticeobjectpaymentobjectreminderobjectOn areminderevent: an object withkind, one ofrenewal_upcoming,payment_overdue, orpayment_final_notice.
Returns 200. If the app has no invoice with that ID, returns 404 with code not_found.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/invoices/01J9ZQ7V1W2X3Y4Z5A6B7C8D9E \
-H "Authorization: Bearer sk_test_..."{
"invoice": {
"id": "01J9ZQ7V1W2X3Y4Z5A6B7C8D9E",
"number": "01J9ZQ7N1P2Q3R4S5T6V7W8X9Y",
"status": "open",
"overdue": false,
"kind": "period",
"amount": 5000,
"credit": 0,
"currency": "XAF",
"item_label": "Monthly",
"buyer_name": "Jane Doe",
"buyer_email": "jane@example.com",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"period_start": 1727600000,
"period_end": 1730192000,
"due_at": 1727686400,
"created_at": 1727600000,
"paid_at": null,
"voided_at": null
},
"pay_url": "https://app.abuna.app/pay/3a7e1c9f5b2d8a4e6c0f7b3d9a1e5c8f2b6d",
"invoice_url": "https://app.abuna.app/invoice/01J9ZQ7E1F2G3H4J5K6M7N8P9Q?exp=1728204800&sig=b4e1c8f2a7d3960e5b1f8c4a2d7e3b9f6a0c5e8d1b4f7a2c9e6d3b0f8a5c1e7d",
"timeline": [
{ "type": "issued", "at": 1727600000 },
{
"type": "notice",
"at": 1727600002,
"notice": {
"id": "01J9ZQ9N1P2Q3R4S5T6V7W8X9Y",
"customer_id": "01J9ZQ6A1B2C3D4E5F6G7H8J9K",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"kind": "subscription_started",
"channel": "email",
"recipient": "jane@example.com",
"subject": "Pay FCFA 5,000 to start your Pro · Monthly plan with My store",
"status": "sent",
"attempts": 1,
"last_error": null,
"next_attempt_at": 1727600000,
"created_at": 1727600000,
"sent_at": 1727600002
}
},
{
"type": "payment",
"at": 1727600090,
"payment": {
"id": "01J9ZQ7P1Q2R3S4T5V6W7X8Y9Z",
"app_id": "01J9ZQ4Y7R3T6V8W2X5B1C0DEF",
"entry_id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
"driver": "pawapay",
"provider": "MTN_MOMO_CMR",
"provider_reference": "9f3b7d1a-5c2e-8f4b-6d0a-3e7c1b5a9d2f",
"amount": 5000,
"currency": "XAF",
"status": "failed",
"message": "insufficient_funds",
"created_at": 1727600090,
"fee": 100,
"session_id": null,
"method_label": "MTN Mobile Money Cameroon"
}
},
{ "type": "reminder", "at": 1727686400, "reminder": { "kind": "payment_overdue" } }
]
}Pay a billing period
POST/v1/apps/{appID}/entries/{id}/pay
Charges the customer's saved phone number for an unpaid billing period. It uses the first network your payment provider lists. With simulated Test payments, the charge always succeeds. To have the customer pay on a hosted page instead, use Pay an invoice.
Response attributes
statusstringsucceeded, orpendingwhile the customer approves the charge on their phone.paidbooleantruewhenstatusissucceeded.entryobjectThe billing period object.
Returns 200. A declined charge returns 402 with the reason as its code, like insufficient_funds or payment_declined. These also fail:
403plan_limit: the customer's first payment would pass your plan's Live subscriber limit.404not_found: the app has no billing period with that ID.409already_paid: the period is paid.409invoice_not_open: the invoice is void, because the subscription was canceled.409payment_in_progress: another payment for the period is still pending.422not_accepting_payments: a Live app can't take payments yet.422payment_unavailable: the network can't charge the invoice's currency.
curl https://api.abuna.app/v1/apps/01J9ZQ4Y7R3T6V8W2X5B1C0DEF/entries/01J9ZQ7E1F2G3H4J5K6M7N8P9Q/pay \
-X POST \
-H "Authorization: Bearer sk_test_..."{
"status": "succeeded",
"paid": true,
"entry": {
"id": "01J9ZQ7E1F2G3H4J5K6M7N8P9Q",
"subscription_id": "01J9ZQ6M1N2P3Q4R5S6T7V8W9X",
"price_id": "01J9ZQ5N1P2Q3R4S5T6V7W8XYZ",
"pay_token": "3a7e1c9f5b2d8a4e6c0f7b3d9a1e5c8f2b6d",
"starts_at": 1727600000,
"ends_at": 1730192000,
"period_index": 0,
"grace_period_ends_at": 1727686400,
"paid_at": 1727600100,
"paused_at": null,
"in_force": true,
"created_at": 1727600000
}
}Next steps
- SubscriptionsStart, list, read, update, cancel, keep, and change the plan of a customer's subscription to a price.
- NoticesThe emails and Telegram messages Abuna sent your customers.
- Lists and filtersWhat list endpoints return, how to filter them, and how many rows they send.
- EventsThe record of everything that happened in an app, and every event type.