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_KEY

There 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-v1

Use 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

MethodPathDescription
GET/meReturn the user + key associated with your token.
GET/invoicesList invoices. Supports ?limit, ?offset or ?cursor, ?status. Response includes next_cursor.
POST/invoicesCreate an invoice.
GET/invoices/:idFetch a single invoice by id.
PATCH/invoices/:idUpdate status, total, or data payload.
DELETE/invoices/:idDelete an invoice.
POST/invoices/:id/shareReturn {url}, a snapshot encoded in the link fragment.
POST/invoices/:id/pdfReturn {url, filename}; open the URL in a browser to generate the PDF.
POST/shareStateless: 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

CodeNameMeaning
200OKRequest succeeded.
201CreatedResource created (POST /invoices).
204No ContentDelete succeeded.
400Bad RequestValidation failed. See error body.
401UnauthorizedMissing, malformed, or revoked API key.
404Not FoundUnknown route or resource.
413Payload Too LargeDeclared Content-Length exceeded 262144 bytes; this is not a streaming size limit.
429Too Many RequestsRate limit exceeded. Retry after Retry-After seconds.
500Server ErrorAuthentication 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.