Invoice REST API documentation
By Invoodle · Reviewed
Programmatically create, update, and share invoices from your own backend. Ideal for syncing invoices from your CRM, e-commerce store, or accounting system.
The API requires an account, a deployed Supabase API function and an API key. It stores saved invoices in your account; the browser-local invoice editor remains available without a key. Keep keys on your backend, outside public frontend code.
Authentication
All requests must include a bearer token. Create keys in your dashboard. Keys look like invd_live_… and are shown only once.
Authorization: Bearer invd_live_YOUR_KEYThere is no separate test key today — every invd_live_ key writes to your real account. Use a separate test account for experiments. A dedicated key in your main account still has access to that account’s invoices; revoking it does not undo writes.
Base URL
https://pskmcpbbmvhczxzlazom.supabase.co/functions/v1/api-v1Use your deployment’s Supabase project URL followed by /functions/v1/api-v1. Replace YOUR_PROJECT and all key placeholders before running examples. First call GET /me to check authentication without creating an invoice. The private website preview does not confirm API availability.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /me | Return the user + key associated with your token. |
| GET | /invoices | List invoices. Supports ?limit, ?offset or ?cursor, ?status. Response includes next_cursor. |
| POST | /invoices | Create an invoice. |
| GET | /invoices/:id | Fetch a single invoice by id. |
| PATCH | /invoices/:id | Update status, total, or data payload. |
| DELETE | /invoices/:id | Delete an invoice. |
| POST | /invoices/:id/share | Return {url}, a snapshot encoded in the link fragment. |
| POST | /invoices/:id/pdf | Return {url, filename}; open the URL in a browser to generate the PDF. |
| POST | /share | Stateless: send invoice JSON, get share URL + QR PNG data URL. |
List invoices
curl "https://pskmcpbbmvhczxzlazom.supabase.co/functions/v1/api-v1/invoices?limit=25" \
-H "Authorization: Bearer invd_live_YOUR_KEY"Create an invoice
curl -X POST https://pskmcpbbmvhczxzlazom.supabase.co/functions/v1/api-v1/invoices \
-H "Authorization: Bearer invd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoice_number": "INV-1001",
"status": "sent",
"total": 1456.78,
"currency": "INR",
"data": {
"template": "classic",
"country": "IN",
"currencyKey": "INR",
"status": "sent",
"storeName": "Acme Studios",
"customerName": "Wayne Enterprises",
"invoiceNumber": "INV-1001",
"invoiceDate": "2026-01-15",
"items": [
{ "id": 1, "description": "Logo design", "quantity": 1, "unit": "job", "rate": 1234.56, "amount": 1234.56 }
],
"tax": { "type": "percentage", "value": 18 },
"discount": { "type": "none", "value": 0 },
"shipping": { "type": "none", "value": 0 }
}
}'Generate a share link + QR
curl -X POST https://pskmcpbbmvhczxzlazom.supabase.co/functions/v1/api-v1/share \
-H "Authorization: Bearer invd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"data": { "storeName": "Acme", "items": [{"description":"Logo","quantity":1,"rate":100,"amount":100}] }}'Invoice fields and totals
Use the nested data shape shown above. It needs at least one item. Keep top-level invoice_number, status and currency consistent with data.invoiceNumber, data.status and data.currencyKey. The API stores the supplied top-level total; it does not calculate it from line items. Without total, the saved summary defaults to zero.
The curl example has a 1,234.56 INR subtotal plus an illustrative 18% tax, rounded to a total of 1,456.78 INR. That rate is sample arithmetic, not a tax recommendation. Set each item’s amount to quantity × rate, then calculate tax, discount and shipping using the same rules as your invoice. The other language examples use USD with no tax adjustment.
PATCH accepts invoice_number, status, total, currency and data. It replaces the entire data value rather than merging nested fields, and it does not recalculate summary fields. Send a complete invoice payload when changing data. Set both status fields when both must change.
Share links and browser PDF downloads
These API share endpoints put a compressed invoice snapshot in the URL fragment. Anyone with the full link can read it. It is not encrypted, expiring or revocable through these endpoints; deleting a saved invoice does not invalidate an existing snapshot link. Avoid sensitive information and review the privacy policy before sharing.
The PDF endpoint returns JSON, not PDF bytes. Open its url in a browser that runs JavaScript to generate the download. The stateless /share endpoint does not save an invoice row, but still requires a key and uses rate limiting and usage logging. Its qr_png_base64 field contains a complete data:image/png;base64,... URL.
Pagination
GET /invoices accepts either ?offset= (simple) or ?cursor= (recommended for large lists, using created_at and id ordering). The response includes next_cursorwhen more rows are available; pass it as ?cursor= on the next call.
The default limit is 50 and the maximum is 200. Use positive integers, URL-encode the returned cursor and keep the same status filter on subsequent pages. Stop when next_cursor is null. Cursor responses omit count and offset.
GET /invoices?limit=50
{"data": [...], "next_cursor": "2026-07-03T12:34:56.789Z_ab12..."}
GET /invoices?limit=50&cursor=2026-07-03T12:34:56.789Z_ab12...Webhooks
Register endpoints in the webhooks dashboard. We POST signed JSON on invoice.created, invoice.updated, invoice.deleted. Verify with:
// Node.js verify
import crypto from "crypto";
const sig = req.headers["x-invoodle-signature"];
// rawBody must be the original request bytes, before JSON parsing.
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
if (typeof sig !== "string") throw new Error("missing signature");
const actualBytes = Buffer.from(sig);
const expectedBytes = Buffer.from(expected);
if (actualBytes.length !== expectedBytes.length ||
!crypto.timingSafeEqual(actualBytes, expectedBytes)) {
throw new Error("bad signature");
}The dispatcher allows four total attempts with a 10-second request timeout. After failures it records retry times of 1, 5 and 30 minutes; a separate scheduler must invoke pending deliveries. No retry scheduler is included in the current source. Return a 2xx response promptly, queue your work and deduplicate using X-Invoodle-Delivery. The signed body contains id, event, created_at and data; created_at is generated per attempt, so it is not a stable event timestamp.
Rate limits & audit
Each API key is limited to 60 requests per minute (sliding window). Over-limit requests return 429 Too Many Requests. Usage logging is best effort for authenticated requests; failed authentication is not included. Inspect activity — see the usage dashboard. The count-based limiter can allow concurrent bursts and allows requests if its usage store is unavailable.
Idempotency
POST /invoices is not idempotent — retrying the same request creates a new invoice each time. An invoice number is not a uniqueness constraint, and checking a list before writing does not prevent concurrent duplicates. Record the returned invoice id in your integration. If a create request times out, reconcile the account’s records before retrying; use your own durable operation tracking. The API does not support an Idempotency-Key header.
Status codes
| Code | Name | Meaning |
|---|---|---|
| 200 | OK | Request succeeded. |
| 201 | Created | Resource created (POST /invoices). |
| 204 | No Content | Delete succeeded. |
| 400 | Bad Request | Validation failed. See error body. |
| 401 | Unauthorized | Missing, malformed, or revoked API key. |
| 404 | Not Found | Unknown route or resource. |
| 413 | Payload Too Large | Declared Content-Length exceeded 262144 bytes; this is not a streaming size limit. |
| 429 | Too Many Requests | Rate limit exceeded. Retry after Retry-After seconds. |
| 500 | Server Error | Authentication lookup failed; other caught handler errors currently return 400. |
Error format
{ "error": "Invalid or revoked API key" }Standard HTTP codes: 200/201 on success, 204 on delete, 400 for validation, 401 for auth, 404 for missing resources, 500 for server errors.
