InvoiceMate Developers

InvoiceMate API

Multi-tenant invoicing and accounting API for the Swedish market. Every documented endpoint below is generated from the canonical route inventory and continuously drift-checked against the implementation.

Getting started

Four calls take you from zero to a sandboxed API request. All code samples on this page match the tested request shapes — copy them verbatim.

1. Register an account

curl -s https://api.invoicemate.se/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"dev@example.com","password":"lösenord-9abc","organization_name":"Dev Org AB"}'
# → {"token":"<JWT>","organization_id":…,
#    "user":{"user_id":…,"organization_id":…,"role":"admin","email":…}}

2. Log in

curl -s https://api.invoicemate.se/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"dev@example.com","password":"lösenord-9abc"}'
# → {"token":"<JWT>","user":{…}}
# If the account has 2FA enabled the response is
# {"requires_2fa":true,"temp_token":"…"} — complete it via
# POST /auth/2fa/login-verify {"temp_token","code"}.

3. Create an isolated sandbox organisation

curl -s https://api.invoicemate.se/v1/organizations \
  -H "Authorization: Bearer <JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"name":"My API Sandbox","purpose":"sandbox"}'
# → {"organization_id": …}  (separate tenant database)

4. Switch context and call the API

curl -s https://api.invoicemate.se/v1/user/switch-org \
  -H "Authorization: Bearer <JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"organization_id":<sandbox_org_id>}'
# → {"token":"<NEW JWT scoped to the sandbox org>"}

curl -s https://api.invoicemate.se/v1/<sandbox_org_id>/contacts \
  -H "Authorization: Bearer <SANDBOX_JWT>"

Authentication

API calls authenticate with a JWT in the Authorization: Bearer header. Tokens are valid for 24 hours. The token carries user_id, organization_id and role; the org in the token must match the {organizationId} path segment or the request is rejected with 403 — this is the tenant-isolation boundary.

RolePermissions
adminFull read/write within the tenant, including billing and webhooks.
accountantRead/write for bookkeeping surfaces.
viewer / userRead-only.

Exact per-route role gates are published in docs/tenant-auth-matrix.json (generated). Logout is server-side — POST /logout revokes every JWT issued before the call.

Sandbox

Create any number of throwaway organisations with purpose: "sandbox". Each organisation — production or sandbox — lives in its own fully isolated tenant database, so a sandbox can never see or modify live tenant data. Sandbox orgs:

There is no shared seed dataset — you create the fixtures you need inside your own sandbox tenant.

Versioning & deprecation

Endpoints are versioned under /v{N}/…. You may pin a version via the Accept-Version request header or the ?api-version= query parameter; without either, the latest supported version answers.

Errors

Every error is a JSON object {"error": "…"} — never stack traces or SQL.

StatusMeaning
400Validation or input error
401Missing, invalid or expired JWT
403Insufficient role, or token org does not match the path
404Resource not found in this tenant
406Unsupported API version requested
409Conflict (e.g. concurrent modification)
422Semantic error in a well-formed request
429Rate limit exceeded
503Required external dependency not configured / unavailable

Rate limits

Rate limiting is enforced per route class. Default policy:

