Skip to content

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-* and Retry-After headers.

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 created
  • invoice.sentInvoice sent
  • invoice.paidInvoice paid in full
  • payment.recordedPayment recorded
  • estimate.acceptedEstimate accepted
  • contract.signedContract signed
  • lead.createdLead created
  • ticket.createdTicket created
  • project.status_changedProject status changed
  • contact.createdContact created
  • meeting.bookedMeeting booked
  • meeting.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.
API and webhooks · Veilux