Authentication
All API requests use an API key as a bearer token. Create one in the app under Developer → API keys. Keys are either read-only or read/write.
curl https://savrsoft.com/api/v1/events \ -H "Authorization: Bearer savr_live_your_key_here"
Base URL: https://savrsoft.com/api/v1 · All responses are JSON. Errors return { "error": "…" } with a 4xx/5xx status.
Status codes
| Code | When you get it |
|---|---|
200 | Successful read. |
201 | Successfully created. The body is { "id": 42 } — fetch the record if you need the rest of it. |
400 | A required field is missing, e.g. { "error": "name is required" }, or a pagination cursor could not be read. |
401 | No API key, a malformed one, or a key that has been revoked. |
403 | A read-only key was used for a write. Create a read/write key under Developer → API keys. |
404 | The record does not exist, or belongs to another organization — the two are deliberately indistinguishable. Also returned for an unrecognised path or method. |
Scope of this API
The API is read and create only. There are no DELETE, PUT or PATCH
endpoints — a request using one returns 404 Unknown endpoint, the same as any unrecognised path. Deleting and
editing are done in the app, so an integration cannot destroy a caterer's records. If you need write access beyond creating,
tell us what you are building.
Pagination
Every list endpoint is paginated. Pass limit (default 100, maximum 500) and
follow next_cursor until has_more is false:
{
"data": [ ... ],
"has_more": true,
"next_cursor": "eyJuYW1lIjoiQWNtZSIsImlkIjo0Mn0"
}
Pass the cursor straight back to get the next page. Treat it as opaque — the encoding may change:
curl "https://savrsoft.com/api/v1/clients?limit=100&cursor=eyJuYW1lIjoiQWNtZSIsImlkIjo0Mn0" \ -H "Authorization: Bearer savr_live_your_key_here"
Paging is anchored to the last row you received, not to an offset, so rows created or deleted while you page will not cause you to skip or repeat a record. A cursor stays valid indefinitely.
| Parameter | Behaviour |
|---|---|
limit | 1–500. Absent, zero, negative or non-numeric falls back to 100; anything above 500 is capped at 500. |
cursor | next_cursor from the previous page. Omit it for the first page. A cursor that cannot be read — corrupted, or from a different endpoint — returns 400 Invalid cursor rather than silently restarting from the beginning. |
Ordering: /events and /invoices newest first; /recipes and
/clients by name, with ties broken by id so paging stays stable when names repeat.
Endpoints
/v1/meYour organization (id, name, currency).
/v1/eventsList events, newest first. Paginated — see Pagination above.
/v1/events/{id}One event with its dishes.
/v1/events · requires read/writeCreate an event. Body:
{ "name": "Acme holiday party", "event_date": "2026-12-18",
"guests": 120, "price_per_head": 55, "event_type": "corporate" }
/v1/recipes · GET/v1/recipes/{id}List recipes (with allergens & diet flags), or one recipe with ingredients. The list is paginated and ordered by name.
/v1/clients · POST/v1/clientsList clients (paginated, by name) or create one. Create body: { "name": "...", "email": "...", "phone": "...", "company": "..." }
/v1/invoices · GET/v1/invoices/{id}List invoices with computed totals, newest first, or one invoice with line items. The list is paginated.
Webhooks
Register an endpoint under Developer → Webhooks. We POST a JSON payload when events occur:
{
"id": "evt_abc123",
"type": "event.inquiry",
"created_at": "2026-07-17T18:20:00.000Z",
"data": { "id": 42, "name": "Corporate lunch — Dana Ruiz", ... }
}
Event types
| Type | Fires when |
|---|---|
event.created | An event is created |
event.inquiry | A new online catering request arrives |
invoice.sent | An invoice is marked sent |
invoice.paid | An invoice is marked paid |
proposal.approved | A client approves a proposal |
client.created | A client is created |
Verifying signatures
Each delivery includes an x-savrsoft-signature header: the HMAC-SHA256 of the raw request body, keyed with your webhook signing secret. Compute the same and compare:
// Node.js
const crypto = require('crypto');
const sig = crypto.createHmac('sha256', SIGNING_SECRET)
.update(rawBody).digest('hex');
if (sig !== req.headers['x-savrsoft-signature']) reject();
