Recurring schedules
Auto-generate invoices / bills / expenses / journal entries on a cron-like cadence. Subscription billing, monthly retainers, quarterly rent invoices, yearly insurance renewals — all configured once and fired by the FinzBooks scheduler.
A schedule has an entity_type (what document it emits), a frequency (DAILY / WEEKLY / MONTHLY / QUARTERLY / YEARLY) with an interval multiplier (so “every 2 weeks” is WEEKLY with interval: 2), and a template — either a reference to an existing document to clone, or an inline JSON payload to build from scratch. The background job wakes up daily at 09:00 IST, finds schedules whose next_run_at has passed, generates the documents, and advances next_run_at by one interval.
Resource shape
{
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-06-01T03:30:00Z",
"last_run_at": null,
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 0,
"max_runs": null,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:00:00Z"
}Common request headers
Authorization: Bearer aibooks_pat_XXXX...
X-AIBooks-Org: org_8f9a1c2d
Content-Type: application/json/recurringList recurring schedulesAIBooks.recurring.READFilter by ?entity_type=INVOICE or ?is_active=true. Ordered by next_run_atascending — soonest-firing first, which is what an “upcoming runs” widget needs.
Sample request
GET /api/public/v1/recurring?entity_type=INVOICE&is_active=true&per_page=25 HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "success",
"recurring": [
{
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-06-01T03:30:00Z",
"last_run_at": null,
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 0,
"max_runs": null,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:00:00Z"
},
{
"recurring_id": "rec_5e7f9a1b2c3d4e5f6a7b8c9d0e1f2a3b",
"entity_type": "INVOICE",
"name": "Contoso quarterly licence",
"frequency": "QUARTERLY",
"interval": 1,
"start_date": "2026-04-01",
"end_date": "2027-03-31",
"next_run_at": "2026-07-01T03:30:00Z",
"last_run_at": "2026-04-01T03:30:00Z",
"is_active": true,
"template_entity_id": "inv_template_def",
"template_payload": null,
"run_count": 1,
"max_runs": 4,
"auto_charge": true,
"created_time": "2026-03-20T10:14:00Z",
"last_modified_time": "2026-04-01T03:30:02Z"
}
],
"page_context": { "page": 1, "per_page": 25, "has_more_page": false }
}/recurring/{schedule_id}Get one scheduleAIBooks.recurring.READStandard single-resource fetch. Returns the same shape as the list row.
Sample request
GET /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02 HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "success",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-06-01T03:30:00Z",
"last_run_at": null,
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 0,
"max_runs": null,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:00:00Z"
}
}/recurringCreate a scheduleAIBooks.recurring.CREATEPass exactly one of template_entity_id (clone an existing document on every run) or template_payload (inline JSON template). The clone path is by far the most common — you build a prototype invoice in the dashboard, save it, then attach a schedule that clones it monthly.
Body
| Field | Notes |
|---|---|
entity_type | What kind of document to emit. |
name | Human-friendly label shown in the dashboard. |
frequency | DAILY / WEEKLY / MONTHLY / QUARTERLY / YEARLY. |
interval | Every N frequency units. Default 1. |
start_date | First run fires at 09:00 IST on this date. |
end_date | Optional cap. After this date the schedule auto-pauses. |
template_entity_id | ID of an existing document to clone. |
template_payload | Inline JSON — same shape as the resource's POST body. |
max_runs | Stop after N successful runs. |
auto_charge | Invoice schedules only: auto-create a online payment link on each new invoice and embed the URL. |
is_active | Default true. Create paused with false if you want to review before activating. |
Sample request — clone an existing invoice
POST /api/public/v1/recurring HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2d
Content-Type: application/json
{
"entity_type": "INVOICE",
"name": "Acme monthly retainer",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"template_entity_id": "inv_template_abc",
"max_runs": null,
"auto_charge": false,
"is_active": true
}Sample request — inline payload
POST /api/public/v1/recurring HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2d
Content-Type: application/json
{
"entity_type": "INVOICE",
"name": "Office rent — landlord Mehta",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"template_payload": {
"customer_id": "cnt_landlord_mehta",
"branch_id": "br_hyd_01",
"currency": "INR",
"place_of_supply": "TS",
"line_items": [
{
"item_id": "itm_office_rent",
"quantity": 1,
"rate": 85000,
"tax_id": "tax_gst_18"
}
],
"notes": "Auto-generated monthly rent invoice."
}
}Sample response
HTTP/1.1 201 Created
Content-Type: application/json
{
"code": 0,
"message": "The recurring schedule has been created.",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-06-01T03:30:00Z",
"last_run_at": null,
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 0,
"max_runs": null,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:00:00Z"
}
}/recurring/{schedule_id}Update a scheduleAIBooks.recurring.UPDATEPartial update. Pass only the fields you want changed. Updating frequency / interval /start_date recomputes next_run_at on the next scheduler tick — you can also set next_run_at explicitly to skip ahead.
Sample request
PUT /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02 HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2d
Content-Type: application/json
{
"name": "Acme monthly retainer (₹95k from FY27)",
"max_runs": 24,
"next_run_at": "2026-07-01T03:30:00Z"
}Sample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "The recurring schedule has been updated.",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer (₹95k from FY27)",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-07-01T03:30:00Z",
"last_run_at": null,
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 0,
"max_runs": 24,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:42:11Z"
}
}/recurring/{schedule_id}/runFire immediatelyAIBooks.recurring.UPDATEManual trigger. Generates one document using the exact same code path the background scheduler uses, increments run_count, advances next_run_at by one interval, and sets last_run_atto now. Returns the new document's ID so the integrator can fetch the full resource.
Sample request
POST /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02/run HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "The schedule has fired.",
"generated_entity_id": "inv_new_42a91f",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"entity_type": "INVOICE",
"name": "Acme monthly retainer (₹95k from FY27)",
"frequency": "MONTHLY",
"interval": 1,
"start_date": "2026-06-01",
"end_date": null,
"next_run_at": "2026-08-01T03:30:00Z",
"last_run_at": "2026-05-12T07:45:00Z",
"is_active": true,
"template_entity_id": "inv_template_abc",
"template_payload": null,
"run_count": 1,
"max_runs": 24,
"auto_charge": false,
"created_time": "2026-05-12T07:00:00Z",
"last_modified_time": "2026-05-12T07:45:00Z"
}
}If the template document was deleted between schedule create and the manual run, the call fails with validation.invalid_value and the schedule is not advanced.
/recurring/{schedule_id}/pausePauseAIBooks.recurring.UPDATESample request
POST /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02/pause HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "The recurring schedule has been paused.",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"is_active": false,
"next_run_at": "2026-08-01T03:30:00Z"
/* …rest of schedule fields */
}
}/recurring/{schedule_id}/resumeResumeAIBooks.recurring.UPDATEConvenience setters for is_active. Paused schedules are skipped by the scheduler but retain their next_run_at — resuming picks back up where the timeline left off (or shifts forward if next_run_at is already in the past).
Sample request
POST /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02/resume HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "The recurring schedule has been resumed.",
"recurring": {
"recurring_id": "rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02",
"is_active": true,
"next_run_at": "2026-08-01T03:30:00Z"
/* …rest of schedule fields */
}
}/recurring/{schedule_id}Delete a scheduleAIBooks.recurring.DELETEHard delete. Documents already generated by past runs are not affected.
Sample request
DELETE /api/public/v1/recurring/rec_8c3f1a9b40b14e3aaf2d7e91a76c1f02 HTTP/1.1
Host: api.aibooks.in
Authorization: Bearer aibooks_pat_3p9q0v1w2x3y4z5a6b7c8d9e
X-AIBooks-Org: org_8f9a1c2dSample response
HTTP/1.1 200 OK
Content-Type: application/json
{
"code": 0,
"message": "The recurring schedule has been deleted."
}Errors
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "validation.required_field",
"message": "Either 'template_entity_id' or 'template_payload' must be supplied.",
"field": "template_entity_id"
}| Code | HTTP | When |
|---|---|---|
validation.required_field | 400 | Neither template_entity_id nor template_payload was supplied at create time. |
validation.invalid_value | 400 | Both template_entity_id and template_payload set, OR unknown entity_type / frequency, OR the template was deleted before a manual run. |
not_found.resource | 404 | Schedule ID not found in this org. |
Worked example — monthly retainer invoice
- Create a template invoice manually in the dashboard (e.g.
INV-RETAINER-TEMPLATE). Note its ID. POST /recurringwithentity_type: INVOICE,frequency: MONTHLY,start_date: 2026-06-01, andtemplate_entity_id: inv_template_id.- On the 1st of every month at 09:00 IST the scheduler clones the template, generates a new invoice number, stamps today as the invoice date, and emits an
INVOICE_CREATEDwebhook. - If the customer pays directly through a Razorpay link (when
auto_charge: true), a webhook fires and the invoice flips to PAID without manual reconciliation.