# Build a back-office integration with the Ondes PRO API

> Scoped API keys, cursor pagination, idempotency, signed webhooks and test mode: build a reliable back-office integration on the Ondes PRO REST API.

Updated on 2026-10-21 · The Ondes PRO team · https://ondes.pro/en/integrations/mcp/guides/build-an-integration

Syncing an ERP, feeding a warehouse system, pushing customers from a line-of-business app: this guide walks through a complete **server-to-server integration** on the Ondes PRO **REST API v1**. A minimally scoped key, paginated reads, **idempotent** writes, **signed webhooks** so you react instead of polling, and the **MCP server** as a companion while you build.

## How do you authenticate against the Ondes PRO REST API?

A service running with nobody at the keyboard uses an **API key**, sent as `Authorization: ApiKey opk_…`. Every key carries **scopes** in `module:action` form — `billing:read`, `shop:write`, `inventory:read` — and those scopes are a **ceiling**: the key never reaches beyond the permissions of the person who created it. If that person loses access to invoicing, so does the key.

Give each integration its own key with only what it needs. The day a server is compromised, you revoke one key without breaking the others, and the call log in the **Analytics** tab shows what it did. Each key also has a **per-minute quota**, chosen at creation; the `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers tell you where you stand.

## Set up a reliable integration, from key to webhook

The scenario: Atelier Lumen’s logistics back office needs every new invoice, and must create in Ondes PRO the customers its sales team enters.

1. **Create a dedicated key** — Under **API & Developers → API keys**, create a key named “Logistics back office” with `billing:read` and `billing:write`, and a quota sized to your volume. The value is shown once: store it in your server’s secret manager.
2. **Read only what you need** — `?fields=` trims each row to the useful fields, and `?expand=` inlines a relation in the same response instead of issuing extra calls. Pages hold 50 rows by default, up to 200 with `page_size`.

```bash
curl "https://your-workspace.ondes.pro/api/v1/billing/factures/?fields=id,numero,statut,date_echeance&page_size=100" \
  -H "Authorization: ApiKey opk_…"
```
3. **Walk a whole collection with a cursor** — For a full sync, add `pagination=curseur` and follow the `next` link to the end. Unlike page numbers, the cursor neither skips nor repeats rows when invoices are created mid-walk.
4. **Write with an idempotency key** — Any `POST`, `PUT` or `PATCH` can carry `Idempotency-Key`. Same key and same body: the stored response comes back and nothing runs again. Same key, different body: `409`, because that signals a client-side bug. Derive the key from your own system’s identifier, not from a random value drawn on every attempt.

```bash
curl -X POST https://your-workspace.ondes.pro/api/v1/billing/clients/ \
  -H "Authorization: ApiKey opk_…" \
  -H "Idempotency-Key: crm-customer-4812" \
  -H "Content-Type: application/json" \
  -d '{"nom": "Le Comptoir des Marées", "email": "purchasing@comptoir-marees.com", "ville": "Saint-Malo"}'
```
5. **Subscribe to events** — `POST /api/v1/developer/abonnements/creer/` registers your server’s address and the events you follow, named `<resource>.<cree|modifie|supprime>` — for instance `billing.factures.cree`. The signing secret is returned **only at creation**. The address must belong to a declared origin, and internal network addresses are refused.

```json
{
  "nom": "Logistics back office",
  "url": "https://logistics.atelier-lumen.com/hooks/ondes",
  "evenements": ["billing.factures.cree", "billing.factures.modifie"]
}
```
6. **Verify each delivery, then fetch the object** — The `Ondes-Signature` header reads `t=<timestamp>,v1=<hmac>`; recompute the HMAC-SHA256 of `<t>.<body>` with your secret and reject timestamps older than five minutes. The payload only carries the object’s identity (resource, `id`, company): fetch it through the API to get its current state.
7. **Test, then catch up on anything missed** — `POST …/abonnements/<id>/tester/` sends a signed `ondes.test` event. `GET …/<id>/livraisons/` lists what went out and with which status code; `POST …/<id>/rejouer/` resends a delivery after an outage on your side.

## Verify a webhook signature in Python

```python
import hashlib, hmac, time

