Recipe: Sync Your OpenAPI Spec
Import an OpenAPI 3.x spec and Syntext generates endpoint pages — request/response schemas, code samples, and an interactive try-it playground. This recipe keeps the spec in sync automatically.
Recommended: Keep Specs in Your Docs Repo
Commit your specs next to your docs and list them in syntext.json:
{
"openapi": [
{ "path": "./openapi/users.yaml", "prefix": "api-reference/users" },
{ "path": "./openapi/orders.yaml", "prefix": "api-reference/orders" }
]
}
Every build re-reads the specs and regenerates the endpoint pages, so your API reference is synced on every deploy — no extra pipeline step. A single spec can be passed as a plain string ("openapi": "./openapi.json"), and remote specs via url entries are fetched fresh at build time.
Endpoint slugs are derived from the HTTP method and path ({prefix}/get-v1-users-user-id), never from operationId, so generated URLs survive operation renames. If you create an MDX file at the same path as a generated page, your file wins — hand-tune any page without losing the rest of the generation. See openapi in syntext.json for all accepted forms.
Alternative: One-Time Import via API
Point Syntext at a spec URL or upload the document:
curl -X POST https://api.syntext.dev/v1/projects/$PROJECT_ID/openapi \
-H "Authorization: Bearer $SYNTEXT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://api.acme.com/openapi.json" }'
curl -X POST https://api.syntext.dev/v1/projects/$PROJECT_ID/openapi \
-H "Authorization: Bearer $SYNTEXT_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"spec\": $(cat openapi.json) }"
Generated endpoint pages appear under your API reference tab after the next build.
What Gets Rendered
Each generated endpoint page includes the method badge, path, parameters, request/response schemas, code samples, and a try-it playground. The generator understands the following spec features:
Security schemes
The playground's Authorization field follows your spec's securitySchemes — it is never hardcoded to a Bearer token:
http/bearer(andoauth2,openIdConnect) — sendsAuthorization: Bearer <token>http/basic— sendsAuthorization: Basic <credentials>apiKey— the field is labeled with the scheme's ownname(e.g.Api-Key) and the key is sent as that header, or as a query parameter forin: queryschemes
Code samples in all seven languages, live snippet updates, and Try-it requests all use the resolved scheme. Endpoints without a security requirement show the default Bearer field.
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: Api-Key
You can also declare the auth scheme directly in syntext.json — useful when the spec's securitySchemes are missing or wrong. A top-level auth key applies to every spec (and to hand-authored REST pages); an auth key on an openapi entry overrides it for that spec only:
{
"auth": { "name": "Api-Key" },
"openapi": [
{ "path": "./openapi.yaml", "prefix": "api-reference" }
]
}
auth accepts name, plus optional label, in ("header" or "query"), and prefix (e.g. "Bearer "). Precedence: page auth frontmatter > per-entry auth > top-level auth > the spec's securitySchemes.
Header parameters
Parameters with in: header render in their own Headers section on the page and in the playground, where readers can fill them in — values are included in code snippets and sent with Try-it requests. The auth header itself is excluded to avoid duplicating the Authorization field.
Headers the spec doesn't declare can be added from syntext.json — a top-level headers array applies to every spec, and a headers array on an openapi entry overrides it for that spec:
{
"headers": [
{ "name": "X-Idempotency-Key", "required": true, "description": "Unique key to safely retry requests" }
]
}
Spec-declared parameters win on name collisions, and a config header matching the auth header name is dropped.
Markdown descriptions
Operation description fields render as full Markdown — headings, bullet lists, and multiple paragraphs all work. The first paragraph (flattened to one line) becomes the page's meta description.
paths:
/accounts:
post:
summary: Create account
description: |
Creates a new account for the authenticated user.
## Idempotency
Pass an `Idempotency-Key` header to safely retry:
- Keys expire after 24 hours
- Reusing a key returns the original response
Nested schemas, enums, and variants
Schema properties render recursively up to 4 levels deep. Nested object properties appear in an expandable "properties" section. Two schema shapes get special treatment:
enum— allowed values render as a "Possible values" list under the field descriptiononeOf/anyOf— each variant renders in its own expandable block, labeled by the variant schema'stitle(falling back to its type, then "Variant 1", "Variant 2", …)
Multiple named examples
Every entry in a request or response content.examples map renders as its own code block, titled with the example's name:
content:
application/json:
examples:
Personal account:
value: { "type": "personal", "email": "jo@example.com" }
Business account:
value: { "type": "business", "taxId": "DE123456789" }
A single example (singular) renders as one block titled "Request" or "Response".
Request code samples
The right-hand request panel auto-generates snippets in seven languages — cURL, Python, JavaScript, PHP, Go, Java, and Ruby — from the operation's method, path, and server URL. If the spec provides a request body example, the snippets use that real payload instead of placeholder values.
Multiple base URLs (production + sandbox)
When more than one base URL is declared, the API playground shows a base-URL selector, and code snippets plus Try-it requests follow whichever server the reader picks. The simplest way is a top-level baseUrls key in syntext.json — it applies to every spec and to hand-authored REST pages:
{
"baseUrls": [
{ "url": "https://api.example.com", "description": "Production" },
{ "url": "https://sandbox.example.com", "description": "Sandbox" }
]
}
Plain strings work too: "baseUrls": ["https://api.example.com", "https://sandbox.example.com"].
Alternatively, declare servers in the spec itself:
servers:
- url: https://api.example.com
description: Production
- url: https://sandbox.example.com
description: Sandbox
Or, if one spec needs different hosts than the rest of the site, set servers on that openapi entry:
{
"openapi": [
{
"path": "./specs/users.yaml",
"prefix": "api-reference/users",
"servers": [
{ "url": "https://users.example.com", "description": "Production" },
{ "url": "https://users-sandbox.example.com", "description": "Sandbox" }
]
}
]
}
Precedence: per-entry servers > top-level baseUrls > the spec's own servers list. The first base URL is the default; when only one is declared, no selector renders.
Response status dropdown with per-status messages
The response panel groups examples by status code in a dropdown (200, 400, 404, …). When a response example has a name in the spec (a key in content.examples), that name appears as a label in the panel header for the selected status — e.g. selecting 404 shows "CustomerNotFound" next to the copy button. If the spec provides no example name, no label is shown. Multiple named examples for the same status each get their own dropdown entry (404 · CustomerNotFound, 404 · KycNotFound).
Statuses without an authored example still appear in the dropdown: a skeleton example is synthesized from the response schema and merged alongside your authored ones. Authoring an example for one status (say 400) never hides the synthesized examples for the others — entries are sorted by status code, authored examples always win for their own status.
Deprecated operations
Operations with deprecated: true render a "Deprecated" badge next to the page title (and next to the endpoint in the overview list). The generated page's frontmatter also carries deprecated: true, so you can detect deprecated pages programmatically.
Sidebar group labels
Endpoints are grouped in the sidebar by their spec tags. The group label is chosen as follows:
- If the tag is declared in the spec's top-level
tagsarray with anx-displayName, that display name is used - Otherwise, a declared tag's
nameis used verbatim — including its casing - Tags that appear only on operations (not declared at the top level) get a humanized label:
risk-scoringbecomes "Risk Scoring", and common acronyms are fully uppercased — KYC, FX, API, SDK, URL, ID, S2S, KYB, 2FA, SSO, AML, IBAN, ACH, SEPA (sokycbecomes "KYC", not "Kyc")
Humanizing only applies to all-lowercase tag names. If you want a specific label for a lowercase tag, declare it in the top-level tags array with an x-displayName.
tags:
- name: accounts
x-displayName: Account Management # sidebar shows "Account Management"
- name: Payments # declared — shown verbatim as "Payments"
# operations tagged `kyc` without a declaration → shown as "KYC"
Auto-Sync on Release
Re-push the spec whenever your API ships. In GitHub Actions:
name: Sync API docs
on:
push:
branches: [main]
paths: ['openapi.json']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Push spec to Syntext
run: |
curl -fsS -X POST https://api.syntext.dev/v1/projects/${{ vars.SYNTEXT_PROJECT_ID }}/openapi \
-H "Authorization: Bearer ${{ secrets.SYNTEXT_API_KEY }}" \
-H "Content-Type: application/json" \
-d "{\"spec\": $(cat openapi.json)}"
Inspecting What's Imported
# List imported specs
curl https://api.syntext.dev/v1/projects/$PROJECT_ID/openapi/specs \
-H "Authorization: Bearer $SYNTEXT_API_KEY"
# List generated endpoints
curl https://api.syntext.dev/v1/projects/$PROJECT_ID/openapi/endpoints
To remove a spec and its generated pages: DELETE /v1/projects/{projectId}/openapi.
Combining with Hand-Written Content
Generated pages cover the what; write guides for the why. Link generated endpoint pages from your guides, and use annotation coverage to make sure code-level docs keep pace too.
GraphQL, gRPC, and AsyncAPI schemas are also supported via the same import pattern (/graphql, /grpc, /asyncapi endpoints).