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.

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 (and oauth2, openIdConnect) — sends Authorization: Bearer <token>
  • http / basic — sends Authorization: Basic <credentials>
  • apiKey — the field is labeled with the scheme's own name (e.g. Api-Key) and the key is sent as that header, or as a query parameter for in: query schemes

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 description
  • oneOf / anyOf — each variant renders in its own expandable block, labeled by the variant schema's title (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.

Endpoints are grouped in the sidebar by their spec tags. The group label is chosen as follows:

  1. If the tag is declared in the spec's top-level tags array with an x-displayName, that display name is used
  2. Otherwise, a declared tag's name is used verbatim — including its casing
  3. Tags that appear only on operations (not declared at the top level) get a humanized label: risk-scoring becomes "Risk Scoring", and common acronyms are fully uppercased — KYC, FX, API, SDK, URL, ID, S2S, KYB, 2FA, SSO, AML, IBAN, ACH, SEPA (so kyc becomes "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).

Was this page helpful?