Skip to main content

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).

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

GET /v1/notes?projectId=proj_abc&limit=50
GET /v1/notes?projectId=proj_abc&limit=50&cursor=<prev next_cursor>
ParameterDefaultMaxNotes
limit50200Clamped silently. Out-of-range → 400 bad_pagination.
cursorOpaque 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.

Pagination idiom (pseudocode)

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

HTTPCommon codes
400invalid_request, bad_cursor, bad_pagination, version_unsupported
401unauthenticated, invalid_token, key_revoked, key_expired
403scope_missing, plan_not_eligible, workspace_mismatch
404not_found
405method_not_allowed
429rate_limited
500internal_error
503service_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:
BucketLimitUsed by
read120 req/minEvery endpoint shipping in Phase 2.
write30 req/minReserved 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.

Headers we set on every response

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.
ResourcePrefixExample
Workspaceorg_org_2pYJfL3VpQK4G2J7nE9b6Vw (Clerk org id)
Projectproj_proj_01J9ZAB12CD34E56F7G8H9
Notenote_note_01J9ZAB12CD34E56F7G8H9
Postpost_post_01J9ZAB12CD34E56F7G8H9
Sourcesrc_src_01J9ZAB12CD34E56F7G8H9
API keykey_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:
2026-08-01T14:23:11.000Z
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.