All guides
Integration

PR preview deploys — every pull request gets its own docs URL

Push specs from CI with `--preview` and Outworx spins up a throwaway version with a banner showing PR / branch / commit. 14-day rolling TTL, hidden from public version listings, three lines of GitHub Action.

Reviewing API changes used to mean checking out the branch, regenerating the docs, hosting them somewhere, and praying nobody else opens a different PR before yours merges. PR preview deploys collapse that into a single workflow line: push the spec from CI with --preview, your reviewer clicks a link, the preview expires when the PR closes.

outworx push openapi.yaml \
  --project payments-api \
  --preview \
  --pr 247 \
  --commit $GITHUB_SHA \
  --branch $GITHUB_HEAD_REF

That's the whole flow. The result is a hosted preview at /<slug>/preview/<pr> with an amber PREVIEW banner showing the PR number, branch, and commit SHA — so reviewers can't accidentally cite a preview URL as the live docs. This guide covers when to use it, how to wire it into GitHub Actions, the lifecycle semantics, and the gotchas.

What a preview deploy actually is

A preview is a regular project_versions row with is_preview = true plus three pieces of PR metadata: the PR number, the commit SHA, and the branch name. Outworx treats it differently than your release versions in five specific ways:

  • Hidden from listings. GET /api/v1/projects/<slug>/versions excludes preview rows by default. Pass ?include_previews=true for admin tooling. The public docs version dropdown also hides them — your customers see your shipped versions, your reviewers see the PR's version.
  • Never the default. Even if you push a preview as the only version, it won't get promoted. The single-default invariant only applies to non-preview versions.
  • Separate plan cap. Preview versions count against maxPreviewsPerProject (5 on Pro, 20 on Business), not the regular maxVersions cap. Previews can't crowd out release versions.
  • 14-day rolling TTL. Each push pushes the expiry forward by 14 days. A slow review keeps its preview; an abandoned PR auto-cleans up. Pruning runs in pg_cron at 03:23 UTC.
  • Re-pushes update the same row. Push the same preview label again on a later commit and the existing row updates (TTL rolls forward, embeddings + cached MCP types invalidate). No 409, no drift previews piling up.

Wiring it into GitHub Actions

The smallest functional workflow:

name: Docs preview
on:
  pull_request:
    paths:
      - 'openapi.yaml'

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: outworx/upload-spec-action@v1
        with:
          token: ${{ secrets.OUTWORX_TOKEN }}
          project: payments-api
          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 }}

Three things matter here:

  1. paths filter keeps the workflow from re-running when unrelated files change. Without it, every commit on the PR republishes — fine, but wasteful.
  2. preview: true is the toggle. Without it, the action does a regular release-version push.
  3. The token must be a personal access token (otwx_pat_*). Project upload tokens (otwx_*) don't support preview semantics — the CLI rejects the combo upfront with a clear error message.

If you also want a comment on the PR linking to the preview URL, add a follow-up step:

- name: Comment with preview URL
  uses: actions/github-script@v7
  with:
    script: |
      const url = `https://docs.outworx.io/payments-api/preview/${context.issue.number}`;
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `📚 Docs preview: ${url}`,
      });

Other CI providers

The CLI works anywhere Node 20+ runs. The flag shape is identical; only the env var lookup changes:

# GitLab CI
outworx push openapi.yaml --project payments-api --preview \
  --pr "$CI_MERGE_REQUEST_IID" \
  --commit "$CI_COMMIT_SHA" \
  --branch "$CI_COMMIT_REF_NAME"

# CircleCI
outworx push openapi.yaml --project payments-api --preview \
  --pr "$CIRCLE_PULL_REQUEST" \
  --commit "$CIRCLE_SHA1" \
  --branch "$CIRCLE_BRANCH"

# Buildkite
outworx push openapi.yaml --project payments-api --preview \
  --pr "$BUILDKITE_PULL_REQUEST" \
  --commit "$BUILDKITE_COMMIT" \
  --branch "$BUILDKITE_BRANCH"

Plan limits

The preview cap is a separate dial from regular versions. Pro projects can hold up to 5 active previews; Business up to 20. CI re-pushing the SAME preview label updates the existing row, so the cap only fires when CI tries to open a NEW preview while at the limit. If you hit it, close a stale PR (or delete its preview row from the dashboard) and re-run.

The 14-day TTL is rolling, not absolute — every push pushes the expiry forward. So a long-running PR keeps its preview as long as commits keep landing. Once the PR closes (merge or cancel) and no further pushes come in, the preview expires on its own and the prune cron sweeps it.

What happens at merge

Merging the PR doesn't automatically promote the preview. The preview keeps its preview status — you push a regular release version (without --preview) on the merge commit if you want it to become the default:

on:
  push:
    branches: [main]
    paths:
      - 'openapi.yaml'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: outworx/upload-spec-action@v1
        with:
          token: ${{ secrets.OUTWORX_TOKEN }}
          project: payments-api
          spec: openapi.yaml
          # No preview flag → regular release push

This split is intentional. A merged PR isn't the same as a release — the merge commit might land alongside other unrelated work, the docs version might lag the code release by a day, etc. Keeping the release push as a separate workflow file gives you that control.

Gotchas

  • Branch name length. GitHub branches max out around 244 chars; the preview metadata caps a hair higher (256). Truncate or skip the --branch flag for unusual setups.
  • Commit SHA format. The CLI accepts 7-64 hex chars. GitHub's $GITHUB_SHA is the full 40-char SHA. CI variables that give you a short SHA ($CIRCLE_SHA1 is full, $BITBUCKET_COMMIT is short) both work.
  • Preview URL in the banner. The banner links back to the GitHub PR thread when the metadata is present. If you skip the --branch or --commit flags, the banner still renders but the link target is the docs preview itself.
  • Embeddings + MCP types. Each push invalidates the version's embedding index and any cached generated TypeScript types. If you have an MCP client connected to the preview version, it'll fetch fresh types on the next call.
  • Audit log entry. Every preview push writes a PREVIEW_CREATED row to the audit log on Business plans, with the PR / commit / branch in metadata. Useful for forensic traces ("which CI run pushed this preview?") via the metadata.token_id field.

When to reach for it vs alternatives

PR previews are the right tool when reviewers need to see the rendered docs, not just the spec diff. If your team's review flow is "look at the YAML changes, approve" you can probably get by with Spec Diff alone — it posts a sticky comment classifying every change as breaking / additive / cosmetic.

PR previews are most valuable when:

  • You ship customer-facing API docs and want product / support / partners to review the rendered output
  • Your changes touch markdown overrides or per-endpoint customizations (those don't show up in a spec diff)
  • You want a stable URL to share with an enterprise prospect mid-review

You can run both in parallel. The combination — diff comment + clickable preview URL — is what most production-grade API teams ship.

Now wire 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.