Developers

NORMAN DEVELOPERS

CRM, invoicing and reporting API examples

Adapt working Norman API requests for client creation, invoice drafts, receipt uploads, manual expenses and paginated transaction exports.

BUILD SOMETHING USEFUL

Recipes you can adapt

Start with these requests. Set the environment variables from the quickstart, replace IDs with real values, and choose dates and invoice numbers for your business.

1. CRM → client → invoice draft

Use write_clients to create a client. Save its returned publicId in your CRM so later runs can update that record.

curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/clients/" \
  -H "Authorization: Bearer $NORMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme Studio",
  "clientType": "business",
  "country": "DE",
  "email": "billing@example.com"
}'

Use write_invoices for the draft. Replace replace_with_client_uuid with the ID above. The example is for a EUR invoice with a 19% VAT line; adapt the VAT treatment to the company and sale.

curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/invoices/" \
  -H "Authorization: Bearer $NORMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "invoiceNumber": "API-DEMO-001",
  "status": "draft",
  "currency": "EUR",
  "issued": "2026-09-10",
  "serviceStartDate": "2026-09-10",
  "serviceEndDate": "2026-09-10",
  "dueTo": "2026-10-10",
  "isToSend": false,
  "skipBankDetails": true,
  "invoicedItems": [
    {
      "name": "Consulting",
      "quantity": 1,
      "rate": 15000,
      "vatRate": 19
    }
  ],
  "client": "replace_with_client_uuid"
}'

Creation returns HTTP 201 with the invoice ID and totals. isToSend: false keeps email sending out of this step. When ready, update the invoice to the appropriate saved state and use its /send/ operation with the required message fields. Sending requires verified email and an eligible plan.

2. Upload a receipt and read its result

Use write_documents to upload; read_documents lets you fetch the result. Let curl set the multipart boundary.

curl --fail-with-body -X POST "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/" \
  -H "Authorization: Bearer $NORMAN_API_KEY" \
  -F "file=@receipt.pdf"

# Save the returned publicId, then check the document.
curl --fail-with-body "https://api.norman.finance/api/v1/companies/$NORMAN_COMPANY_ID/attachments/replace_with_document_uuid/" \
  -H "Authorization: Bearer $NORMAN_API_KEY"

Upload success means the file was stored. Extraction runs asynchronously, so the initial metadata may be incomplete. Read the document again to inspect extracted fields; allow time between requests. A single file can be up to 10 MB; plan limits also apply. For a job with progress and transaction matching, use the batch OCR workflow.

3. Record a manual expense

Use write_transactions. Replace the company placeholder with the ID linked to the key. The amount below is €49.99.

curl --fail-with-body -X POST "https://api.norman.finance/api/v1/accounting/transactions/" \
  -H "Authorization: Bearer $NORMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "Office supplies",
  "amount": -4999,
  "currency": "EUR",
  "valueDate": "2026-09-10T12:00:00Z",
  "cashflowType": "EXPENSE",
  "company": "replace_with_company_uuid"
}'

4. Pull transactions into your reporting tool

Use read_transactions. This Node.js example reads every page and checks the next URL before sending the key.

const base = new URL("https://api.norman.finance/api/v1/");
const key = process.env.NORMAN_API_KEY;
if (!key) throw new Error("Set NORMAN_API_KEY first");
let next = new URL("accounting/transactions/?page_size=100", base).href;
const rows = [];
while (next) {
  const url = new URL(next, base);
  if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) {
    throw new Error("Unexpected pagination URL");
  }
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${key}` },
    signal: AbortSignal.timeout(30_000),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const page = await response.json();
  rows.push(...page.results);
  next = page.next;
}
console.log(rows); // Map these rows into your sheet or reporting database.