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.

1
Create a customer.POST/v1/customers
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"
  }'
2
Create a draft invoice.POST/v1/outbound-invoices

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"
  }'
3
Issue (sign) the invoice.POST/v1/outbound-invoices/{id}/issue

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)"
4
Email it to the customer.

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.

5
Issue a credit note

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.

Identify the calling key with GET/v1/me — useful for sanity-checking which scopes a key actually carries before you roll it into production.

#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.

json
{
  "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:

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:

json
{
  "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:

json
{
  "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:

text
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: 1

Verify before trusting. HMAC-SHA-256 input is `${t}.${raw_body}` keyed by your plaintext secret:

javascript
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.