def signature_is_valid(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    timestamp = int(parts.get("t", "0"))
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

Compute the signature over the RAW body you received, before any JSON parsing.

## Which errors to handle, and how to respond?

Every API v1 error shares one JSON shape: an `ondes_erreur` object with a stable `code`, a readable `message` and, for validation failures, the offending `champs`.

| Response | Meaning | Recommended reaction |
| --- | --- | --- |
| `400` | Invalid body or parameter | Read `ondes_erreur.champs`, fix it, do not retry unchanged |
| `403` | Missing scope or permission | Add the scope to the key, or ask the admin for the module |
| `409` | Same `Idempotency-Key`, different body | Fix key generation on the client side |
| `429` | Per-minute quota reached | Wait for `Retry-After`, then resume |
| `503` `instance_occupee` | Workspace briefly busy | Retry after `Retry-After`, with the same idempotency key |

## Bulk writes without leaving a half-finished import

On any resource that accepts writes, `POST /api/v1/<resource>/lot/` bundles up to **100 operations** into a single transaction. The body holds an `operations` list, each with its `methode` (`POST`, `PATCH`, `PUT`, `DELETE`), its `id` for an existing object and its `donnees`.

A batch is all or nothing: one rejected operation rolls back the lot, and the response says which one and why. Every operation goes through the same checks as a single call — permissions, company, test mode — so a batch grants nothing new; it just saves a hundred round trips.

## Test mode: build against real routes with no consequences

Some resources have a twin under `/api/v1/test/`: store orders, video meetings, email sends and invitations. The mode comes from the URL and nothing else — no header, no parameter — so you cannot write for real by accident. Test objects stay out of revenue figures, exports and dashboards.

Point your staging environment at the `/test/` URLs and production at the plain ones: switching becomes a configuration variable rather than a code change.

## The MCP server as a development companion

_Example conversation · demo data (Atelier Lumen)_

> **You** : Write our back office’s webhook handler: for every new invoice, store its number and due date in our `ondes_invoices` table.
>
> 🔧 `ondes_exemple` “Subscribe to events” recipe: creating the subscription, checking `Ondes-Signature`, fetching the object.
>
> 🔧 `ondes_impossible` Confirms a delivery never contains the object’s content, only its identity.
>
> **Assistant** : The webhook only carries the resource and the `id`, so I am adding a `GET billing/factures/<id>/?fields=numero,date_echeance,statut` right after the signature check. The write to `ondes_invoices` is an upsert on `id`, so a replay from `rejouer/` never creates a duplicate.

> **API key or OAuth connection for your assistant?** — Your production service uses a minimally scoped key. Your coding assistant can connect through OAuth instead: the connection carries your full permissions, runs on one-hour tokens and can be revoked by name from the MCP tab. Never reuse the production key in a development tool. Details are on the [MCP server security](https://ondes.pro/en/integrations/mcp/security) page.

## Going further

- [REST API v1](https://ondes.pro/en/integrations/rest-api) — Around 135 REST resources, scoped keys, an OpenAPI schema and signed webhooks.
- [MCP vs REST API](https://ondes.pro/en/integrations/mcp/mcp-vs-rest-api) — What really sets them apart, when to pick which, and why at Ondes PRO they work together.
- [Invoicing](https://ondes.pro/en/integrations/mcp/modules/invoicing) — Quotes, invoices, reminders and credit notes prepared by your assistant, within invoicing rules.
- [Cursor](https://ondes.pro/en/integrations/mcp/clients/cursor) — One `.cursor/mcp.json` file, one OAuth sign-in: Cursor’s agent codes against your real workspace.

## Frequently asked questions

### Which authentication should a server integration use?

An API key in the `Authorization: ApiKey opk_…` header, created under API & Developers → API keys with only the scopes it needs. Scopes are a ceiling: the key never exceeds its creator’s permissions.

### How do I avoid duplicates when a request fails halfway?

Send an `Idempotency-Key` header on every write. Replayed with the same body, the request returns the stored response without running again; with a different body, it returns 409.

### How do I get notified of changes without polling the API?

Create a subscription through `developer/abonnements`: Ondes PRO sends your server events such as `billing.factures.cree`, signed with the `Ondes-Signature` header. The delivery log and the `rejouer/` action let you recover from downtime.

### Why doesn’t the webhook include all of the object’s data?

By design: the payload only carries the object’s identity. You fetch it through the API with your key, which guarantees its current state and applies your permissions at that moment.

### Is there an OpenAPI schema?

Yes, an OpenAPI 3.1 schema generated from your workspace’s real routes, downloadable from API & Developers once signed in. It is personalised: it only describes the workspace’s modules.

### What happens when my quota runs out?

The API returns 429 with a `Retry-After` header. The `X-RateLimit-*` headers on every response let you plan ahead, and a workspace manager can raise the key’s per-minute quota.
