All guidesIntegration

Outworx REST API — Manage Projects from CI

Drive every Outworx Docs action from a script: create projects, push spec versions, promote a default. Personal access tokens, copy-paste curl, and the full OpenAPI spec.

Every action you can take in the Outworx dashboard — creating projects, pushing new spec versions, promoting a default, soft-deactivating an old version — is also reachable from a curl one-liner. This guide walks through authentication, the request/response shape, the flows that matter most for CI pipelines, and where to find the OpenAPI spec.

Why use the API

The dashboard is for humans. The REST API is for everything else: pre-merge spec validation in CI, scripted onboarding for a new microservice, internal tooling that mirrors a project tree from GitHub into Outworx, or anything else where clicking through a UI would be a chore.

Two surfaces cover roughly the same ground but with different boundaries:

  • Per-project upload tokens (the older otwx_ token from Settings → API access) — scoped to spec upload on one project. Best when CI only needs to push a spec.
  • Personal access tokens (this guide) — account-scoped, do anything you can do via the dashboard, work across all your projects with one token. Best for everything else.

Generate a personal access token

  1. Open Settings in the dashboard.
  2. Find the API tokens card.
  3. Click New token, name it (ci-pipeline, local-dev, etc.), optionally set an expiry.
  4. Copy the plaintext that appears — we don't store it and we won't show it again. The dashboard list afterwards only shows the prefix (otwx_pat_…) so you can recognise tokens at a glance, plus revoke them if a token leaks.

Tokens act as the user who created them. A token belonging to a Pro-plan user has Pro limits; downgrade the user and the token's allowance drops with it.

The shape of every request

curl -H "Authorization: Bearer otwx_pat_..." \
  https://docs.outworx.io/api/v1/projects

That's it. JSON in, JSON out. There's no separate API host — docs.outworx.io/api/v1/* is the canonical surface so you don't need a second DNS record.

Success envelopes

Single-resource endpoints return the resource directly:

{
  "id": "f1b7697f-...",
  "name": "Payments API",
  "slug": "payments-api",
  "...": "..."
}

List endpoints wrap data in { data, pagination }:

{
  "data": [ /* projects */ ],
  "pagination": { "total": 47, "limit": 50, "offset": 0 }
}

Error envelope

Every error response uses the same shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "`name` is required.",
    "details": { "...": "..." }
  }
}

code is the stable machine-readable identifier — pattern-match on it. message is human-readable and may change. The full list of codes (MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, NOT_FOUND, VALIDATION_ERROR, CONFLICT, PAYLOAD_TOO_LARGE, RATE_LIMITED, PLAN_LIMIT, UNSUPPORTED_FORMAT) lives in the OpenAPI spec.

We don't differentiate "doesn't exist" from "not yours" — both return 404 NOT_FOUND. A 403 there would leak slug existence to anyone with a token.

Common flows

Create a project + push a spec from a fresh CI job

# 1. Create the project. Slug auto-generates from name when omitted.
curl -X POST -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Payments API","description":"Public Payments API"}' \
  https://docs.outworx.io/api/v1/projects

# 2. Push the OpenAPI spec as the first version. Body IS the spec —
#    NOT a JSON envelope. The first active version is auto-marked
#    as the default.
curl -X POST -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary '@openapi.json' \
  'https://docs.outworx.io/api/v1/projects/payments-api/versions?label=v1'

That's the whole bootstrap. Two calls, no dashboard.

Promote a new version on every release

The single-default invariant is enforced server-side: setting is_default: true on a non-default version automatically demotes whatever was the default before. You don't need two PATCH calls.

# Push v2 alongside v1, leaving v1 as the default visitors see.
curl -X POST -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary '@openapi.json' \
  'https://docs.outworx.io/api/v1/projects/payments-api/versions?label=v2'

# Run your QA. When ready, flip the default in one call.
curl -X PATCH -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_default":true}' \
  https://docs.outworx.io/api/v1/projects/payments-api/versions/v2

If you instead try to demote the default directly (is_default: false on the current default), the API rejects it with 400 VALIDATION_ERROR — the project would have no default, and the docs page would 404 for visitors. Promote a replacement; we'll demote the old one for you.

DELETE on the default is also blocked with 409 CONFLICT for the same reason. Promote first, delete second.

Soft-deactivate a version without deleting it

is_active: false hides the version from the docs page but keeps the row, the spec content, and the analytics intact. Useful for pulling back a release that broke something without losing the audit trail.

curl -X PATCH -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_active":false}' \
  https://docs.outworx.io/api/v1/projects/payments-api/versions/v2

Reactivate by sending {"is_active":true}.

PR preview deploys

Push a spec with ?preview=true and the new version is treated as PR-scoped: filtered out of the public version picker, kept out of the sitemap, marked noindex, and given a 14-day TTL. Reviewers click a link from the PR comment and see live docs for the proposed change without it bleeding into your release version history.

Previews are Pro+ (free plans get 402 PLAN_LIMIT) with their own per-plan cap (maxPreviewsPerProject: 5 on Pro, 20 on Business). They don't count against your regular maxVersions cap.

# CI on PR open / push:
curl -X POST -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary '@openapi.json' \
  "https://docs.outworx.io/api/v1/projects/$SLUG/versions?label=pr-${PR_NUMBER}&preview=true&pr=${PR_NUMBER}&commit=${COMMIT_SHA}&branch=${BRANCH_NAME}"

