Preview API
Symbol's current API version, what it covers, and how it will change.
Symbol's HTTP API is currently in preview. The preview namespace is the
current version of the API — not a beta branch that runs alongside a stable
release. There is no v1 yet. When Symbol promotes the surface to v1, the
preview routes will continue to exist for a deprecation window so that
integrations can migrate at their own pace.
Here are the API docs: https://symbol.chat/docs/api
What "preview" means
- Preview is the current version. Every public, in-scope resource
endpoint is served under
/api/preview/*. - Preview is free. Users on the preview tier self-select as early adopters who accept aggressive iteration in exchange for shaping the contract.
- Schemas, error shapes, and field semantics may change during the preview window — but never silently. Breaking changes are announced through the API stability policy (see Stability & promotion below) before they ship.
- The URL prefix is the loudest possible signal that the contract
is still moving. Once a route is promoted to
/api/v1/*, breaking changes there require a 12-month deprecation window. - No SLA yet. Symbol does not yet publish uptime or latency commitments for preview.
Versioning model
/api/preview/* ← current version (this is where you integrate)
/api/v1/* ← future stable contract
/api/v2/* ← later stable contractVersions coexist under different namespaces. We never replace a route in
place. When v1 lands, a /api/preview/capsules/:id route is copied to
/api/v1/capsules/:id and the two are allowed to diverge. Preview may keep
receiving fixes during the deprecation window.
The architecture is intentionally low-magic:
- URL == file location. The route file's path on disk matches the URL
served — no
next.config.tsrewrite layer, no version-aware middleware. - Thin handlers, shared services. Route handlers stay tiny and delegate to a shared service layer. Two version routes can share one service function.
- Promotion is a copy. Promoting preview → v1 is a physical file copy plus a registered code change in the OpenAPI spec, not a rename.
What's in the preview namespace
The preview namespace covers Symbol's resource endpoints — the routes that represent the Symbol data model and account operations. Examples:
/api/preview/capsules/*,/api/preview/types/*,/api/preview/projects/*/api/preview/organizations/*,/api/preview/share/*,/api/preview/collections/*/api/preview/invitations/*,/api/preview/attachments/*/api/preview/mentions/*,/api/preview/references/*/api/preview/library/*,/api/preview/search/*/api/preview/workspaces/*/api/preview/billing/*(non-webhook),/api/preview/gdpr/*,/api/preview/support/*
The OpenAPI document is the authoritative inventory of the public preview surface. Endpoints not listed there are not part of the public contract and may move, change, or disappear without notice — including any URLs discoverable by inspecting the application but not advertised in the spec.
Preview signals
Beyond the URL prefix, preview is signalled in several places so it's hard to miss:
- The OpenAPI document marks every preview operation with an
x-preview: trueextension (see emission convention) - The hosted API reference shows a banner: "Preview API — subject to breaking change without notice"
- Generated TypeScript and Dart clients carry a banner in the file header
- The Postman collection name is suffixed
(preview) - MCP tool descriptions note the preview surface
x-preview OpenAPI extension
Every operation served under /api/preview/* emits x-preview: true at
the operation level in the generated OpenAPI document. Tooling consumes it
to render banners, gate generated-client release channels, and drive the
public changelog.
Example:
paths:
/api/preview/capsules/{id}:
get:
summary: Get capsule
x-preview: true
responses:
"200": { ... }Stable (/api/v1/*) operations do not emit this extension. The
emission rule is path-derived: the OpenAPI generator inspects each
operation's URL prefix and stamps x-preview: true on every operation
served from /api/preview/*. No per-route annotation is required, so the
flag cannot drift from the URL. A unit test asserts the invariant against
the generated artifact.
Error envelope
All error responses use RFC 9457 Problem Details.
The body is JSON with a Content-Type: application/problem+json header and
the following canonical members:
| Member | Type | Notes |
|---|---|---|
type | string | URI identifying the problem type (e.g. https://symbol.chat/problems/not-found). Stable and machine-readable. |
title | string | Short human-readable summary, suitable as a UI fallback. Stable per type. |
status | integer | Mirrors the HTTP status code. |
detail | string | Human-readable explanation specific to this occurrence. |
Endpoints may include extension members at the top level alongside the canonical fields. Common extensions:
| Extension | Where used | Description |
|---|---|---|
errors | 422 from request schema validation | Array of { path, rawPath, message, code } describing each Zod validation issue. path is a dotted string (items.0.name); rawPath preserves segment types so clients can disambiguate indices from numeric keys (["items", 0, "name"]). |
unknown_field | 422 from rejectUnknownFields | Name of the field that wasn't accepted. |
suggestion | 422 from rejectUnknownFields | Suggested correct field name when one is known. |
capsule_count | 409 when deleting a Type that has capsules | How many capsules block the delete. |
Example — validation failure:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://symbol.chat/problems/unprocessable-entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Validation error: title required at \"title\"; expected number, received string at \"age\"",
"errors": [
{ "path": "title", "rawPath": ["title"], "message": "title required", "code": "too_small" },
{ "path": "age", "rawPath": ["age"], "message": "expected number", "code": "invalid_type" }
]
}Example — unknown field:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://symbol.chat/problems/unprocessable-entity",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Field 'content' is not accepted on this endpoint; did you mean 'content_md'?",
"unknown_field": "content",
"suggestion": "content_md"
}Standard problem types
| Status | type URI | title |
|---|---|---|
| 400 | https://symbol.chat/problems/bad-request | Bad Request |
| 401 | https://symbol.chat/problems/unauthorized | Unauthorized |
| 403 | https://symbol.chat/problems/forbidden | Forbidden |
| 404 | https://symbol.chat/problems/not-found | Not Found |
| 409 | https://symbol.chat/problems/conflict | Conflict |
| 422 | https://symbol.chat/problems/unprocessable-entity | Unprocessable Entity |
| 500 | https://symbol.chat/problems/internal-server-error | Internal Server Error |
The type URIs are stable identifiers — they don't have to resolve to
documentation, but they are unique and won't be repurposed. Clients should
prefer matching on type over parsing detail.
Exempt routes
A small set of external-contract routes use a different error envelope and are out of scope for the Problem Details migration:
- OAuth 2.0 token / client routes (
/api/oauth/*) emit{ "error": "invalid_grant", "error_description": "..." }per RFC 6749 §5.2. - NextAuth callback URLs (
/api/auth/[...nextauth]) follow NextAuth's internal contract. - Stripe webhooks (
/api/billing/webhook) follow Stripe's required envelope.
First-party clients (Flutter, MCP) tolerate both shapes during the rollout
of the per-entity migration: they read detail first and fall back to
error_description / error when the body is from an exempt route.
Rate limits
Authenticated requests to /api/preview/* are rate-limited per user
and per API key. The per-user limit is your account's total budget
across every tool that uses your account; the per-API-key limit is a
sub-ceiling that prevents one integration from consuming the whole
budget.
Limits today (subject to change — tracked on the changelog
as cosmetic):
| Bucket | Default |
|---|---|
| Per-user (Free) | 500 requests per hour |
| Per-user (Pro) | 1000 requests per hour |
| Per-API-key (Free) | 400 requests per hour, tunable per key |
| Per-API-key (Pro) | 800 requests per hour, tunable per key |
The per-API-key default is sized to 80% of your per-user cap so a single integration can saturate most of your budget while still leaving headroom for additional keys, and so a plan upgrade lifts the per-key default automatically without you having to edit each key. Override it per key in Settings → API Keys if you need a tighter or looser limit.
Every authenticated response carries the budget headers so you can self-throttle without waiting for a 429:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Cap for the more restrictive of (per-key, per-user). |
X-RateLimit-Remaining | Approximate budget left in the current window. |
X-RateLimit-Reset | Unix-second timestamp when the current window resets. |
When a limit is exceeded the response is HTTP 429 with the standard Problem Details body and these additional headers:
| Header | Notes |
|---|---|
Retry-After | Seconds until the next request will succeed. |
X-RateLimit-Scope | global (whole-account budget) or endpoint (per-key or per-route ceiling). |
Treat Remaining as advisory, not authoritative — under concurrent
load, two requests racing the boundary can each see a non-zero
remaining and one of them still gets the 429.
To inspect your current usage and per-API-key counters live, call
GET /api/preview/user/rate-limit-status.
Stability & promotion
The stability contract — breaking-change notice periods, promotion
criteria for preview → v1, and the channels Symbol uses to announce
changes — lives in the dedicated API stability policy.
The short version: anything under /api/preview/* may change with
at least 14 days notice via the API changelog
and an email to API key holders; once /api/v1/* ships, breaking
changes there carry a 12-month deprecation window.