savrsoft

API documentation

A simple REST API to read and write your catering data, plus signed webhooks for real-time events. Create a key under Developer in the app.

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

CodeWhen you get it
200Successful read.
201Successfully created. The body is { "id": 42 } — fetch the record if you need the rest of it.
400A required field is missing, e.g. { "error": "name is required" }, or a pagination cursor could not be read.
401No API key, a malformed one, or a key that has been revoked.
403A read-only key was used for a write. Create a read/write key under Developer → API keys.
404The 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.

ParameterBehaviour
limit1–500. Absent, zero, negative or non-numeric falls back to 100; anything above 500 is capped at 500.
cursornext_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

GET/v1/me

Your organization (id, name, currency).

GET/v1/events

List events, newest first. Paginated — see Pagination above.

GET/v1/events/{id}

One event with its dishes.

POST/v1/events · requires read/write

Create an event. Body:

{ "name": "Acme holiday party", "event_date": "2026-12-18",
  "guests": 120, "price_per_head": 55, "event_type": "corporate" }
GET/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.

GET/v1/clients · POST/v1/clients

List clients (paginated, by name) or create one. Create body: { "name": "...", "email": "...", "phone": "...", "company": "..." }

GET/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

TypeFires when
event.createdAn event is created
event.inquiryA new online catering request arrives
invoice.sentAn invoice is marked sent
invoice.paidAn invoice is marked paid
proposal.approvedA client approves a proposal
client.createdA 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();
Privacy · Security · Terms · Questions? savrsoft.com · Build catering integrations in minutes.