Billing schedules
Invoa API billing schedules: create milestone and recurring billing plans, list and retrieve them, and trigger a milestone to create its invoice.
A billing schedule plans a series of invoices for one customer: a deposit on signing, a balance before a date, a monthly retainer. Each schedule item becomes an invoice either automatically on its date or when you trigger it.
Note: Billing schedules need the Pro plan (or the Pro trial). On Free and Core, creating a schedule and triggering an item return 422 plan_limit; listing and retrieving still work. If a schedule is set to issue automatically (autoIssue: true), triggering one of its items issues and emails the invoice, so the API key also needs documents:issue and documents:send; without them the request returns 403 insufficient_scope and nothing is created.
| Method | Path | Scope |
|---|---|---|
GET | /schedules | documents:read |
GET | /schedules/{id} | documents:read |
POST | /schedules | documents:draft |
POST | /schedules/items/{id}/trigger | documents:draft, plus documents:issue and documents:send when the schedule auto-issues |
For how schedules work in the app, see Billing schedules.
How items become invoices
- Date items (
triggerType: "date") are invoiced automatically. Every morning at the company's automation time (09:30 Malaysia time by default), Invoa invoices pending date items that fall due within the schedule'sleadDays. - Manual items (
triggerType: "manual") wait until you trigger them, for example "on signing" or "go-live". Trigger them withPOST /schedules/items/{id}/trigger, in the app, or through Claude. - Recurrences create several items at once: every
everyMonthsmonths,counttimes. They start on a fixed date, or a set number of months after a manual item is triggered.
Each triggered item creates one invoice for that contact, with one line: the item's label (and description) at quantity 1 and unitPrice equal to the item's amount, plus tax for its taxType. The invoice's due date follows the contact's payment terms, or 30 days.
If the schedule has autoIssue: false (the default), the invoice is a draft for you to check and issue. With autoIssue: true, Invoa issues it and emails it to the contact straight away.
When every item is invoiced, the schedule's status becomes completed.
The schedule object
GET /schedules/{id}, POST /schedules return:
{
"schedule": {
"id": "5c400000-0000-4000-8000-000000000002",
"tenantId": "7e0a0000-0000-4000-8000-000000000001",
"contactId": "c0a10000-0000-4000-8000-000000000007",
"quotationId": null,
"name": "Teratai Biru, Langkawi retreat and travel desk",
"status": "active",
"autoIssue": false,
"leadDays": 3,
"notes": null,
"createdAt": "2026-10-15T03:00:00.000Z",
"updatedAt": "2026-10-15T03:00:00.000Z"
},
"items": [
{
"id": "5c1e0000-0000-4000-8000-000000000001",
"scheduleId": "5c400000-0000-4000-8000-000000000002",
"seq": 1,
"triggerType": "manual",
"dueDate": null,
"label": "Deposit on signing",
"description": null,
"amount": 500000,
"taxType": "SST_SERVICE",
"status": "pending",
"invoiceId": null,
"triggeredAt": null,
"createdAt": "2026-10-15T03:00:00.000Z",
"updatedAt": "2026-10-15T03:00:00.000Z"
},
{
"id": "5c1e0000-0000-4000-8000-000000000002",
"seq": 2,
"triggerType": "date",
"dueDate": "2026-11-15",
"label": "Balance before departure",
"amount": 1000000,
"taxType": "SST_SERVICE",
"status": "pending",
"invoiceId": null
},
{
"id": "5c1e0000-0000-4000-8000-000000000003",
"seq": 3,
"triggerType": "manual",
"dueDate": null,
"label": "Travel desk retainer 1/6",
"amount": 69900,
"taxType": "SST_SERVICE",
"status": "pending",
"invoiceId": null
}
],
"contact": { "id": "c0a10000-0000-4000-8000-000000000007", "name": "Aina Rahman", "companyName": "Teratai Biru Ventures Sdn. Bhd." },
"invoices": [],
"totals": { "total": 1919400, "invoiced": 0, "pending": 1919400 }
}The example is shortened: it shows three of the eight items and leaves out some fields.
| Field | Description |
|---|---|
schedule.status | active, paused, completed or cancelled. |
schedule.autoIssue | Issue and email invoices automatically. |
schedule.leadDays | How many days before a date item's dueDate it is invoiced. |
schedule.quotationId | The quotation the schedule came from, if any. |
schedule.builder | The items exactly as you sent them when creating the schedule. |
items[].seq | Position in the schedule, starting at 1. |
items[].triggerType | date or manual. Recurrence items anchored on a manual item are manual with no dueDate until that item is triggered, then become date items. |
items[].amount | Amount before tax, in sen. |
items[].status | pending, invoiced or skipped. |
items[].invoiceId | The invoice created for this item. |
invoices | The invoices created so far: id, number, status, total, paidAmount, issueDate, dueDate. |
totals | total of all items, invoiced and pending, before tax, in sen. |
contact | The full contact object (shortened above). |
List schedules
GET /api/v1/schedulesReturns schedules newest first. Each item is the schedule record plus contactName, itemCount, pendingCount, total (sen, excluding skipped items) and nextDueDate.
| Query parameter | Type | Description |
|---|---|---|
status | string | active, paused, completed or cancelled. |
contactId | string | Only this contact's schedules. |
limit, offset | integer | See Pagination. |
curl "https://app.invoa.io/api/v1/schedules?status=active" \
-H "Authorization: Bearer $INVOA_API_KEY"{
"data": [
{
"id": "5c400000-0000-4000-8000-000000000002",
"name": "Teratai Biru, Langkawi retreat and travel desk",
"status": "active",
"autoIssue": false,
"leadDays": 3,
"contactId": "c0a10000-0000-4000-8000-000000000007",
"contactName": "Aina Rahman",
"itemCount": 8,
"pendingCount": 8,
"total": 1919400,
"nextDueDate": "2026-11-15"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false }
}Retrieve a schedule
GET /api/v1/schedules/{id}curl https://app.invoa.io/api/v1/schedules/5c400000-0000-4000-8000-000000000002 \
-H "Authorization: Bearer $INVOA_API_KEY"Returns { "data": <schedule object> }. Errors: 404 not_found.
Create a schedule
POST /api/v1/schedules| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | Yes | The customer to bill. |
name | string | Yes | 1 to 200 characters. Used as the reference on each invoice. |
items | array | Yes | 1 to 50 entries, each a one-off item or a recurrence. |
autoIssue | boolean | No | Issue and email invoices automatically. Default false. |
leadDays | integer | No | 0 to 60. Default 3. |
quotationId | string | No | The quotation this schedule bills. |
notes | string | No | Up to 5,000 characters. |
One-off item
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | one_off. |
triggerType | string | Yes | date or manual. |
dueDate | date | For date | When the item is due. |
label | string | Yes | 1 to 200 characters. Becomes the invoice title and line description. |
description | string | No | Up to 2,000 characters, added to the line description. |
amount | integer | Yes | Amount before tax, in sen, more than 0. |
taxType | string | Yes | See Tax types. |
Recurrence
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | recurrence. |
everyMonths | integer | Yes | Months between items, 1 to 60. |
count | integer | Yes | Number of items, 1 to 120. |
startRule | object | Yes | { "type": "date", "date": "2026-12-01" }, or { "type": "after_item", "afterSeq": 1, "offsetMonths": 1 } to start after a manual item is triggered. afterSeq must point at a manual item earlier in the list; the first recurrence item falls due offsetMonths after that item is triggered (default 1). |
label | string | Yes | Use {n} for the occurrence number. For date-started recurrences, {month} becomes the month, for example "Dec 2026". |
description | string | No | Up to 2,000 characters. |
amount | integer | Yes | Amount per item before tax, in sen. |
taxType | string | Yes | See Tax types. |
curl https://app.invoa.io/api/v1/schedules \
-H "Authorization: Bearer $INVOA_API_KEY" \
-H "content-type: application/json" \
-d '{
"contactId": "c0a10000-0000-4000-8000-000000000007",
"name": "Teratai Biru, Langkawi retreat and travel desk",
"items": [
{ "kind": "one_off", "triggerType": "manual", "label": "Deposit on signing", "amount": 500000, "taxType": "SST_SERVICE" },
{ "kind": "one_off", "triggerType": "date", "dueDate": "2026-11-15", "label": "Balance before departure", "amount": 1000000, "taxType": "SST_SERVICE" },
{
"kind": "recurrence",
"everyMonths": 1,
"count": 6,
"startRule": { "type": "after_item", "afterSeq": 1, "offsetMonths": 1 },
"label": "Travel desk retainer {n}/6",
"amount": 69900,
"taxType": "SST_SERVICE"
}
]
}'Returns 201 Created with { "data": <schedule object> }. Items are numbered in order: here the deposit is seq 1, the balance seq 2 and the six retainer items seq 3 to 8.
Errors:
400 validation, for example adateitem withoutdueDate, or anafter_itemrecurrence that points at a date item or a later item.404 not_foundif the contact is not in this company.
Trigger a schedule item
POST /api/v1/schedules/items/{id}/triggerCreates the invoice for one pending item now. Use it for manual milestones, or to bill a date item early. {id} is the item's ID from items[].id, not the schedule's.
| Field | Type | Required | Description |
|---|---|---|---|
issueDate | date | No | Invoice date. Default today in the company's timezone. |
The body is optional. Triggering a manual item that a recurrence is anchored on also dates that recurrence's items.
curl -X POST https://app.invoa.io/api/v1/schedules/items/5c1e0000-0000-4000-8000-000000000001/trigger \
-H "Authorization: Bearer $INVOA_API_KEY"{
"data": {
"item": {
"id": "5c1e0000-0000-4000-8000-000000000001",
"seq": 1,
"label": "Deposit on signing",
"status": "invoiced",
"invoiceId": "d0c00000-0000-4000-8000-000000000050",
"triggeredAt": "2026-10-16T01:05:00.000Z"
},
"issued": false,
"invoice": {
"id": "d0c00000-0000-4000-8000-000000000050",
"type": "invoice",
"status": "draft",
"number": null,
"title": "Deposit on signing",
"reference": "Teratai Biru, Langkawi retreat and travel desk",
"subtotal": 500000,
"taxTotal": 40000,
"total": 540000
}
}
}item is the updated schedule item and invoice the full document object (both shortened). issued is true when the schedule has autoIssue and the invoice was issued and emailed; in that case Invoa also sends the document.issued and document.sent webhooks, as it does when the daily schedule run issues an invoice.
Important: On a schedule with autoIssue: true, this endpoint issues and emails a real invoice. The key needs documents:issue and documents:send as well as documents:draft, or the call returns 403 insufficient_scope.
Errors:
403 insufficient_scopeif the schedule issues automatically and the key lacksdocuments:issueordocuments:send.404 not_foundif the item or its schedule is not in this company.422 invalid_stateif the item is alreadyinvoicedorskipped, or the schedule is notactive.
Related
Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.