Docs / API reference

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.

MethodPathScope
GET/schedulesdocuments:read
GET/schedules/{id}documents:read
POST/schedulesdocuments:draft
POST/schedules/items/{id}/triggerdocuments: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's leadDays.
  • Manual items (triggerType: "manual") wait until you trigger them, for example "on signing" or "go-live". Trigger them with POST /schedules/items/{id}/trigger, in the app, or through Claude.
  • Recurrences create several items at once: every everyMonths months, count times. 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:

json
{
  "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.

FieldDescription
schedule.statusactive, paused, completed or cancelled.
schedule.autoIssueIssue and email invoices automatically.
schedule.leadDaysHow many days before a date item's dueDate it is invoiced.
schedule.quotationIdThe quotation the schedule came from, if any.
schedule.builderThe items exactly as you sent them when creating the schedule.
items[].seqPosition in the schedule, starting at 1.
items[].triggerTypedate or manual. Recurrence items anchored on a manual item are manual with no dueDate until that item is triggered, then become date items.
items[].amountAmount before tax, in sen.
items[].statuspending, invoiced or skipped.
items[].invoiceIdThe invoice created for this item.
invoicesThe invoices created so far: id, number, status, total, paidAmount, issueDate, dueDate.
totalstotal of all items, invoiced and pending, before tax, in sen.
contactThe full contact object (shortened above).

List schedules

http
GET /api/v1/schedules

Returns schedules newest first. Each item is the schedule record plus contactName, itemCount, pendingCount, total (sen, excluding skipped items) and nextDueDate.

Query parameterTypeDescription
statusstringactive, paused, completed or cancelled.
contactIdstringOnly this contact's schedules.
limit, offsetintegerSee Pagination.
bash
curl "https://app.invoa.io/api/v1/schedules?status=active" \
  -H "Authorization: Bearer $INVOA_API_KEY"
json
{
  "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

http
GET /api/v1/schedules/{id}
bash
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

http
POST /api/v1/schedules
FieldTypeRequiredDescription
contactIdstringYesThe customer to bill.
namestringYes1 to 200 characters. Used as the reference on each invoice.
itemsarrayYes1 to 50 entries, each a one-off item or a recurrence.
autoIssuebooleanNoIssue and email invoices automatically. Default false.
leadDaysintegerNo0 to 60. Default 3.
quotationIdstringNoThe quotation this schedule bills.
notesstringNoUp to 5,000 characters.

One-off item

FieldTypeRequiredDescription
kindstringYesone_off.
triggerTypestringYesdate or manual.
dueDatedateFor dateWhen the item is due.
labelstringYes1 to 200 characters. Becomes the invoice title and line description.
descriptionstringNoUp to 2,000 characters, added to the line description.
amountintegerYesAmount before tax, in sen, more than 0.
taxTypestringYesSee Tax types.

Recurrence

FieldTypeRequiredDescription
kindstringYesrecurrence.
everyMonthsintegerYesMonths between items, 1 to 60.
countintegerYesNumber of items, 1 to 120.
startRuleobjectYes{ "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).
labelstringYesUse {n} for the occurrence number. For date-started recurrences, {month} becomes the month, for example "Dec 2026".
descriptionstringNoUp to 2,000 characters.
amountintegerYesAmount per item before tax, in sen.
taxTypestringYesSee Tax types.
bash
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 a date item without dueDate, or an after_item recurrence that points at a date item or a later item.
  • 404 not_found if the contact is not in this company.

Trigger a schedule item

http
POST /api/v1/schedules/items/{id}/trigger

Creates 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.

FieldTypeRequiredDescription
issueDatedateNoInvoice 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.

bash
curl -X POST https://app.invoa.io/api/v1/schedules/items/5c1e0000-0000-4000-8000-000000000001/trigger \
  -H "Authorization: Bearer $INVOA_API_KEY"
json
{
  "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_scope if the schedule issues automatically and the key lacks documents:issue or documents:send.
  • 404 not_found if the item or its schedule is not in this company.
  • 422 invalid_state if the item is already invoiced or skipped, or the schedule is not active.

Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.