API and webhooks
Create scoped API keys, read clients and invoices over the REST API, and receive signed webhooks when invoices and clients change.
Tillage has a small REST API and outgoing webhooks for connecting your own tools: send paid invoices to your books, sync clients to a CRM, or create usage-based invoices from a billing system. API keys also work with the MCP server. Everything here is managed in Team Settings → Developers.
API keys
Create a key
- Go to Team Settings → Developers.
- Under API Keys, enter a name, such as the integration it’s for.
- Tick the scopes it needs, then click Create Key. Or use a preset:
- Create MCP Key selects read and write for clients, quotes, invoices and contracts.
- Create Gauge Key selects Read invoices and Write invoices.
- Copy the key. It starts with
tk_live_and is shown only once.
Keys belong to the team they were created in and can only see that team’s data. To stop a key working, click Revoke next to it.
Scopes
| Scope | Label | Grants |
|---|---|---|
clients:read |
Read clients | View clients and their contacts |
clients:write |
Write clients | Create and update clients |
quotes:read |
Read quotes | View quotes and line items |
quotes:write |
Write quotes | Create and update quotes |
invoices:read |
Read invoices | View invoices and payment status |
invoices:write |
Write invoices | Create and update invoices |
contracts:read |
Read contracts | View contracts and contract templates |
contracts:write |
Write contracts | Create, update and send contracts and templates |
webhooks:read |
Read webhooks | Reserved for webhook management |
webhooks:write |
Write webhooks | Reserved for webhook management |
Give each key only the scopes it needs.
REST API
Send the key as a bearer token:
curl https://app.tillage.ai/api/v1/invoices?limit=50 \
-H "Authorization: Bearer tk_live_..."
Endpoints
| Method and path | Scope | What it does |
|---|---|---|
GET /api/v1/clients |
clients:read |
List clients with their contacts |
GET /api/v1/invoices |
invoices:read |
List invoices with status, payment status, dates and billing schedule details |
POST /api/v1/invoices |
invoices:write |
Create a usage invoice |
Listing and paging
Both list endpoints accept:
| Parameter | Description |
|---|---|
limit |
Results per page, 1–100. Default 25. |
starting_after |
The ID to continue after. Use next_cursor from the previous page. |
updated_after |
Only records updated at or after this time. |
status |
Filter by status. For clients, defaults to active clients; pass all for every client, or a comma-separated list. |
payment_status |
Invoices only. Filter by payment status. |
Responses include data, has_more, next_cursor and a ready-made next_page_url. Keep requesting until has_more is false.
Each invoice also includes recurring_frequency, counts_toward_mrr and counts_toward_arr, so you can rebuild recurring revenue in your own tools.
Create a usage invoice
POST /api/v1/invoices is for billing systems that meter usage and need Tillage to invoice and collect it. Send JSON with:
| Field | Required | Notes |
|---|---|---|
team_id |
Yes | Must match the key’s team. |
client_id |
Yes | |
title |
Yes | |
type |
No | Must be usage if sent. |
amount, total_amount |
Yes | Non-negative numbers. |
due_date |
Yes | |
idempotency_key |
Yes | Sending the same key again returns the original invoice instead of creating a second one. |
status |
No | issued (default), draft or queued. |
auto_send |
No | Defaults to true. A draft sent with auto_send on becomes queued and is emailed on its issue date. |
auto_charge |
No | Defaults to true: the client’s saved payment method is charged when due, if their auto-pay consent isn’t Never. |
issue_date, description, line_items, tax_amount, payment_terms, source, source_data |
No | payment_terms defaults to “Due on receipt”. |
allow_credit_cards, allow_bank_transfers |
No | Payment methods allowed online. |
Rate limits
Each key can make 120 requests a minute. Responses include an X-RateLimit-Limit header. Over the limit, you’ll get a 429 with a Retry-After header.
Webhooks
Webhooks send an HTTPS POST to your server when something changes in Tillage.
Add an endpoint
- Go to Team Settings → Developers.
- Under Webhooks, enter your endpoint URL and click Add Endpoint.
- Copy the signing secret. It’s copied to your clipboard when the endpoint is created and starts with
whsec_. Store it now; you won’t see it again. - Click Test to send a
webhook.testevent.
The endpoint must use HTTPS, and private or local network addresses aren’t allowed. New endpoints receive every event below. The card shows whether each endpoint is enabled, its failure count, and recent deliveries.
Events
| Event | When |
|---|---|
invoice.created |
An invoice is created |
invoice.updated |
An invoice changes |
invoice.paid |
An invoice is paid |
invoice.payment_failed |
An online payment fails |
client.created |
A client is created |
client.updated |
A client changes |
contact.created |
A contact is added |
contact.updated |
A contact changes |
contact.deleted |
A contact is removed |
webhook.test |
You click Test |
Payload
{
"id": "evt_...",
"type": "invoice.paid",
"created": "2026-11-02T14:05:11.000Z",
"teamId": "...",
"clientId": "...",
"data": { }
}
data holds the record that changed. Use id to ignore an event you’ve already processed.
Verify the signature
Each request carries these headers:
| Header | Value |
|---|---|
X-Tillage-Event |
The event type |
X-Tillage-Event-Id |
The event ID |
X-Tillage-Timestamp |
Unix time in seconds when the request was signed |
X-Tillage-Signature |
Hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your signing secret |
To verify, compute the HMAC over the timestamp, a period and the raw request body, then compare it with the header:
import crypto from "node:crypto";
function isValidTillageWebhook(rawBody, headers, secret) {
const timestamp = headers["x-tillage-timestamp"];
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = headers["x-tillage-signature"] || "";
return (
expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
}
Use the raw body exactly as received, before any JSON parsing. Rejecting requests with an old timestamp protects you against replays.
Retries
Respond with any 2xx status within 10 seconds. Otherwise Tillage retries, up to 6 attempts in total, waiting 1 minute, 5 minutes, 30 minutes, 2 hours and then 12 hours between attempts. After 20 failures in a row, the endpoint is disabled.