InvoLoops Developer Docs
Build on the InvoLoops API
Issue legal invoices, manage customers and templates, and listen for lifecycle events — all via a single bearer-authenticated REST surface.
Bearer-key auth
One header. Per-key scopes, live/test modes, instant revoke.
Idempotent writes
Idempotency-Key on every POST. Safe to retry under load.
Compliance built in
Gapless numbering, signed audit log, Spec-04 immutability — all enforced server-side.
Webhooks
HMAC-signed deliveries for every invoice + extraction lifecycle event.
#Quickstart
Generate an API key in Settings → API keys. Pick Test mode for development; switch to Livewhen you're ready to issue real invoices.
curl https://involoops.com/api/v1/customers \
-H "Authorization: Bearer inv_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01HX5XYZ-create-acme-001" \
-d '{
"name": "Acme Ltd",
"email": "[email protected]",
"tax_id": "514999999"
}'The gapless document number is allocated immediately, so the response includes formatted_number like 2026-TI-000001.
curl https://involoops.com/api/v1/outbound-invoices \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"document_type": "TAX_INVOICE",
"customer_id": "<customer-uuid>",
"invoice_date": "2026-04-20",
"items": [
{ "description": "Consulting — April",
"quantity": 10, "unitPrice": 450, "total": 4500 }
],
"subtotal": "4500.00",
"vat_rate": "18",
"vat_amount": "810.00",
"total": "5310.00",
"currency": "ILS"
}'The PDF is generated, the canonical JSON + PDF are archived to immutable storage, and (where applicable) the invoice is submitted to ITA / SHAAM for allocation.
curl -X POST \
https://involoops.com/api/v1/outbound-invoices/<id>/issue \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Idempotency-Key: $(uuidgen)"Use POST/v1/outbound-invoices/{id}/send — the recipient gets a 7-day signed PDF download link; status transitions to sent. Then mark it paid with POST/v1/outbound-invoices/{id}/mark-paid, or fetch the signed PDF URL with GET/v1/outbound-invoices/{id}/pdf.
if you need to reverse (fully or partially): POST/v1/outbound-invoices/{id}/credit-note with { amount, reason }. Allocates a fresh CREDIT_INVOICE sequence; voids the original when remaining reaches zero.
#Authentication
Every call requires Authorization: Bearer <api-key>. Keys carry one of two prefixes:
inv_live_…— production. Mutates real records, sends real email, calls SHAAM live.inv_test_…— sandbox. Side effects are skipped or mocked.
Each key is scoped. Routes refuse anything not in the key's scope set with 403 missing_scope. Available scopes are listed under AvailableScope in the reference.
#MCP (AI clients)
InvoLoops also exposes a Model Context Protocol endpoint at /api/mcp. Use the same bearer API key; tools enforce the same scopes as the REST routes.
{
"mcpServers": {
"involoops": {
"url": "https://involoops.com/api/mcp",
"headers": {
"Authorization": "Bearer inv_live_…"
}
}
}
}Tools cover business status / expense analytics (invoices:read), customers, templates, vendors (list), and the outbound invoice lifecycle (draft → update draft → issue → send / mark-paid / void / delete draft / credit note / PDF URL). Full catalog: docs/api/10-mcp.md in the repo.
#Errors (RFC 7807)
Errors are Problem Details objects served as application/problem+json:
{
"type": "https://involoops.com/developers#errors",
"title": "Request body failed validation",
"status": 422,
"code": "validation_failed",
"request_id": "01HX5...",
"errors": [
{ "path": "items.0.quantity", "message": "must be > 0" }
]
}Always log request_id when you contact support — one line in our logs covers the full request.
#Idempotency
Every state-creating POST requires an Idempotency-Key header (8–255 chars, [A-Za-z0-9_-] — UUID or ULID is fine). Replays within 24h with the same body return the original response verbatim. Reusing a key with a different body returns 409 idempotency_replay_mismatch— that's a bug in your code, not ours.
#Pagination
List endpoints return cursor-paginated envelopes:
{
"data": [ ... ],
"page": {
"next_cursor": "eyJrIjoiLi4uIn0",
"has_more": true,
"limit": 50
}
}Default limit is 50, max 200. Pass ?cursor=<next_cursor>to fetch the next page. Cursors are opaque — don't parse them.
#Rate limits
Each key has its own bucket: 60 requests/sec (burst) and 1,000 requests/minute (sustained). Rejected calls return 429 rate_limited with a Retry-After header. Honor it — retrying tighter only prolongs the freeze.
Watch the response headers: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Burst-Remaining.
#Uploads (inbound extraction)
Offload PDF / image intake to InvoLoops. Three steps: ask for a presigned URL, PUT the bytes directly to S3, then tell us to extract. Asynchronous — the extraction runs in a background worker; subscribe to invoice.processed to get the result.
# 1. Get a presigned upload URL + upload_id.
curl https://involoops.com/api/v1/uploads \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"file_name": "vendor-receipt.pdf",
"content_type": "application/pdf",
"size_bytes": 182341
}'
# → { "upload_id": "...", "upload": { "url": "https://s3...", "method": "PUT", ... } }
# 2. PUT the bytes to the S3 presigned URL. No proxy through our API —
# your bandwidth, their CORS.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @vendor-receipt.pdf
# 3. Confirm + enqueue extraction.
curl -X POST https://involoops.com/api/v1/uploads/$UPLOAD_ID/process \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'
# → 202 Accepted, status: "queued"When the worker finishes, a webhook fires:
{
"id": "evt_01HX...",
"type": "invoice.processed",
"version": 1,
"created_at": "2026-04-21T10:00:00+03:00",
"data": {
"upload_id": "<upload-uuid>",
"invoice_id": "<invoice-uuid>",
"status": "done",
"auto_approved": false,
"confidence": 94
}
}Or, if you'd rather poll, hit GET/v1/uploads/{id} until status is done or error. On terminal failure (duplicate file, unreadable PDF, LLM upstream outage) you'll also receive an invoice.processing_failed webhook.
Limits: 50 MB per file, 60-page ceiling on PDFs, and the same monthly quota as the web app (plan-tier gated).
#Webhooks
Subscribe to lifecycle events to stop polling. Register an endpoint with POST/v1/webhook-endpoints — the response includes a plaintext secret shown exactly once. Store it; every delivery is signed with it.
curl https://involoops.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://example.com/hooks/involoops",
"description": "Finance ops notifier",
"events": [
"outbound_invoice.issued",
"outbound_invoice.paid",
"outbound_invoice.voided"
]
}'Every delivery carries:
POST /hooks/involoops HTTP/1.1
User-Agent: InvoLoops-Webhook/1.0
Content-Type: application/json; charset=utf-8
X-Involoops-Signature: t=1745151600,v1=<hex hmac-sha256>
X-Involoops-Event-Id: evt_01HX...
X-Involoops-Event-Type: outbound_invoice.issued
X-Involoops-Delivery: <delivery-uuid>
X-Involoops-Attempt: 1Verify before trusting. HMAC-SHA-256 input is `${t}.${raw_body}` keyed by your plaintext secret:
import crypto from "node:crypto";
export function verifyInvoloopsSignature(headers, rawBody, secret) {
const sig = headers["x-involoops-signature"] ?? "";
const parts = Object.fromEntries(
sig.split(",").map(p => p.split("=").map(s => s.trim())),
);
const t = Number(parts.t);
const sent = parts.v1;
if (!t || !sent) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-min skew window
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
if (expected.length !== sent.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sent, "hex"),
);
}Respond with any 2xx to acknowledge. Non-2xx (or a ≥10s timeout) triggers retries at 30 s, 2 m, 10 m, 30 m, 2 h, 6 h, 12 h, 24 h — then the delivery is dead-lettered. After 50 consecutive failures the endpoint auto-disables; re-enable via PATCH/v1/webhook-endpoints/{id} with { "active": true }.
Event types currently published: outbound_invoice.{created,issued,sent,paid,voided}, customer.{created,updated,deleted}, invoice.{processed,processing_failed}. The event schema lives in the reference as WebhookEventEnvelope.
Testing your receiver
Fire a synthetic delivery at any of your endpoints with POST/v1/webhook-endpoints/{id}/test — the response returns the receiver's status, body snippet, and any error, in-band, so you can iterate without issuing real invoices. Test deliveries carry X-Involoops-Test: true and never count toward the endpoint's auto-disable counter.
curl -X POST \
https://involoops.com/api/v1/webhook-endpoints/$ENDPOINT_ID/test \
-H "Authorization: Bearer $INVOLOOPS_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# → { "ok": true, "outcome": "delivered",
# "response_status": 200, "response_snippet": "…" }#Versioning
URL-pathed: /api/v1/.... New optional fields ship within v1 without notice. Breaking changes mean v2 — and v1 stays online for ≥12 months after v2 GA, with Deprecation and Sunset response headers.
Ready to try requests live? Open the interactive API reference or download the raw OpenAPI 3.1 spec.