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
- Open Settings in the dashboard.
- Find the API tokens card.
- Click New token, name it (
ci-pipeline,local-dev, etc.), optionally set an expiry. - 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/projectsThat'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/v2If 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/v2Reactivate 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" || trueTwo safety nets, in order of preference:
- Explicit DELETE on PR close — the
cleanup-previewjob above. Runs as soon as the PR closes (merged or not). - 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_atfield 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-apiCustom 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.jsonThat'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
Free Hosted Mock Server for Every API Spec
Every OpenAPI, Swagger, and GraphQL spec on Outworx Docs gets a free hosted mock URL on upload — with stateful overrides, templating, validation, and chaos.
Test Webhooks: Capture, Sign, and Replay
A unique capture URL catches webhooks in real-time. Send-test, replay, and an HMAC verifier for Stripe, GitHub, Shopify, Slack + 6 more — all browser-side.
Generate TypeScript Types from OpenAPI in Seconds
Turn any OpenAPI or Swagger spec into clean TypeScript types via Outworx Docs' MCP server — no CLI, no codegen configs. Generate live, cached, and current.
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.