SurfaceLimit
Auth endpoints (/login, /register, password reset, 2FA)10 req / 15 min
Inbound webhook receivers (/v1/webhooks/*)120 req / min
AI endpoints (assistant, categorisation, invoice generation)60 req / min
Organisation provisioning (POST /v1/organizations)30 req / hour
Public payment links (/v1/{org}/payment-links/*)30 req / min
Outbound email send surfaces120 req / min (10 req / min for emails/send-bulk)
Global default600 req / min

Per-deployment overrides exist (RATE_LIMIT_RULES); treat these numbers as the floor. Back off on 429.

Idempotency & pagination

Mutations support opt-in idempotency: send an Idempotency-Key: <unique string> header (8–200 chars, [A-Za-z0-9_-], e.g. a UUID) on POST/PUT/PATCH/DELETE. A retry with the same key and identical body within 24 hours replays the stored response; the same key with a different body returns 409, as does a concurrent request still in flight. Keys are scoped per organisation + user + method + path. Financial mutations are transactional regardless.

List endpoints paginate with page and limit query parameters (limits are capped, e.g. 100 on contacts). The envelope varies by resource — contacts return {"items":[…],"count":n,"page":n,"pageSize":n}, invoices return {"data":[…],"pagination":{"page","pageSize","pages","total"}}. Check the exact shape per operation in the API reference.

Webhooks

Subscribe per organisation; deliveries are HTTPS POSTs signed with HMAC-SHA256. Only the events below exist — the full registry is introspectable at GET /v1/{org}/webhooks/events.

EventFires when
invoice.createdan invoice is created
invoice.updatedan invoice is updated
invoice.deletedan invoice is deleted
invoice.sentan invoice is sent to the customer
invoice.partial_createda partial invoice is created
payment.receiveda payment is registered on an invoice
contact.createda contact is created
contact.updateda contact is updated
contact.deleteda contact is deleted

Subscribe

curl -s https://api.invoicemate.se/v1/<org>/webhooks/subscribe \
  -H "Authorization: Bearer <JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"eventId":<id from /webhooks/events>,"uri":"https://your.host/hooks/im","name":"my hook"}'
# → {"id":…,"secret":"<64-hex signing secret — store it, it is shown once>"}

Subscription URLs must be public HTTPS — private/internal addresses are rejected (SSRF guard) at subscribe time and re-checked on every delivery and replay.

Delivery

Body is the envelope {"event","event_id","timestamp","organization_id","data"}. Headers:

HeaderValue
X-Webhook-Signaturesha256=<HMAC-SHA256 hex over raw body, subscription secret>
X-Webhook-TimestampUnix seconds
X-Event-Typee.g. invoice.created
X-Event-IdUnique delivery id — deduplicate on this
X-Webhook-Replay1 on operator/test replays only

Any 2xx acknowledges. 5xx and 429 are retried by the scheduled worker (exponential backoff); all other non-2xx are final for that event. Delivery history: GET /v1/{org}/webhooks/subscriptions/{id}/deliveries.

Verify the signature — PHP

$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$ok   = hash_equals('sha256=' . hash_hmac('sha256', $body, $secret), $sig);

Verify the signature — Node.js

const crypto = require('crypto');
const sig = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.headers['x-webhook-signature']));

Replay a delivery (sandbox-friendly)

curl -s -X POST \
  https://api.invoicemate.se/v1/<org>/webhooks/subscriptions/<subId>/deliveries/<deliveryId>/replay \
  -H "Authorization: Bearer <JWT>"
# → {"replay_delivery_id":…} — byte-identical body, same event_id,
#   fresh HMAC over the current secret. Use it to test your
#   signature check and your idempotency handling.

Code examples

cURL — create a contact

curl -s https://api.invoicemate.se/v1/<org>/contacts \
  -H "Authorization: Bearer <JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Konsult AB","se_number":"559123-4567","email":"faktura@konsult.se"}'

JavaScript (fetch)

const res = await fetch(`https://api.invoicemate.se/v1/${orgId}/contacts`, {
  headers: { Authorization: `Bearer ${token}` }
});
const contacts = await res.json();

PHP

$ch = curl_init("https://api.invoicemate.se/v1/{$orgId}/contacts");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],
]);
$contacts = json_decode(curl_exec($ch), true);

Python

import requests
r = requests.get(f"https://api.invoicemate.se/v1/{org_id}/contacts",
                 headers={"Authorization": f"Bearer {token}"})
contacts = r.json()

Changelog & deprecation policy

Support

Integration questions: support@invoicemate.se. Security reports: see security.txt (RFC 9116). Do not run volume/load tests against production; use a sandbox org.