The Suprata API
REST over HTTPS, JSON in and out, authenticated by API key. Connect Suprata to systems you run: a booking site that creates customers, a script that pulls invoices, a dashboard on your own stack, or a Zapier workflow that texts a customer when their job is done.
There are three ways in, and most integrations use more than one:
- Read records (
GET): customers, jobs, invoices, payments, reservations, texts and more. - Write through named, validated endpoints (
POST /v1/accounts,POST /v1/jobs, ...) that run the same code the app runs, so the side effects happen. - Get told when something happens: subscribe a URL to events (webhooks), or poll the event feed.
Base URL
https://<your-account>.suprata.com/api/v1
v1 is current. Fields get added to responses without notice; nothing is removed or repurposed within v1 (breaking changes would ship as v2). Parse leniently and ignore fields you don't recognize.
Authentication
Generate a key in Suprata under Suprata Automate → API Keys, then send it as a bearer token:
curl https://your-account.suprata.com/api/v1/me \
-H "Authorization: Bearer sk_live_your_key"
X-Api-Key: <key> is accepted too, for clients that strip Authorization.
Each key has a scope and a mode (live / test). Scope is fixed at creation; issue a new key to change it.
| Scope | Can do |
|---|---|
| Read only | Every GET. |
| Read & write | Everything above, plus the write endpoints, operations, and subscribing webhooks. |
Subscribing a webhook counts as a write (it changes your account), so a Zapier connection needs a Read & write key. GET /v1/me echoes the account and scopes the key resolved to; call it first to confirm wiring.
Keys bypass user permissions
This is the part that isn't obvious. A key is a service account, not a user. It reaches every record its endpoints cover and ignores the per-user permissions that gate the UI. A write key can change anything in scope, no matter who created it. Consequences:
- Store keys like passwords: an environment variable or a secrets manager, never in client-side code or a committed file.
- Only admins can mint them (gated by the "Manage API Keys" permission), so creating a key can't be used to escalate past permissions you've set.
- Revoke a leaked key immediately. Revocation is instant, and it also switches off every webhook that key subscribed.
- Give each integration its own key, labelled with what it is ("Zapier", "Booking site"). When one misbehaves you can revoke it without breaking the others, and the label tells you which one it was.
Pagination
Cursor-based, not page-numbered (cursors don't skip or double-count when rows are added mid-walk). Lists are always newest first (descending id).
limit: page size, 1–200 (default 50).starting_after=<id>: records older than that id.- The response carries
has_moreandnext_cursor. Loop, passingnext_cursorback asstarting_after, untilhas_moreisfalse.
Polling for changes
If you sync on a schedule rather than with webhooks, don't re-read everything each time. Most resources take:
created_after: records created after a time.updated_since: records changed at or after a time (customers, invoices, reservations, price list).
Both accept a Unix timestamp (1791320786) or an ISO 8601 date/time (2026-10-06T14:00:00Z). A date without an offset is read in your account's timezone. Which resource supports which is listed in the endpoint reference; asking for one a resource doesn't support returns 400 rather than silently ignoring it.
The simple, correct pattern: remember the highest id you've processed, ask for the newest page, and stop when you reach an id you've seen.
Rate limits
Each key may make 120 requests per minute. Every response says where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed per minute for this key. |
X-RateLimit-Remaining |
What's left in the current minute. |
X-RateLimit-Reset |
Unix time the current minute ends. |
Go over and you get 429 Too Many Requests with a Retry-After header (seconds). Wait that long and carry on. The limit is per key, so a runaway script doesn't starve your Zapier connection. If you genuinely need more, use webhooks instead of polling, and contact support if that still isn't enough.
Webhooks: getting told instead of asking
Subscribe a URL to one or more events and Suprata POSTs each one to it, signed. You can set these up two ways:
- In the app, under Suprata Automate → Webhooks. Good for a fixed endpoint you run.
- Through the API (
POST /v1/webhooks), which is what Zapier does when you turn a Zap on. A subscription made this way belongs to the key that made it: that key can list and delete it, nothing else can through the API, and it shows in the app's Webhooks screen marked "Connected app".
Every delivery looks like this:
{
"id": 88231,
"event": "job.status_changed",
"created": 1791320786,
"entity": { "table": "jobs", "id": 5120 },
"data": { "jobs": { "...": "..." }, "accounts": { "...": "..." } },
"delivery_id": 40112
}
idis the event id. It's the same on every retry and on every endpoint the event goes to, and it's the idGET /v1/eventsuses. De-duplicate on it: a receiver that was slow to answer can get the same event twice.delivery_ididentifies this particular delivery (also sent as theX-Platform-Deliveryheader, alongsideX-Platform-EventandX-Platform-Event-Id).dataholds the record the event is about and the records around it (a job's customer, a payment's invoice).
Verify the signature
Each delivery carries X-Platform-Signature: t=<unix time>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" using the endpoint's signing secret (shown once, when the endpoint is created). Compute it over the raw body bytes, compare in constant time, and reject anything with a t more than a few minutes old.
import hmac, hashlib, time
def valid(secret, header, raw_body):
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300
Answer with any 2xx quickly and do the slow work afterwards. Anything else is retried with backoff (1 minute, 5, 30, 2 hours, 6 hours); after six failed attempts the endpoint is switched off and shows the reason in the Webhooks screen.
Response shape
List:
{ "object": "list", "resource": "invoices", "count": 50, "has_more": true, "next_cursor": 567371, "data": [ … ] }
A single record, and the result of a write:
{ "object": "invoices", "data": { "id": "567371", … } }
Numbers come back as strings in many fields (they're read straight from the database). Convert as you read.
Errors
Every error is {"error": "a sentence saying what went wrong"} with one of these statuses:
| Code | Meaning |
|---|---|
400 |
A filter you sent can't be used (unreadable date, a search field that resource doesn't have). |
401 |
Missing, invalid, or revoked key. |
402 |
Texting: your prepaid balance is empty. |
403 |
The key's scope doesn't cover the request, or the feature isn't on your plan. |
404 |
No such record or endpoint. |
405 |
That method isn't supported on that path. |
409 |
Your account isn't set up for this yet (e.g. sending a text with no texting number). |
422 |
A write failed validation. Nothing changed; the message says which field. |
429 |
Rate limited; see Retry-After. |
503 |
That part of the API isn't enabled on your account yet. |
Next
- Endpoints & fields: every endpoint, its filters, and each resource's fields.
- Writing data: creating customers, jobs, notes and texts, and the operations behind them.
GET /v1/openapi.json: the OpenAPI 3.0 spec, for SDK generation or Swagger UI.