Conventions
This page documents the rules that apply uniformly to every endpoint in the Scripe API. If your client respects these conventions, every new endpoint we ship will Just Work — there are no per-resource surprises.Versioning
Pin a date-stamped version on every request:- The current version is
2026-08-01(Phase 2 release). - Omitted header → we default to the current version. This is fine for exploration but dangerous in production — pin explicitly so the next version cut doesn’t change your responses under you.
- Unknown version →
400 version_unsupported. The error body lists the versions we still accept. - We accept at least the two most recent versions at any time, with a
minimum 90-day overlap when we sunset an old one. Sunset is announced
via the
Scripe-Deprecationresponse header before any code change:
Scripe-Deprecation header, plan a migration before the
sunset date. Bumping the pinned version is usually a one-line change.
We never make breaking changes within a pinned version. Additive
changes (new optional fields on responses, new endpoints) can ship at
any time and are documented in the changelog at
errors/ (versioned alongside the error codes).
Pagination
All list endpoints (GET /v1/notes, GET /v1/posts, GET /v1/projects)
use opaque cursor pagination. There are no page numbers; you loop until
the response says there are no more rows.
Request
Response envelope
next_cursorisnullonce there are no more rows.has_moreis the canonical “loop again?” signal. Don’t compare cursor values — they’re opaque and may change shape across versions.- A cursor is bound to the request’s filter set. Changing
projectId,dateFrom,dateTo,status,folderId, etc. mid-loop will likely emit400 bad_cursorbecause the keyset reference no longer applies. - A cursor never expires server-side, but your filters might (e.g. you
filter by a
dateTothat’s now in the past). Treat400 bad_cursoras “start the loop over from the beginning” rather than as a bug.
Pagination idiom (pseudocode)
Errors
Every non-2xx response returns the same envelope:codeis the canonical machine-readable identifier. Stable across releases. Switch on this — never onmessageor HTTP status alone.messageis human-readable and may evolve.request_idis your handle to correlate with our Sentry span if you open a support ticket. It also matches theX-Request-Idresponse header.docs_urldeep-links to a page describing the code, the most likely cause, and a suggested remediation.details(optional) carries a code-specific payload. For example,bad_cursordoesn’t include details, butinvalid_requestmay include the offending field.
Status code summary
A 401 always means “fix your authentication”. A 403 always means “ask
the dashboard for more scopes / upgrade your plan”. A 429 means “back
off and retry”; never treat it as a permanent failure.
Rate limits
Limits are per-workspace, per-bucket, sliding 60-second window. Two buckets exist:
Each successful response carries:
X-RateLimit-Reset is in seconds, not a timestamp. On a 429 we also
include Retry-After:
Survival tips
- Pre-throttle. Watch
Remainingand slow yourself down rather than burning the whole budget. - Respect
Retry-After. It’s the smallest safe sleep value; longer is fine. - Group by workspace, not by key. Adding more keys for the same workspace doesn’t increase your budget.
- Test mode counts. A test key shares the workspace bucket with live keys. CI loops shouldn’t run against production data without a dedicated test workspace.
Headers we set on every response
Retry-After on 429 and
Allow on 405.
Resource IDs
Every resource exposes a typed string id with a stable prefix. Treat the whole string as opaque — you should never parse beyond the prefix.
A 404 on
GET /v1/projects/proj_does_not_exist is indistinguishable
from a 404 on GET /v1/projects/proj_belongs_to_other_workspace. We do
not leak the existence of cross-workspace resources via 401/403/404
distinctions.
Date and time
Every timestamp on the wire is ISO 8601 UTC with a trailingZ:
dateFrom, dateTo) accept YYYY-MM-DD
and are interpreted as start- and end-of-day in UTC respectively
(dateFrom=2026-08-01 → >= 2026-08-01T00:00:00Z,
dateTo=2026-08-01 → <= 2026-08-01T23:59:59.999Z).
CORS
The API respondsAccess-Control-Allow-Origin: * for read endpoints, so
any browser can call it as long as the user supplies their own
Authorization header. We do not echo cookies, and we strip any
inbound Cookie headers — the API surface is stateless and never
participates in browser session auth.
CORS preflight (OPTIONS) returns 204 without authentication.
Anything else?
If a behaviour isn’t documented here or in the per-resource pages, file a bug rather than relying on the observed behaviour. The OpenAPI spec atpackages/api-public/openapi/v1.yaml
is the canonical schema; anything you observe but cannot find in the
spec is undocumented and may change.