For developers
The Zentra API.
Connect your website, your shop or your tools to Zentra. Read your data, submit drafts and get notified of every paid invoice.
- JSON reads
- Customers, items, quotes, orders, invoices and payments through a simple REST API.
- Nothing without your approval
- What your tools send arrives as a draft. You approve it in the app.
- Included
- In Solo, Team and Pro at no extra cost. 600 calls per hour per key.
Before you start
- In the Zentra app, open Settings › API, shops and post. Or use your online account.
- Create a key and give it a name, for example “website”. Choose “Read”, or “Read and drafts” if your tool should also submit requests.
- Copy the key right away: it starts with zk_ and will not be shown again. Keep it like a password, on the server side only.
- Open Zentra on one of the company’s computers. The app then publishes the data the API can read, and keeps it up to date while it is open.
Only the company’s owner and administrators create and revoke keys. A revoked key stops working immediately.
Your data stays in the app: the server only keeps the latest published version of these six record types, for companies that have created a key or a webhook. The publishedAt field of each response says when the app published them.
Authentication
Every call sends the key in the Authorization header. All addresses start with:
https://zentraapp.ch/api/v1Examplecurl https://zentraapp.ch/api/v1/invoices?status=issued&limit=20 \
-H "Authorization: Bearer zk_votre_cle"Responses are JSON in UTF-8 and are never cached. Do not call the API from a browser: the key would be visible.
Resources
Six read-only resources. Each can be read as a list, or one record at a time by its id.
/customers- Customers
id, number, name, company, contactPerson, email, phone, addressLine1, addressLine2, postalCode, city, country, archived /articles- Catalogue items and services
id, kind, sku, name, description, unit, unitPriceCents, vatBp, trackStock, stockQuantityMilli, archived /quotes- Quotes
id, number, customerId, title, status, currency, issueDate, validUntil, subtotalCents, discountCents, vatCents, totalCents, lines /orders- Customer orders
id, number, customerId, title, status, currency, orderDate, externalReference, totalCents, lines /invoices- Invoices
id, number, customerId, title, status, type, currency, issueDate, dueDate, subtotalCents, discountCents, vatCents, totalCents, paidCents, openCents, lines /payments- Payments received
id, invoiceId, date, amountCents, currency, method, reference
Quotes, orders and invoices contain their lines, with position, articleId, description, quantityMilli, unit, unitPriceCents, discountBp, vatBp and totalCents. Every record also carries updatedAt and changedAt.
Lists and pagination
Lists are sorted by change date. To fetch everything without missing anything, follow nextCursor while hasMore is true.
limit- Results per page, from 1 to 100. 50 by default.
cursor- The nextCursor value of the previous page.
updated_since- Only what has changed since this date (ISO 8601 or Unix timestamp).
status- Filter by status, for example issued, paid or draft.
customer_id- Only one customer’s documents.
include_deleted- true to also receive deletions, as { id, deleted: true }.
{
"object": "list",
"entity": "invoices",
"data": [
{
"object": "invoice",
"id": "inv_8f2c",
"number": "2026-0142",
"customerId": "cus_51a0",
"status": "issued",
"currency": "CHF",
"issueDate": "2026-10-20",
"dueDate": "2026-11-19",
"totalCents": 129690,
"paidCents": 0,
"openCents": 129690,
"lines": [ … ],
"changedAt": "2026-10-20T09:14:03.000Z"
}
],
"hasMore": true,
"nextCursor": "c1_WzE3OTI…",
"publishedAt": "2026-10-26T07:55:12.000Z"
}Sync customers// Node 18 ou plus récent : reprendre tout ce qui a changé depuis la dernière fois
const headers = { Authorization: `Bearer ${process.env.ZENTRA_API_KEY}` };
let url = 'https://zentraapp.ch/api/v1/customers?limit=100&include_deleted=true&updated_since=2026-10-01';
while (url) {
const response = await fetch(url, { headers });
if (!response.ok) throw new Error((await response.json()).error.message);
const page = await response.json();
for (const customer of page.data) console.log(customer.id, customer.name ?? 'supprimé');
url = page.nextCursor ? `https://zentraapp.ch/api/v1/customers?limit=100&include_deleted=true&cursor=${page.nextCursor}` : null;
}Amounts and quantities
All figures are integers calculated by the app. The server recalculates nothing.
…Cents- Amounts in cents: 129690 = CHF 1’296.90.
…Milli- Quantities in thousandths: 1000 = one unit.
vatBp, discountBp- Rates in basis points: 100 = 1%. The standard VAT rate is written 810.
currency- CHF or EUR.
openCents- Balance still due on an invoice.
Submit a draft
A form on your website can create a quote request, an order or a new customer. The draft arrives in the app, to approve or reject. Nothing is saved or sent without you.
POST /draftscurl -X POST https://zentraapp.ch/api/v1/drafts \
-H "Authorization: Bearer zk_votre_cle" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: site-demande-20261026-0042" \
-d '{
"kind": "quote",
"customer": { "name": "Marie Rochat", "email": "marie.rochat@example.ch" },
"reference": "Formulaire du site",
"note": "Pose prévue en novembre",
"lines": [
{ "sku": "RAD-600", "description": "Radiateur 600 W", "quantity": "3" },
{ "description": "Pose et mise en service", "quantity": "1.5" }
]
}'Response 202{ "object": "draft", "id": "in_3b9e…", "kind": "quote", "state": "pending", "duplicate": false }- The kind field is customer, quote or order. A quote or an order has 1 to 200 lines.
- Give an existing customer by their id, or their details: name, company, email, phone, addressLine1, addressLine2, postalCode, city, country.
- Each line has a quantity, and a sku, an articleId or a description. Any prices are only hints: the app applies your catalogue and calculates the totals.
- With the Idempotency-Key header, a repeated submission creates only one draft. The response then shows duplicate: true.
- To follow a draft, read /drafts/ followed by its id: the state is pending, imported or rejected, with the reference of the document created.
Webhooks
Zentra notifies your https address at every event you choose. Add the webhook in the same place as the keys. Its signing secret starts with whsec_ and is shown only once.
customer.created- A customer is created.
quote.created- A quote is issued.
quote.accepted- A quote is accepted.
order.created- An order is confirmed.
invoice.created- An invoice is issued.
invoice.paid- An invoice is paid in full.
payment.created- A payment is recorded.
draft.imported- A draft submitted through the API was approved in the app.
draft.rejected- A draft submitted through the API was rejected.
POST https://exemple.ch/zentra
Zentra-Event: invoice.paid
Zentra-Event-Id: evt_…
Zentra-Delivery-Id: dlv_…
Zentra-Signature: t=1793091600,v1=5d41c0…
{
"id": "evt_…",
"type": "invoice.paid",
"apiVersion": 1,
"createdAt": "2026-10-26T08:20:00.000Z",
"data": { "object": "invoice", "record": { "id": "inv_8f2c", "status": "paid", … } }
}The signature is a hexadecimal HMAC-SHA256, computed with your secret over the timestamp t, a dot, then the body received. Reject a timestamp older than five minutes.
Verify the signature (Node)import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody : le corps reçu, tel quel, avant tout JSON.parse
export function isFromZentra(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(',').map((part) => part.split('=')));
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}Subscriptions through the API
Zapier, Make, n8n or your own tool can create and remove their webhooks with the API key. GET /me checks the key and gives the company’s name; GET /webhooks lists the subscriptions.
Subscribe and unsubscribePOST https://zentraapp.ch/api/v1/webhooks
Authorization: Bearer zk_votre_cle
Content-Type: application/json
{ "url": "https://hooks.zapier.com/hooks/catch/…", "events": ["invoice.paid"] }
201 { "object": "webhook", "id": "whe_…", "events": ["invoice.paid"], "secret": "whsec_…" }
DELETE https://zentraapp.ch/api/v1/webhooks/whe_…- Reply with a 2xx code within ten seconds. Redirects are not followed.
- On failure, delivery is retried up to eight times: after 1 minute, 5 minutes, 30 minutes, then 2, 6, 12 and 24 hours.
- After twenty failures in a row, the webhook pauses. The “Resume” button restarts it.
- The same event can arrive twice: deduplicate with Zentra-Event-Id.
- The “Send a test” button sends a ping event.
AI assistants (MCP)
Claude, ChatGPT or any MCP-compatible assistant can read your Zentra data. It answers “Who still owes me money?” from the amounts calculated by the app.
In Claude, without a key
- In Claude, open Settings, then Connectors, and choose Add custom connector. On a Team or Enterprise plan, an owner of the Claude organization adds it.
- Name it Zentra and paste the server address. Leave the advanced settings empty.
- Click Connect. Zentra opens: sign in, choose the company and allow access.
- Open Zentra once on one of the company’s computers. The app then publishes the data the assistant reads.
https://zentraapp.ch/api/mcpOnly the account holder and administrators can connect an assistant, one company at a time. Connected assistants are listed in your account under Integrations, where one click disconnects them.
In Claude Code
Terminalclaude mcp add --transport http zentra https://zentraapp.ch/api/mcpThen run /mcp in Claude Code and choose Zentra to sign in.
Ready-made questions
The server also offers ready-made questions. In Claude, they appear in the conversation’s “+” menu.
qui_me_doit- Who owes me money?
ma_journee- What is waiting for me today?
relances_semaine- Prepare this week’s reminders
resume_mois- Summarise my month
creer_devis- Create a quote
nouveau_client- Add a customer
With an API key
A tool that does not offer account sign-in can use an API key, sent in the Authorization header.
Setup{
"mcpServers": {
"zentra": {
"type": "http",
"url": "https://zentraapp.ch/api/mcp",
"headers": { "Authorization": "Bearer zk_votre_cle" }
}
}
}search_customers- Searches customers by name, company or email.
get_customer- A customer and their latest invoices.
list_invoices- Open, overdue, paid or draft invoices.
get_invoice- An invoice with its lines and payments.
list_quotes- Quotes by status and by customer.
list_orders- Customer orders.
search_articles- Catalogue items, with price and stock.
company_summary- Key figures: open and overdue invoices, number of records.
prepare_draft- Files for approval a customer, supplier, item, project, project task, appointment, quote, order, draft invoice or note. If a detail is missing, it answers with the questions to ask. Only if drafts are allowed.
draft_requirements- The required details of each draft, with the question to ask.
search_projects- Projects and sites, to attach a task, an appointment or a note.
search_suppliers- Suppliers by name or contact.
list_autopilot_tasks- The tasks waiting in Autopilot, with the actions the assistant may request.
act_on_autopilot_task- Confirms a task when you ask: create a record or a received draft, add an appointment, postpone, ignore, mark a task done. Only if you ticked “Confirm Autopilot tasks” when connecting.
check_request- Says whether a draft or a requested action has already been handled by the app.
The assistant cannot send, pay or delete anything. A draft it prepares waits for your approval in the app, unless you ask it to confirm it.
When a detail is missing, Zentra does not guess: an item without a price, a customer resembling several records or a project that cannot be found go back to the assistant as questions, which it asks you before creating.
A confirmed action is carried out by the Zentra app as soon as it is open on one of the company’s computers, then written to the Autopilot log. Sending an e-mail or a reminder, recording a payment, issuing an invoice and starting debt collection always happen in the app.
Errors and limits
An error returns an HTTP status and an error object with a stable code and a message in French.
{ "error": { "code": "rate_limited", "message": "Trop d’appels pour cette clé. Réessayez dans une heure." } }- 400
invalid_query, invalid_draft, invalid_body - The request is malformed. The message says which field to fix.
- 401
unauthorized - Key missing, miscopied or revoked.
- 402
subscription_inactive - The company’s subscription is no longer active.
- 403
forbidden - The key can only read. Create a “Read and drafts” key.
- 404
not_found - Unknown resource or id.
- 405
method_not_allowed - Only GET is accepted, except POST on drafts.
- 429
rate_limited - More than 600 calls in the hour for this key. The Retry-After header gives the wait in seconds.
- 503
unavailable - Service temporarily unavailable. Try again later.
Limits: 600 calls per hour per key, 100 results per page, 64 KB per draft.
Connect Shopify, WooCommerce or postal mailTry the API on your own data.
Fourteen days free, API and webhooks included.
No credit card. No automatic payment.
