All guides
Integration

Auto-sync OpenAPI Docs from GitHub Actions

Keep hosted API docs in lockstep with main. Wire a GitHub Action that uploads your OpenAPI spec to Outworx Docs on every push — no redeploys, no manual updates.

Abdalla Emad· Founder, Outworx DocsApril 24, 20265 min read

Docs drift. You ship a new endpoint, your OpenAPI spec updates in the repo, but the hosted docs still show last week's schema. Customers file "your docs are wrong" tickets. You swear you'll set up a cron. You don't.

Outworx Docs solves this two ways. The easiest is spec URL auto-sync — point us at a URL and we re-fetch on a schedule. The more robust way is a GitHub Action that pushes the spec to us on every merge to main, so docs update the instant your API does.

This guide covers both, with the GitHub Action setup at the center.

Option 1 — Spec URL auto-sync (the 30-second option)

If your API serves the OpenAPI JSON at a public URL (e.g. https://api.yourcompany.com/openapi.json), you don't need GitHub Actions at all.

  1. Open your project's API Spec tab on Outworx.
  2. Click New versionFrom URL.
  3. Paste the URL and pick a sync interval: hourly, daily, or weekly.
  4. Done.

Every interval tick, Outworx fetches the URL, parses it, and bumps the version. Your hosted docs reflect the latest spec without any CI pipeline.

When this isn't enough: if your OpenAPI spec lives only in a git repo (never served publicly), or you want zero-lag updates the moment a PR merges, use the GitHub Action approach below.

Option 2 — GitHub Actions (zero-lag, push-based)

The flow:

  1. You merge a PR to main that changes your OpenAPI spec
  2. A GitHub Action fires on push
  3. It POSTs the spec file to Outworx's upload API
  4. Your hosted docs update within seconds

Step 1: Get your project upload token

Open your Outworx Docs dashboard, click into your project, and go to the Settings tab. Scroll down past Project Details to the API access — spec upload card and click Generate upload token. You'll get a token that looks like otwx_... and is scoped to spec upload on this one project (nothing else — it can't read chats, analytics, or MCP). The same card shows pre-filled curl and GitHub Actions snippets with your token already dropped in, so you can copy-paste straight into your workflow file.

Copy the token. If you miss it, you can click the eye icon to reveal it again later, or rotate to get a new one.

Add it to your GitHub repo as a secret: Settings → Secrets and variables → Actions → New repository secret.

  • Name: OUTWORX_TOKEN
  • Value: your otwx_... token

Step 2: Drop in the workflow

The fastest path is the official abdallaemadeldin/upload-spec-action@v1. Three lines of YAML, no curl to maintain, no exit-code translation, no "did I quote the JSON right?" debugging at 2am.

Create .github/workflows/sync-docs.yml:

name: Sync API docs

on:
  push:
    branches: [main]
    paths:
      - "openapi.yaml"
      - "openapi.json"
      # Uncomment if your spec is generated from source — pair this
      # with the "Regenerate spec" step below so a code change actually
      # produces a fresh openapi.yaml before the upload runs.
      # - "src/**/*.ts"

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Uncomment if your API auto-generates its spec from code.
      # - name: Regenerate spec
      #   run: npm ci && npm run generate:openapi

      - uses: abdallaemadeldin/upload-spec-action@v1
        with:
          token: ${{ secrets.OUTWORX_TOKEN }}
          project: <YOUR_SLUG>
          spec: ./openapi.yaml

Replace <YOUR_SLUG> with your project slug (the subdomain before .docs.outworx.io). Without a version input, the push updates your project's current default version in place — the behavior most teams want for a main-branch sync.

Where does ${{ secrets.OUTWORX_TOKEN }} come from? It's the repo secret you added in Step 1. GitHub substitutes it at run time so your actual token is never in the YAML (or your git history). The action's exit codes mirror the CLI: 1 for user errors (won't retry on its own), 2 for transient API errors (safe to re-run).

Step 3: Optional — version your docs by git tag

If you want each git tag to become a new docs version rather than overwriting the default, pass the version input:

- uses: abdallaemadeldin/upload-spec-action@v1
  with:
    token: ${{ secrets.OUTWORX_TOKEN }}
    project: <YOUR_SLUG>
    spec: ./openapi.yaml
    version: ${{ github.ref_name }}  # e.g. v1.2.3 → version label "v1.2.3"

Step 4: Optional — preview deploys for every PR

Pair the action with pull_request events to publish a per-PR preview at /preview/<pr-number>:

on:
  pull_request:
    paths: ["openapi.yaml"]

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: abdallaemadeldin/upload-spec-action@v1
        with:
          token: ${{ secrets.OUTWORX_TOKEN }}
          project: <YOUR_SLUG>
          spec: ./openapi.yaml
          preview: true
          pr: ${{ github.event.pull_request.number }}
          commit: ${{ github.event.pull_request.head.sha }}
          branch: ${{ github.event.pull_request.head.ref }}

Preview pushes are filtered out of your public docs picker, never become the default version, and auto-prune 14 days after the PR closes. They also require a personal access token (otwx_pat_*) — per-project upload tokens don't carry the version:write scope previews need. Mint one in Settings → API tokens.

Prefer raw curl?

You don't need the action. The same upload endpoint is reachable via curl:

- name: Upload to Outworx Docs
  env:
    OUTWORX_TOKEN: ${{ secrets.OUTWORX_TOKEN }}
  run: |
    curl -sS --fail-with-body -X POST \
      "https://docs.outworx.io/api/projects/<YOUR_SLUG>/upload" \
      -H "Authorization: Bearer $OUTWORX_TOKEN" \
      -H "Content-Type: application/yaml" \
      --data-binary "@openapi.yaml"

Trigger on tag pushes instead:

on:
  push:
    tags:
      - "v*.*.*"

Now every release tag becomes a permanent docs version visitors can switch between. The latest tag becomes the default.

Verifying it works

After your first merge, check the Actions tab on GitHub — you should see "Sync API docs" green. On the Outworx side, go to API Spec → you should see a new version with Last synced: a few seconds ago.

If the action fails, the response body is JSON with a machine-readable code and a human error:

  • MISSING_TOKEN / INVALID_TOKEN — the Authorization header is absent or doesn't match. Re-copy from the Settings tab.
  • UPGRADE_REQUIRED — you're on a plan that caps the number of active versions (Free: 1, Pro: 3). The body includes your plan + limit + an upgrade URL. Either drop the ?version= query param to update your default version in place, or upgrade.
  • SPEC_TOO_LARGE — bigger than 50MB (or whatever your hosting platform caps request bodies at — Vercel serverless is 4.5MB on Hobby/Pro). For truly huge specs, point a spec URL at the file and let auto-sync fetch it server-side, which has no size cap.
  • UNKNOWN_FORMAT — the body wasn't recognized as OpenAPI/Swagger/GraphQL. Often a curl pathing issue; check --data-binary '@path/to/openapi.yaml'.
  • PARSE_FAILED — the spec was malformed. The message explains which line / field.
  • LABEL_CONFLICT — race between two pushes at the same label. Retry.

Why this pattern works so well

API specs are the source of truth for your contract with developers. When docs lag behind the spec, you pay in confusion, support tickets, and lost trust. Push-based sync flips the dynamic — you can't ship an API change that doesn't update the docs, because they're literally the same commit.

Combine this with Outworx's MCP server and your users' AI assistants will always have the latest spec too. Ship once, everything else catches up automatically.

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.