API and webhooks.
A small, exact REST API over the same permission checks the product uses, and signed webhooks for the moments that matter. Everything here exists today; what does not is listed at the end.
Authentication
A firm’s administrator creates a key under Setup → API and chooses its scopes. The key is shown once. Send it as a bearer token. A key can never do more than its scopes allow, and never more than an administrator could — permissions, modules and suspension apply to it exactly as they do to a person.
curl "https://veilux.io/api/v1/invoices?q=acme&sort=dueOn&dir=asc&pageSize=50" \
-H "Authorization: Bearer pk_live_…"- 401 — no key, a malformed key, or a key that has been revoked or has expired.
- 404 — the record does not exist, belongs to another firm, or the key may not see it; also when the firm does not have the API module. The three are deliberately indistinguishable.
- 429 — rate limited: 600 requests a minute per key, with
RateLimit-*andRetry-Afterheaders.
Retrying a write
Send an Idempotency-Key header with a create — any unique string up to 255 characters, a UUID is ideal. If the connection drops and you send the same request again with the same key, you get the first response back, marked Idempotent-Replayed: true, and nothing is created twice. Keys are kept for 24 hours and belong to your firm alone.
- 409 — the first request with this key is still running. Wait a second and retry.
- 422 — this key was already used with a different request. Use a new key for a new request.
- A 5xx is never remembered, so a retry after a fault on our side does the work.
Lists
Every list endpoint takes the same parameters as the list screens in the product and returns { rows, total, page, pageSize, pageCount }.
- q
- Search text, over the fields that list searches.
- page
- From 1.
- pageSize
- Default 25, at most 200.
- sort, dir
- A sortable field, and asc or desc.
- <field>=<value>
- A filter the list offers, such as companyId=… on invoices. A field the list does not offer is ignored, never passed to the database.
Endpoints
- GET/api/v1/meany key
- The key itself: its name, its scopes and how much of the rate limit is left. Use it to check a key works.
- GET/api/v1/customerscustomer:read
- Customers (companies), paged and searchable.
- GET/api/v1/customers/{id}customer:read
- One customer.
- POST/api/v1/customerscustomer:create
- Create a customer.
- PATCH/api/v1/customers/{id}customer:update
- Update a customer.
- GET/api/v1/contactscustomer:read
- Contacts — the people inside customers.
- POST/api/v1/contactscustomer:update
- Add a contact to a customer.
- PATCH/api/v1/contacts/{id}customer:update
- Update a contact.
- GET/api/v1/invoicesinvoice:read
- Invoices with their derived totals and balance.
- POST/api/v1/invoicesinvoice:write
- Create a draft invoice from line items. The amount is computed from the lines; it is never accepted from the request.
- GET/api/v1/invoices/{id}invoice:read
- One invoice, with the same derived totals and balance as the list.
- PATCH/api/v1/invoices/{id}invoice:write
- Edit a DRAFT invoice — a sent one answers 409, naming why. Credit a sent invoice and raise a new one instead.
- POST/api/v1/invoices/{id}/sendinvoice:write
- Move a DRAFT invoice to SENT and email it to the customer, exactly as the “Send” button does.
- POST/api/v1/invoices/{id}/paymentspayment:record
- Record a payment against a SENT invoice.
- GET/api/v1/projectsthe key’s project scopes
- Projects the key may see — all of them, or only its own, the same split a person with those permissions gets.
- POST/api/v1/projectsproject:write
- Create a project.
- PATCH/api/v1/projects/{id}project:write
- Edit a project.
- GET/api/v1/ticketsthe key’s ticket scopes
- Support tickets the key may see, scoped the way the inbox is.
- POST/api/v1/ticketsticket:reply
- Raise a support ticket.
- PATCH/api/v1/tickets/{id}ticket:reply
- Edit a ticket: subject, department, priority, assignee, and status.
- POST/api/v1/tickets/{id}/repliesticket:reply
- Reply to a ticket, or leave an internal note.
- GET/api/v1/tasksthe key’s project scopes
- Tasks the key may see, the same scope split as projects.
- POST/api/v1/taskstask:write
- Create a task.
- PATCH/api/v1/tasks/{id}task:write
- Edit a task.
- POST/api/v1/timetime:log
- Log time by hand against a task or a project.
- GET/api/v1/estimatesestimate:read
- Estimates with their derived totals.
- POST/api/v1/estimatesestimate:write
- Create a draft estimate from line items.
- GET/api/v1/estimates/{id}estimate:read
- One estimate, with the same derived totals as the list.
- POST/api/v1/estimates/{id}/sendestimate:write
- Move a DRAFT estimate to SENT and email it to the customer.
Webhooks
Add an endpoint under Setup → Webhooks and choose its events. Each delivery is a JSON POST, retried with backoff until your endpoint answers 2xx, and signed so you can prove it came from us.
invoice.createdInvoice createdinvoice.sentInvoice sentinvoice.paidInvoice paid in fullpayment.recordedPayment recordedestimate.acceptedEstimate acceptedcontract.signedContract signedlead.createdLead createdticket.createdTicket createdproject.status_changedProject status changedcontact.createdContact createdmeeting.bookedMeeting bookedmeeting.cancelledMeeting cancelled
Verifying a delivery
Each request carries X-Portal-Webhook-Timestamp and X-Portal-Webhook-Signature: an HMAC-SHA256 of the timestamp and the raw body, joined by a dot, with your endpoint’s secret. Refuse anything older than five minutes, then compare in constant time. Verify the raw bytes, before any JSON parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_MS = 5 * 60 * 1000;
/** True only for a delivery Veilux signed, within five minutes. */
export function verifyNorweftWebhook(rawBody: string, headers: Headers, secret: string): boolean {
const timestamp = headers.get('x-portal-webhook-timestamp') ?? '';
const received = headers.get('x-portal-webhook-signature') ?? '';
const sentAt = Number(timestamp) * 1000;
if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) >= TOLERANCE_MS) return false;
const expected =
'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && timingSafeEqual(a, b);
}MCP, for AI assistants
The same key works as an MCP server at POST /api/mcp — one tool per endpoint above, calling the identical code, checked against the identical scopes. Point an MCP client (Claude Desktop, or any client that speaks “Streamable HTTP”) at the URL with an Authorization: Bearer pk_live_… header; there is no separate key type. The full connection guide and security model are in docs/MCP.md.
- get_me
- The calling key itself: its name and scopes. Mirrors GET /api/v1/me.
- list_customerscustomer:read
- List customers (companies), paged and searchable. Mirrors GET /api/v1/customers.
- get_customercustomer:read
- One customer by id. Mirrors GET /api/v1/customers/{id}.
- list_contactscustomer:read
- List contacts — the people inside customers. Mirrors GET /api/v1/contacts.
- list_invoicesinvoice:read
- List invoices with their derived totals and balance. Mirrors GET /api/v1/invoices.
- get_invoiceinvoice:read
- One invoice, with the same derived totals and balance as the list. Mirrors GET /api/v1/invoices/{id}.
- create_invoiceinvoice:write
- Create a draft invoice from line items. The total is computed from the lines; it is never accepted as an argument. Mirrors POST /api/v1/invoices.
- list_projectsproject:read_all or project:read_own
- Projects the key may see — all of them, or only its own, the same split a person with those permissions gets. Mirrors GET /api/v1/projects.
- list_ticketsticket:read_all or ticket:read_assigned
- Support tickets the key may see, scoped the way the inbox is. Mirrors GET /api/v1/tickets.
- create_customercustomer:create
- Create a customer (company). Mirrors POST /api/v1/customers.
- update_customercustomer:update
- Update a customer (company). Mirrors PATCH /api/v1/customers/{id}.
- create_contactcustomer:update
- Add a contact (a person inside a customer). Mirrors POST /api/v1/contacts.
- update_contactcustomer:update
- Update a contact. Mirrors PATCH /api/v1/contacts/{id}.
- create_projectproject:write
- Create a project. Mirrors POST /api/v1/projects.
- update_projectproject:write
- Edit a project. Mirrors PATCH /api/v1/projects/{id}.
- create_ticketticket:reply
- Raise a support ticket. Mirrors POST /api/v1/tickets.
- update_ticketticket:reply
- Edit a ticket: subject, department, priority, assignee, and status. Mirrors PATCH /api/v1/tickets/{id} (which adds status on top of the staff PATCH — see that route).
- reply_ticketticket:reply
- Post a reply (or an internal note) on a ticket. Mirrors POST /api/v1/tickets/{id}/replies.
- update_invoiceinvoice:write
- Edit a DRAFT invoice (never a sent one — see the route this mirrors). Mirrors PATCH /api/v1/invoices/{id}.
- send_invoiceinvoice:write
- Move a DRAFT invoice to SENT and email it to the customer. Mirrors POST /api/v1/invoices/{id}/send.
- record_paymentpayment:record
- Record a payment against an invoice. Mirrors POST /api/v1/invoices/{id}/payments.
- list_tasksproject:read_all or project:read_own
- List tasks, scoped the way the reader is. Mirrors GET /api/v1/tasks.
- create_tasktask:write
- Create a task. Mirrors POST /api/v1/tasks.
- update_tasktask:write
- Edit a task. Mirrors PATCH /api/v1/tasks/{id}.
- log_timetime:log
- Log time by hand against a task or a project. Mirrors POST /api/v1/time.
- list_estimatesestimate:read
- List estimates, with derived totals. Mirrors GET /api/v1/estimates.
- get_estimateestimate:read
- One estimate, with the same derived totals as the list. Mirrors GET /api/v1/estimates/{id}.
- create_estimateestimate:write
- Create a draft estimate from line items. Mirrors POST /api/v1/estimates.
- send_estimateestimate:write
- Move a DRAFT estimate to SENT and email it to the customer. Mirrors POST /api/v1/estimates/{id}/send.
Not built yet
- Delete: customers, contacts, projects and tickets are deactivated or archived over the API, never deleted — the same rule the staff UI follows where it applies.
- Credit notes, proposals, expenses, contracts, leads and the knowledge base are not in the API yet.
- OAuth apps: keys are created by a firm’s own administrator, not granted to a third-party app.