The Suprata API

REST over HTTPS, JSON, key-authenticated. Base URL, scopes, pagination and polling, rate limits, webhooks, error codes, and the one non-obvious thing: keys bypass user permissions.

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_more and next_cursor. Loop, passing next_cursor back as starting_after, until has_more is false.

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
}
  • id is the event id. It's the same on every retry and on every endpoint the event goes to, and it's the id GET /v1/events uses. De-duplicate on it: a receiver that was slow to answer can get the same event twice.
  • delivery_id identifies this particular delivery (also sent as the X-Platform-Delivery header, alongside X-Platform-Event and X-Platform-Event-Id).
  • data holds 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.