Documentation Index
Fetch the complete documentation index at: https://apidocs.scripe.io/llms.txt
Use this file to discover all available pages before exploring further.
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:
Scripe-Api-Version: 2026-08-01
- 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-Deprecation response header before any code change:
Scripe-Deprecation: 2026-08-01; sunset=2026-12-01; replacement=2026-11-01
If you see a 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).
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
GET /v1/notes?projectId=proj_abc&limit=50
GET /v1/notes?projectId=proj_abc&limit=50&cursor=<prev next_cursor>
| Parameter | Default | Max | Notes |
|---|
limit | 50 | 200 | Clamped silently. Out-of-range → 400 bad_pagination. |
cursor | — | — | Opaque base64 string. Echo what we returned last time. |
Response envelope
{
"data": [ /* resource objects */ ],
"pagination": {
"next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2L…",
"has_more": true
}
}
next_cursor is null once there are no more rows.
has_more is 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
emit 400 bad_cursor because the keyset reference no longer applies.
- A cursor never expires server-side, but your filters might (e.g. you
filter by a
dateTo that’s now in the past). Treat 400 bad_cursor
as “start the loop over from the beginning” rather than as a bug.
let cursor: string | null = null;
while (true) {
const u = new URL("https://api.scripe.io/v1/notes");
u.searchParams.set("projectId", projectId);
u.searchParams.set("limit", "100");
if (cursor) u.searchParams.set("cursor", cursor);
const res = await fetch(u, { headers });
const { data, pagination } = await res.json();
yield* data;
if (!pagination.has_more) break;
cursor = pagination.next_cursor;
}
Errors
Every non-2xx response returns the same envelope:
{
"error": {
"code": "key_revoked",
"message": "This API key has been revoked.",
"request_id": "req_01J9Z…",
"docs_url": "https://docs.scripe.io/api/v1/errors#key_revoked"
}
}
code is the canonical machine-readable identifier. Stable across
releases. Switch on this — never on message or HTTP status alone.
message is human-readable and may evolve.
request_id is your handle to correlate with our Sentry span if you
open a support ticket. It also matches the X-Request-Id response
header.
docs_url deep-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_cursor doesn’t include details, but invalid_request may
include the offending field.
The full code catalogue lives in errors/. One page per code.
Status code summary
| HTTP | Common codes |
|---|
| 400 | invalid_request, bad_cursor, bad_pagination, version_unsupported |
| 401 | unauthenticated, invalid_token, key_revoked, key_expired |
| 403 | scope_missing, plan_not_eligible, workspace_mismatch |
| 404 | not_found |
| 405 | method_not_allowed |
| 429 | rate_limited |
| 500 | internal_error |
| 503 | service_unavailable |
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:
| Bucket | Limit | Used by |
|---|
read | 120 req/min | Every endpoint shipping in Phase 2. |
write | 30 req/min | Reserved for Phase 3 write endpoints. |
Each successful response carries:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 41
X-RateLimit-Reset is in seconds, not a timestamp. On a 429 we also
include Retry-After:
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 17
Survival tips
- Pre-throttle. Watch
Remaining and 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.
Scripe-Api-Version: 2026-08-01
X-Request-Id: req_01J9Z…
Access-Control-Allow-Origin: *
Cache-Control: no-store (default; health probes override)
For 4xx and 5xx the same headers apply, plus 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.
| Resource | Prefix | Example |
|---|
| Workspace | org_ | org_2pYJfL3VpQK4G2J7nE9b6Vw (Clerk org id) |
| Project | proj_ | proj_01J9ZAB12CD34E56F7G8H9 |
| Note | note_ | note_01J9ZAB12CD34E56F7G8H9 |
| Post | post_ | post_01J9ZAB12CD34E56F7G8H9 |
| Source | src_ | src_01J9ZAB12CD34E56F7G8H9 |
| API key | key_ | key_01J9ZAB12CD34E56F7G8H9 |
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 trailing Z:
Date-only filter parameters (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 responds Access-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 at
packages/api-public/openapi/v1.yaml
is the canonical schema; anything you observe but cannot find in the
spec is undocumented and may change.