The response includes the version row with is_preview: true and a preview object holding the PR number, commit SHA, branch, and expires_at. The preview is reachable at:

https://{slug}.docs.outworx.io/pr-{PR_NUMBER}/

(or your custom domain). Re-pushing the same label as new commits land on the PR updates the existing row (200 OK) instead of returning 409 — designed for CI to push on every commit. The TTL rolls forward each time, so an active PR keeps its preview indefinitely.

GitHub Action

name: Outworx — PR preview docs

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
  pull_request_target:
    types: [closed]

jobs:
  push-preview:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Push preview spec
        env:
          OUTWORX_TOKEN: ${{ secrets.OUTWORX_TOKEN }}
          SLUG: my-project
          PR: ${{ github.event.pull_request.number }}
          SHA: ${{ github.event.pull_request.head.sha }}
          BRANCH: ${{ github.event.pull_request.head.ref }}
        run: |
          RESP=$(curl -sS --fail-with-body -X POST \
            -H "Authorization: Bearer $OUTWORX_TOKEN" \
            -H 'Content-Type: application/json' \
            --data-binary '@openapi.json' \
            "https://docs.outworx.io/api/v1/projects/$SLUG/versions?label=pr-$PR&preview=true&pr=$PR&commit=$SHA&branch=$BRANCH")
          echo "PREVIEW_URL=https://$SLUG.docs.outworx.io/pr-$PR/" >> $GITHUB_ENV
      - name: Sticky PR comment
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          header: outworx-preview
          message: |
            📘 **Docs preview**: ${{ env.PREVIEW_URL }}
            Updated on every push. Auto-cleaned 14 days after the PR closes.

  cleanup-preview:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Delete preview version
        env:
          OUTWORX_TOKEN: ${{ secrets.OUTWORX_TOKEN }}
          SLUG: my-project
          PR: ${{ github.event.pull_request.number }}
        run: |
          curl -sS -X DELETE \
            -H "Authorization: Bearer $OUTWORX_TOKEN" \
            "https://docs.outworx.io/api/v1/projects/$SLUG/versions/pr-$PR" || true

Two safety nets, in order of preference:

  1. Explicit DELETE on PR close — the cleanup-preview job above. Runs as soon as the PR closes (merged or not).
  2. 14-day TTL via cron — if the close webhook never fires (CI crashed, repo was deleted, etc), the prune cron sweeps stale previews. The expires_at field is rolled forward on every preview re-push, so an active PR keeps its preview for as long as it sees activity.

The dashboard hides preview rows from the version picker on the public docs page. To list them programmatically (for your own preview-management tooling), pass ?include_previews=true to the GET endpoint:

curl -H "Authorization: Bearer $OUTWORX_TOKEN" \
  "https://docs.outworx.io/api/v1/projects/$SLUG/versions?include_previews=true"

Make a project public + assign a custom domain

curl -X PATCH -H "Authorization: Bearer $OUTWORX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_public":true,"custom_domain":"docs.example.com"}' \
  https://docs.outworx.io/api/v1/projects/payments-api

Custom domain setting is gated on the Business plan — Free and Pro tiers get 402 PLAN_LIMIT when they try to set one. Clearing a domain (passing null) is allowed on every plan, mirroring what the subscription-sync would do automatically on a Business → Pro downgrade.

Setting custom_domain over the API does not auto-verify — verification still goes through the dashboard's DNS check + Vercel registration step. The API exposes the field for setup parity (so you can prepare a migration without clicking) but the verification handshake stays a dashboard step.

Plan limits

Every endpoint enforces the owning user's plan limits. Hit one and you get 402 PLAN_LIMIT with a message that points at exactly which limit you tripped:

  • Max projects per account
  • Max active versions per project (Free: 1, Pro: 3, Business: unlimited)

The body looks like:

{
  "error": {
    "code": "PLAN_LIMIT",
    "message": "Your Free plan allows 1 active version per project."
  }
}

The spec

The full OpenAPI 3.1 spec is at:

https://docs.outworx.io/api/v1/openapi.json

That's the source of truth — every example here, every schema, every error code. Drop the URL into Postman / Insomnia / Stoplight to get an importable collection. Drop it into openapi-typescript to generate a typed client. Or if you want a full rendered reference page, point an Outworx Docs project at it (yes, you can host the docs for the Outworx API through Outworx; we do).

Status & rate limits

Every authenticated request bumps the token's last_used_at so dormant tokens stand out in the dashboard list. We emit standard rate-limit headers when applicable; expect 429 RATE_LIMITED if you blast through a burst — the body's message includes the retry-after hint.

For automation that needs to run continuously, the comfortable budget is a few requests per second per token. If you need more, contact us before you start hammering — we'd rather raise a limit than have you discover it the hard way.

What's not in v1

  • Mock server config — overrides, chaos, auth enforcement: dashboard-only for now.
  • Theme tokens — color palette + theme-mode lock: dashboard-only.
  • Collaborators — invite / revoke: dashboard-only (RBAC lands first).
  • Webhooks (inbound + outbound) — spec drift notifications, inbox config: dashboard-only.

These are queued for v2. Priority loosely tracks customer pull — if you need one of them programmatically, tell us and we'll likely move it up.

Related guides

Ship your API docs in under a minute.

Upload your OpenAPI, Swagger, or GraphQL spec and get beautiful hosted docs with AI chat and a per-project MCP server — free forever for 2 projects.