Symbol Docs

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 contract

Versions 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.ts rewrite 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: true extension (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:

MemberTypeNotes
typestringURI identifying the problem type (e.g. https://symbol.chat/problems/not-found). Stable and machine-readable.
titlestringShort human-readable summary, suitable as a UI fallback. Stable per type.
statusintegerMirrors the HTTP status code.
detailstringHuman-readable explanation specific to this occurrence.

Endpoints may include extension members at the top level alongside the canonical fields. Common extensions:

ExtensionWhere usedDescription
errors422 from request schema validationArray 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_field422 from rejectUnknownFieldsName of the field that wasn't accepted.
suggestion422 from rejectUnknownFieldsSuggested correct field name when one is known.
capsule_count409 when deleting a Type that has capsulesHow 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

Statustype URItitle
400https://symbol.chat/problems/bad-requestBad Request
401https://symbol.chat/problems/unauthorizedUnauthorized
403https://symbol.chat/problems/forbiddenForbidden
404https://symbol.chat/problems/not-foundNot Found
409https://symbol.chat/problems/conflictConflict
422https://symbol.chat/problems/unprocessable-entityUnprocessable Entity
500https://symbol.chat/problems/internal-server-errorInternal 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):

BucketDefault
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:

HeaderMeaning
X-RateLimit-LimitCap for the more restrictive of (per-key, per-user).
X-RateLimit-RemainingApproximate budget left in the current window.
X-RateLimit-ResetUnix-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:

HeaderNotes
Retry-AfterSeconds until the next request will succeed.
X-RateLimit-Scopeglobal (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 previewv1, 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.

On this page