API
REST API for boards, cards, team-visible notes, CRM, time, expenses, and invoices. Available on the Max plan. Create API keys in your team settings (Boards → Settings → API).
For AI assistants via the Model Context Protocol, use the @florianindustries/saku-mcp package on npm (registry) with the same API keys and scopes. See the MCP server docs.
Authentication
Send your API key in the Authorization header as a Bearer token.
Authorization: Bearer YOUR_API_KEY
Rate limits
Saku REST API keys are limited to 120 requests per 60-second window. Authenticated responses include the standard RateLimit-Policy and RateLimit-Limit headers. A 429 response also includes RateLimit, RateLimit-Remaining, RateLimit-Reset, and Retry-After. Wait for the stated number of seconds before retrying. Do not automatically retry a write unless it is safe and idempotent.
Base URL
All endpoints are relative to saku.ie, e.g. https://saku.ie/api/v1.
Versioning and deprecation
Saku versions its REST API in the URL. The current stable version is v1. We make compatible additions within a version. Before removing or changing behaviour incompatibly, we announce the change at least 90 days ahead. Deprecated operations include Deprecation: true,Sunset, and a link to this policy in their responses.
Endpoints
/api/v1/boardsList all boards for the team associated with your API key. Requires boards:read. Returns boards with lists, labels, and board-level member roles. The teamMembers field (names and emails) is included only when the key also has members:read ; use GET /api/v1/members for the member directory.
Response (200)
{
"boards": [
{
"id": "board_abc",
"name": "My Board",
"description": null,
"lists": [
{ "id": "list_xyz", "name": "To Do", "position": 0 }
],
"labels": [
{ "id": "label_1", "name": "Bug", "colour": "#ef4444" }
],
"members": [{ "userId": "user_1", "role": "EDITOR" }]
}
],
"teamMembers": [
{ "id": "user_1", "name": "Jane", "email": "[email protected]" }
]
}The example above includes teamMembers as returned when the key has members:read.
/api/v1/membersList team members with IDs, names, and emails. Requires the members:read permission (separate from boards:read).
Response (200)
{
"members": [
{ "id": "user_1", "name": "Jane", "email": "[email protected]" }
]
}/api/v1/boards/:idGet a specific board with all its lists and cards. Requires the boards:read permission. The teamMembers field is present only when the key includes members:read.
Response (200)
{
"id": "board_abc",
"name": "My Board",
"description": null,
"lists": [
{
"id": "list_xyz",
"name": "To Do",
"position": 0,
"cards": [
{
"id": "card_1",
"title": "Fix login bug",
"description": "Happens on mobile",
"position": 0,
"priority": "HIGH",
"dueDate": "2026-04-15T00:00:00.000Z",
"assignees": [{ "userId": "user_1" }],
"labels": [{ "id": "label_1", "name": "Bug", "colour": "#ef4444" }]
}
]
}
],
"labels": [{ "id": "label_1", "name": "Bug", "colour": "#ef4444" }],
"members": [{ "userId": "user_1", "role": "EDITOR" }],
"teamMembers": [{ "id": "user_1", "name": "Jane", "email": "[email protected]" }]
}/api/v1/cards/:idGet full details for a card, including checklists, assignees, and labels. Requires the cards:read permission.
Response (200)
{
"id": "card_1",
"title": "Fix login bug",
"description": "Happens on mobile",
"listId": "list_xyz",
"position": 0,
"priority": "HIGH",
"dueDate": "2026-04-15T00:00:00.000Z",
"createdAt": "2026-01-01T12:00:00.000Z",
"updatedAt": "2026-01-02T08:00:00.000Z",
"createdViaApiSource": null,
"assignees": [{ "userId": "user_1" }],
"labels": [{ "id": "label_1", "name": "Bug", "colour": "#ef4444" }],
"checklists": [
{
"id": "cl_1",
"name": "Acceptance criteria",
"items": [
{ "id": "cli_1", "content": "Reproduces on iOS", "completed": true, "position": 0 }
]
}
]
}/api/v1/cards/:idUpdate a card. Only send the fields you want to change. Requires the cards:update permission.
Request body
| Field | Type | Description |
|---|---|---|
| title | string | New card title |
| description | string | New card description |
| priority | string | null | CRITICAL, URGENT, HIGH, MEDIUM, LOW, or MINOR. Pass null to clear. |
| dueDate | string | null | ISO 8601 date string, or null to clear |
Response (200)
{
"id": "card_1",
"title": "Fixed login bug",
"description": "Happens on mobile",
"listId": "list_xyz",
"position": 0,
"priority": "MEDIUM",
"dueDate": null,
"updatedAt": "2026-04-01T10:30:00.000Z"
}/api/v1/cards/:idArchive a card (soft remove). The card is hidden from the board but can be restored in the Saku app. Requires the cards:delete permission.
Response (200)
{ "success": true, "archived": true }Returns 404 if the card does not exist, belongs to another team, or is already archived. Returns 401 without a valid API key and 403 without the cards:delete scope.
/api/v1/listsCreate a new list (column) in a board. The list is appended after existing lists. Requires the lists:create permission.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| boardId | string | Yes | Board to add the list to |
| name | string | Yes | List name |
Response (201)
{
"id": "list_new",
"name": "Done",
"position": 3,
"boardId": "board_abc"
}/api/v1/cardsCreate a card in a list. Use the boards endpoint to get valid listId, label ids, and assignee user ids (or emails).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| listId | string | Yes | List ID (from GET /boards) |
| title | string | Yes | Card title |
| content | string | No | Card description |
| label | string | No | Single label ID |
| labels | string[] | No | Label IDs |
| assignee | string | No | User ID or email |
| assignees | string[] | No | User IDs or emails |
| source | string | No | Optional label shown as "Created via API (source)" |
Example request
POST /api/v1/cards
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"listId": "list_xyz",
"title": "New task",
"content": "Description here",
"label": "label_1",
"assignee": "user_1",
"source": "Contact Us Form"
}Response (201)
{
"id": "card_abc",
"title": "New task",
"description": "Description here",
"listId": "list_xyz",
"position": 0,
"createdViaApiSource": "Contact Us Form",
"assignees": [{ "userId": "user_1" }],
"labels": [{ "id": "label_1", "name": "Bug", "colour": "#ef4444" }]
}Notes
Team-visible notes only. A team API key can list and read notes in team vaults (vault visibility TEAM, not archived or deleted). Notes marked private are excluded even inside a team vault. Personal vaults are never exposed. Creating notes requires the API key to have an associated creator user (createdByUserId). See the Notes product guide for vaults, wiki links, and sharing behaviour.
/api/v1/notesList team-visible notes with pagination. Also returns writable team vaults (for discovering vaultId before create). Requires notes:read.
Query: vaultId (optional), limit (default 100, max 500), offset.
Response (200)
{
"notes": [
{
"id": "note_abc",
"title": "Meeting notes",
"vaultId": "vault_xyz",
"folderId": null,
"isPinned": false,
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-10T14:30:00.000Z"
}
],
"vaults": [{ "id": "vault_xyz", "name": "Team wiki" }],
"limit": 100,
"offset": 0
}/api/v1/notes/:idGet a note including full contentMarkdown. Requires notes:read.
Response (200)
{
"id": "note_abc",
"title": "Meeting notes",
"contentMarkdown": "# Summary\n\nSee [[note:other-id]]",
"vaultId": "vault_xyz",
"folderId": null,
"isPinned": false,
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-10T14:30:00.000Z"
}/api/v1/notesCreate a note in a team vault. Wiki-links in contentMarkdown (e.g. [[note:ID]]) are synced automatically. Requires notes:create.
Request body
{
"vaultId": "vault_xyz",
"title": "Optional title",
"contentMarkdown": "# Body in Markdown"
}vaultId is required. Title defaults to "Untitled" when omitted.
Response (201)
{
"note": {
"id": "note_new",
"title": "Optional title",
"vaultId": "vault_xyz",
"createdAt": "2026-06-11T09:00:00.000Z"
}
}/api/v1/notes/:idUpdate title and/or content. Send only fields to change; contentMarkdown replaces the entire body (read-modify-write via GET first). Requires notes:update.
Request body
{
"title": "New title",
"contentMarkdown": "Updated body"
}At least one field is required.
CRM
Accounts, contacts, leads, and deals for the team associated with your API key. Read endpoints require crm:read; create and update require crm:write. Sensitive administration needs additional Max API-key scopes (crm:export, crm:import, crm:merge, crm:forecast, crm:automation); existing keys are not broadened automatically. Human OWNER/ADMIN callers may export, import and merge on Pro+. The workspace must be on Pro or Max (effective plan, including trial), and write callers must be OWNER, ADMIN, or MEMBER. VIEWER and GUEST roles are read-only. Free or downgraded workspaces receive 403 with CRM requires Pro or Max. After a Max downgrade, workflow definitions and run history stay readable on Pro, but automation writes and execution require Max again. Monetary values use cents (e.g. 50000 = €500.00). See the CRM product guide for accounts, pipeline, and linked notes in the app.
/api/v1/crm/accountsList CRM accounts (companies) with pagination.
Query: limit, cursor. Pass nextCursor from the previous response.
Response (200)
{
"accounts": [
{
"id": "account_abc",
"companyName": "Acme Ltd",
"primaryEmail": "[email protected]",
"phone": "+353 1 234 5678",
"website": "https://acme.ie",
"status": "ACTIVE",
"healthScore": 80,
"lastInteractionAt": "2026-06-01T12:00:00.000Z",
"createdAt": "2026-01-15T09:00:00.000Z",
"updatedAt": "2026-06-10T14:00:00.000Z",
"contactsCount": 2,
"leadsCount": 1,
"dealsCount": 0
}
],
"limit": 100,
"nextCursor": null
}/api/v1/crm/accounts/:idGet an account by ID, including its contacts.
Response (200)
{
"id": "account_abc",
"companyName": "Acme Ltd",
"primaryEmail": "[email protected]",
"phone": "+353 1 234 5678",
"website": "https://acme.ie",
"billingAddress": null,
"accountNotes": null,
"status": "ACTIVE",
"healthScore": 80,
"lastInteractionAt": "2026-06-01T12:00:00.000Z",
"createdAt": "2026-01-15T09:00:00.000Z",
"updatedAt": "2026-06-10T14:00:00.000Z",
"contacts": [
{
"id": "contact_xyz",
"name": "Jane Smith",
"email": "[email protected]",
"jobTitle": "Director",
"isPrimary": true
}
]
}/api/v1/crm/accountsCreate a CRM account.
{
"companyName": "Acme Ltd",
"primaryEmail": "[email protected]",
"phone": "+353 1 234 5678",
"website": "https://acme.ie"
}Response (201)
{
"account": {
"id": "account_new",
"companyName": "Acme Ltd",
"createdAt": "2026-06-11T10:00:00.000Z"
}
}/api/v1/crm/contactsList contacts with optional account filter.
Query: clientId (account ID), limit, cursor. Pass nextCursor from the previous response.
Response (200)
{
"contacts": [
{
"id": "contact_xyz",
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+353 87 123 4567",
"jobTitle": "Director",
"clientId": "account_abc",
"isPrimary": true,
"createdAt": "2026-02-01T09:00:00.000Z",
"updatedAt": "2026-06-05T11:00:00.000Z"
}
],
"limit": 100,
"nextCursor": null
}/api/v1/crm/contactsCreate a contact. Optionally link to an account via clientId.
{
"name": "Jane Smith",
"clientId": "account_abc",
"email": "[email protected]",
"phone": "+353 87 123 4567",
"jobTitle": "Director"
}Response (201)
{
"contact": {
"id": "contact_new",
"name": "Jane Smith",
"clientId": "account_abc",
"createdAt": "2026-06-11T10:00:00.000Z"
}
}/api/v1/crm/stagesList lead and deal pipeline stages. Call before creating or moving leads/deals to discover stage IDs.
{
"leadStages": [
{ "id": "stage_1", "name": "New", "colour": "#14b8a6", "position": 0, "isDefault": true }
],
"dealStages": [
{ "id": "stage_2", "name": "Proposal", "colour": "#0d9488", "position": 0, "isDefault": true }
]
}/api/v1/crm/leadsList leads with optional filters.
Query: stageId, accountId, status (OPEN, QUALIFIED, LOST), limit, cursor. Pass nextCursor from the previous response.
Response (200)
{
"leads": [
{
"id": "lead_abc",
"title": "Website enquiry",
"status": "OPEN",
"stageId": "stage_1",
"accountId": "account_abc",
"contactId": "contact_xyz",
"source": "Website",
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-10T14:00:00.000Z"
}
],
"limit": 100,
"nextCursor": null
}/api/v1/crm/leadsCreate a lead. Omit stageId to use the default lead stage.
{
"title": "Website enquiry",
"description": "Interested in Pro plan",
"source": "Website",
"stageId": "stage_1",
"accountId": "account_abc",
"contactId": "contact_xyz"
}When both accountId and contactId are sent, the contact must belong to that account.
Response (201)
{
"lead": {
"id": "lead_new",
"title": "Website enquiry",
"stageId": "stage_1",
"createdAt": "2026-06-11T10:00:00.000Z"
}
}/api/v1/crm/leads/:idUpdate a lead. Send only fields to change; at least one field required. Status: OPEN, QUALIFIED, LOST.
Response (200)
{
"id": "lead_abc",
"title": "Qualified enquiry",
"status": "QUALIFIED",
"stageId": "stage_2",
"accountId": "account_abc",
"contactId": "contact_xyz",
"source": "Website",
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-11T10:30:00.000Z"
}/api/v1/crm/dealsList deals with optional filters.
Query: stageId, accountId, status (OPEN, WON, LOST), limit, cursor. Pass nextCursor from the previous response.
Response (200)
{
"deals": [
{
"id": "deal_abc",
"title": "Annual support contract",
"status": "OPEN",
"stageId": "stage_2",
"accountId": "account_abc",
"contactId": "contact_xyz",
"leadId": "lead_abc",
"valueCents": 1200000,
"closeDate": "2026-09-30T00:00:00.000Z",
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-10T14:00:00.000Z"
}
],
"limit": 100,
"nextCursor": null
}/api/v1/crm/dealsCreate a deal. Omit stageId to use the default deal stage.
{
"title": "Annual support contract",
"description": "12-month renewal",
"stageId": "stage_2",
"accountId": "account_abc",
"contactId": "contact_xyz",
"leadId": "lead_abc",
"valueCents": 1200000,
"closeDate": "2026-09-30"
}Response (201)
{
"deal": {
"id": "deal_new",
"title": "Annual support contract",
"stageId": "stage_2",
"createdAt": "2026-06-11T10:00:00.000Z"
}
}/api/v1/crm/deals/:idUpdate a deal. Send only fields to change; at least one field required. Status: OPEN, WON, LOST. Pass closeDate: null to clear the close date.
Response (200)
{
"id": "deal_abc",
"title": "Annual support contract",
"status": "WON",
"stageId": "stage_3",
"accountId": "account_abc",
"contactId": "contact_xyz",
"leadId": "lead_abc",
"valueCents": 1200000,
"closeDate": "2026-09-30T00:00:00.000Z",
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-11T10:30:00.000Z"
}Time
Time entries for the API key owner on the team associated with the key. List and read require time:read; create, start, and stop require time:write. The key must have an associated creator user (createdByUserId). Durations are in minutes. See the Time product guide for timers, timesheets, and imports in the app.
/api/v1/time-entriesList recent time entries for the API key owner. Requires time:read.
Query: limit (default 100, max 500), offset.
Response (200)
{
"entries": [
{
"id": "time_abc",
"userId": "user_1",
"teamId": "team_xyz",
"cardId": "card_1",
"cardTitle": "Ship API docs",
"description": "Writing endpoint docs",
"durationMinutes": 45,
"startTime": "2026-07-14T09:00:00.000Z",
"endTime": "2026-07-14T09:45:00.000Z",
"isBillable": true,
"source": "MANUAL",
"links": [
{
"targetId": "card_1",
"targetType": "CARD",
"targetTeamId": null,
"isPrimary": true
}
],
"tagNames": ["docs"],
"tags": [{ "name": "docs", "color": "#10B981" }]
}
],
"limit": 100,
"offset": 0
}/api/v1/time-entries/:idGet a single time entry owned by the API key user. Requires time:read.
Response (200)
{
"entry": {
"id": "time_abc",
"userId": "user_1",
"teamId": "team_xyz",
"cardId": null,
"cardTitle": null,
"description": "Client call",
"durationMinutes": 30,
"startTime": "2026-07-14T10:00:00.000Z",
"endTime": "2026-07-14T10:30:00.000Z",
"isBillable": true,
"source": "MANUAL",
"links": [],
"tagNames": [],
"tags": []
}
}/api/v1/time-entriesCreate a completed (manual) time entry. Provide either startTime and endTime, or durationMinutes (counted backwards from now). Requires time:write.
Request body
{
"description": "Design review",
"startTime": "2026-07-14T11:00:00.000Z",
"endTime": "2026-07-14T12:00:00.000Z",
"isBillable": true,
"cardId": "card_1",
"tagNames": ["design"],
"links": [
{
"targetId": "card_1",
"targetType": "CARD",
"isPrimary": true
}
]
}Optional fields: durationMinutes, source (MANUAL or IMPORT), isBillable, cardId, tagNames, links.
Response (201)
{
"entry": {
"id": "time_new",
"userId": "user_1",
"teamId": "team_xyz",
"description": "Design review",
"durationMinutes": 60,
"startTime": "2026-07-14T11:00:00.000Z",
"endTime": "2026-07-14T12:00:00.000Z",
"source": "MANUAL",
"links": [],
"tagNames": ["design"],
"tags": [{ "name": "design", "color": "#F59E0B" }]
}
}/api/v1/time-entries/startStart a running timer for the API key owner. Only one active timer is allowed at a time. Requires time:write.
Request body
{
"description": "Pairing session",
"cardId": "card_1",
"isBillable": true,
"tagNames": ["pairing"]
}All fields are optional. Empty body starts an untitled timer.
Response (201)
{
"entry": {
"id": "time_running",
"userId": "user_1",
"teamId": "team_xyz",
"description": "Pairing session",
"durationMinutes": null,
"startTime": "2026-07-14T13:00:00.000Z",
"endTime": null,
"source": "TIMER",
"links": [],
"tagNames": ["pairing"],
"tags": [{ "name": "pairing", "color": "#3B82F6" }]
}
}/api/v1/time-entries/stopStop the active timer (or a specific running entry via timeEntryId). Requires time:write.
Request body
{
"description": "Optional final description",
"timeEntryId": "time_running",
"roundingMinutes": 15
}Optional: cardId, description, timeEntryId, roundingMinutes (0, 6, or 15).
Response (200)
{
"entry": {
"id": "time_running",
"durationMinutes": 48,
"startTime": "2026-07-14T13:00:00.000Z",
"endTime": "2026-07-14T13:48:00.000Z",
"source": "TIMER"
}
}Expenses
Expense claims visible to the API key owner. Reads require expenses:read; creation, edits, workflow decisions, reimbursement, deletion, and invoice attachment require expenses:write. The key must have an associated team member. Role, plan, board-admin review policy, receipt rules, and record visibility are enforced exactly as they are in the app. See the Expenses product guide.
/api/v1/expensesList visible expenses. Filter with scope ( all, mine, approvals, or reimbursements), status, clientId, boardId, cardId, query, and limit.
{
"expenses": [{
"id": "expense_abc",
"title": "Train to customer workshop",
"amountCents": "4250",
"quantity": 1,
"totalCents": "4250",
"currency": "EUR",
"status": "SUBMITTED",
"reimbursable": true,
"isBillableToClient": true,
"claimant": { "id": "user_1", "name": "Jane" },
"client": { "id": "client_1", "name": "Acme Ltd" },
"board": { "id": "board_1", "name": "Acme delivery" },
"card": { "id": "card_1", "title": "On-site workshop" }
}],
"summary": { "totalCount": 1, "awaitingApprovalCount": 1 },
"context": { "role": "ADMIN", "canCreate": true, "canReviewAny": true }
}/api/v1/expensesCreate a draft or submit immediately. Money is an integer cent string. Mutations require an Idempotency-Key header.
{
"title": "Train to customer workshop",
"amountCents": "4250",
"quantity": 1,
"currency": "EUR",
"category": "TRAVEL",
"incurredAt": "2026-08-09T12:00:00.000Z",
"clientId": "client_1",
"boardId": "board_1",
"cardId": "card_1",
"reimbursable": true,
"isBillableToClient": true,
"receiptFileId": "file_1",
"submit": true
}GET /api/v1/expenses/:id
Return one visible expense with receipt metadata, review details, invoice source, capabilities, and complete activity history.
PATCH /api/v1/expenses/:id
Replace editable draft or returned-claim fields using the same body as create. Claimants can edit their own editable claims; admins can reassign the claimant and linked entities.
POST /api/v1/expenses/:id/transition
Send action as submit, approve, reject, pay, or reopen. Reject requires a note. Pay accepts an optional paymentReference. Invalid status changes return a fixed workflow error.
POST /api/v1/expenses/:id/invoice
Send { "invoiceId": "inv_1" } to add an approved or paid billable expense to a matching draft invoice. Duplicate billing, client mismatch, and currency mismatch are rejected.
Invoices
Team invoices for the API key's team. List and read require invoices:read; create, update, void, and send require invoices:write. Money amounts are integer cent strings (for example "5000" for 50.00 in the invoice currency). Supported currencies: EUR, USD, GBP. Send requires the API key owner to be a team owner or admin. See the Invoicing product guide for composing, branding, and payments in the app. MCP tools: list_invoices, get_invoice, create_invoice, send_invoice.
/api/v1/invoicesList invoices with cursor pagination. Requires invoices:read.
Query: limit (default 50, max 100), cursor, clientId, status (DRAFT, SENT, VIEWED, PAID, OVERDUE, VOID).
Response (200)
{
"invoices": [
{
"id": "inv_abc",
"number": "INV-2026-0001",
"status": "SENT",
"clientId": "client_1",
"clientName": "Acme Ltd",
"currency": "EUR",
"totalCents": "12300",
"issueDate": "2026-07-15T10:00:00.000Z",
"dueDate": "2026-07-29T00:00:00.000Z",
"updatedAt": "2026-07-15T10:05:00.000Z"
}
],
"limit": 50,
"cursor": null
}/api/v1/invoicesCreate a draft invoice for a CRM client, optionally with manual lines. Requires invoices:write. The key must have an associated creator user.
Request body
{
"clientId": "client_1",
"currency": "EUR",
"lines": [
{
"description": "Design review",
"quantity": 2,
"unitCents": "7500",
"vatRatePercent": 23
}
]
}Response (201)
{
"invoice": {
"id": "inv_abc",
"status": "DRAFT",
"number": null,
"clientId": "client_1",
"currency": "EUR",
"subtotalCents": "15000",
"taxCents": "3450",
"totalCents": "18450",
"templateKey": "classic",
"payUrl": "https://saku.ie/i/team_xyz/tok_…"
},
"lines": [
{
"id": "line_1",
"kind": "MANUAL",
"description": "Design review",
"quantity": "2",
"unitCents": "7500",
"amountCents": "15000",
"vatRatePercent": "23",
"position": 0
}
]
}/api/v1/invoices/:idGet one invoice with lines, payments, activity, client summary, and payUrl. Requires invoices:read.
Response (200)
{
"invoice": {
"id": "inv_abc",
"status": "SENT",
"number": "INV-2026-0001",
"currency": "EUR",
"totalCents": "18450",
"amountPaidCents": "0",
"payUrl": "https://saku.ie/i/team_xyz/tok_…"
},
"lines": [],
"payments": [],
"activity": [],
"client": {
"id": "client_1",
"companyName": "Acme Ltd"
}
}/api/v1/invoices/:idUpdate draft invoice metadata and/or replace manual lines. Requires invoices:write. Only drafts can be edited.
Request body
{
"notes": "Net 14",
"paymentTermsDays": 14,
"dueDate": "2026-07-29T00:00:00.000Z",
"discountCents": "1000",
"discountPercent": null,
"templateKey": "classic",
"defaultVatRatePercent": 23,
"lines": [
{
"description": "Retainer",
"quantity": 1,
"unitCents": "50000",
"vatRatePercent": 23
}
]
}templateKey: classic, compact, or bold. Sending lines replaces all manual lines on the draft.
/api/v1/invoices/:idVoid an invoice (does not hard-delete). The invoice number stays reserved. Requires invoices:write.
Response (200)
{
"invoice": {
"id": "inv_abc",
"status": "VOID",
"number": "INV-2026-0001",
"payUrl": "https://saku.ie/i/team_xyz/tok_…"
}
}/api/v1/invoices/:id/sendIssue (if still a draft) and email the invoice with PDF attachment. Requires invoices:write. The API key owner must be a team owner or admin. Free teams are limited to 5 sends per calendar month (HTTP 402 when the quota is reached).
Request body (optional)
{
"recipientEmails": ["[email protected]"]
}Omit the body or recipientEmails to use the client's primary email.
Response (200)
{
"invoice": {
"id": "inv_abc",
"status": "SENT",
"number": "INV-2026-0001",
"sentAt": "2026-07-15T10:05:00.000Z",
"payUrl": "https://saku.ie/i/team_xyz/tok_…"
}
}API access is available on the Max plan Max. Upgrade from pricing and billing. For AI agent integrations, see the MCP server docs.