Content Sync
ProEnterprisePush documentation content to Firstrun and automatically extract interactive steps.
Interactive firstrun guide available
Step-by-step with copy-to-clipboard and progress tracking.
Overview
Push raw documentation (markdown, text, or a URL) and get a response back instantly. Fire and forget — the API accepts your content and returns a wizard_id immediately. Step extraction runs in the background. Poll the status endpoint if you need to know when it's done.
Prerequisite: Content Sync is available on the Pro or Enterprise plan. Create an API key first (admin or owner role required).
Sync Content
POST /api/v1/sync
Send raw content or a URL. The response returns immediately with a wizard_id and a status telling you what happened.
Option 1: Send a URL (simplest)
curl -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer fr_key_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "setup-guide",
"url": "https://docs.acme.dev/setup"
}'Option 2: Send raw content
curl -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer fr_key_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "setup-guide",
"content": "# Getting Started\n\nInstall with npm install..."
}'Option 3: Upload a file
POST /api/v1/sync/upload — multipart form data. Supports PDF, HTML, Markdown, and plain text (max 5 MB).
curl -X POST https://api.firstrun.dev/api/v1/sync/upload \
-H "Authorization: Bearer fr_key_..." \
-F "external_id=setup-guide" \
-F "file=@docs/setup.pdf" \
-F "auto_publish=true"Response (all three options)
{
"wizard_id": "d4f5a6b7-...",
"external_id": "setup-guide",
"status": "created",
"wizard_status": "generating",
"message": "Wizard created, extracting steps."
}Response statuses
| status | wizard_status | What happened |
|---|---|---|
created | generating | New wizard created. Pipeline is running — poll for completion. |
updated | generating | Content changed since last sync. Steps are being re-extracted — poll for completion. |
unchanged | draft / published | Content identical to last sync (SHA-256 match). Nothing happens — no need to poll. |
Most CI runs will hit unchanged and return instantly. Only actual content changes trigger the pipeline.
Request Body
| Field | Type | Description |
|---|---|---|
external_id | string | Required. A name you choose to identify this wizard. You make this up — pick anything descriptive like "setup", "deploy-guide", or "docs/api/auth". In CI, use the filename (e.g. docs/setup from docs/setup.md). Same external_id always maps to the same wizard — first call creates, subsequent calls update. You never get duplicates, and you never need to store a wizard ID on your side. Max 255 characters. |
content | string | Raw markdown or text (min 50 chars, max 1M chars). Provide exactly one of content or url. |
url | string | URL to scrape. Provide exactly one of content or url. |
auto_publish | boolean | Publish the wizard automatically after extraction succeeds. Default: false. |
visibility | string | "public" or "internal". Only used when auto_publish is true. Default: "public". |
Poll for Completion
GET /api/v1/sync/{wizard_id}
When status is created or updated, the pipeline is running in the background. Poll until status is no longer generating:
# Poll until extraction completes
while true; do
RESP=$(curl -s https://api.firstrun.dev/api/v1/sync/{wizard_id} \
-H "Authorization: Bearer fr_key_...")
STATUS=$(echo "$RESP" | jq -r '.status')
if [ "$STATUS" != "generating" ]; then
echo "Done: status=$STATUS, steps=$(echo "$RESP" | jq '.step_count')"
break
fi
echo "Still generating (stage: $(echo "$RESP" | jq -r '.pipeline_stage'))..."
sleep 5
doneResponse
{
"wizard_id": "d4f5a6b7-...",
"external_id": "setup-guide",
"status": "draft",
"pipeline_stage": null,
"title": "Getting Started",
"step_count": 5,
"error_message": null
}Status values
| status | Meaning |
|---|---|
generating | Pipeline is running. Check pipeline_stage for progress (scraping → cleaning → extracting → enriching). Keep polling. |
draft | Extraction complete. Steps are ready. Wizard is unpublished. |
published | Extraction complete and auto-published. |
failed | Pipeline failed. Check error_message for details. |
List Wizards
GET /api/v1/sync/wizards
List all wizards in your organization with their external_id mappings. Useful for discovering what's already been synced.
curl https://api.firstrun.dev/api/v1/sync/wizards \
-H "Authorization: Bearer fr_key_..."How It Works
You pick the external_id, Firstrun handles the rest
The external_id is a name you choose — it can be anything (a slug, a file path, a label). Firstrun uses it to link your content to a wizard. First call with a given external_id creates a new wizard. Every subsequent call with the same external_id updates that same wizard. You never get duplicates, and you never need to store a wizard ID on your side.
Content hash skips unnecessary work
Firstrun computes a SHA-256 hash of the content (or URL) you send. If the hash matches the previous sync, the response is instant "unchanged" with no pipeline run. This means you can sync on every CI build without waste — only actual content changes trigger extraction.
Concurrent sync protection
If a wizard is currently being generated (pipeline running), a new sync for the same external_id returns 409 with the wizard_id and a suggested retry_after delay. Wait for the current extraction to finish before syncing again.
# 409 response body:
{
"detail": {
"error": "wizard_generating",
"message": "Wizard is currently generating. Wait for completion before syncing again.",
"wizard_id": "d4f5a6b7-...",
"retry_after": 30
}
}Version snapshots
When content changes and steps are re-extracted, the previous version is automatically saved. Your analytics history is never lost.
CI/CD Examples
The recommended pattern is to sync all your doc files on every push. Unchanged files return instantly (content hash match), so only files that actually changed trigger extraction. Engineers never need to touch the CI config when adding or editing docs.
GitHub Actions — sync all docs (recommended)
Every push to main syncs every file in docs/. Unchanged files are instant no-ops. New files automatically create new wizards.
name: Sync docs to Firstrun
on:
push:
branches: [main]
paths: [docs/**]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync all docs
run: |
for file in docs/*.md; do
SLUG=$(basename "$file" .md)
echo "Syncing $SLUG..."
RESP=$(curl -sf -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer ${{ secrets.FIRSTRUN_API_KEY }}" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"external_id": "docs/$SLUG",
"content": $(jq -Rs . < "$file"),
"auto_publish": true
}
EOF
)
echo " → $(echo "$RESP" | jq -r '.status')"
doneA typical run where one file changed out of twenty:
Syncing setup... → unchanged
Syncing quickstart... → unchanged
Syncing auth... → updated ← only this file triggers extraction
Syncing deploy... → unchanged
...GitLab CI
# .gitlab-ci.yml
sync-docs:
stage: deploy
script:
- |
for file in docs/*.md; do
SLUG=$(basename "$file" .md)
curl -sf -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer $FIRSTRUN_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"external_id": "docs/$SLUG",
"content": $(jq -Rs . < "$file"),
"auto_publish": true
}
EOF
done
rules:
- changes:
- docs/**Single URL (no checkout needed)
If you just want to sync one hosted page, you can point Firstrun at the URL directly:
curl -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer fr_key_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "setup-guide",
"url": "https://docs.acme.dev/setup",
"auto_publish": true
}'Limits & Best Practices
Wizard limit
Each organization has a wizard limit based on your plan. Each unique external_id creates one wizard. If you hit the limit, you'll get a 403. Delete unused wizards or upgrade your plan.
Large doc sets (first-time sync)
Each sync request extracts steps using AI, which takes 10–30 seconds per wizard. If you're syncing many files for the first time, add a short delay between requests to avoid overwhelming the pipeline:
for file in docs/*.md; do
SLUG=$(basename "$file" .md)
curl -sf -X POST https://api.firstrun.dev/api/v1/sync \
-H "Authorization: Bearer ${{ secrets.FIRSTRUN_API_KEY }}" \
-H "Content-Type: application/json" \
-d @- <<EOF
{
"external_id": "docs/$SLUG",
"content": $(jq -Rs . < "$file"),
"auto_publish": true
}
EOF
sleep 2 # Pace requests during initial import
doneAfter the initial import, subsequent runs are fast — only changed files trigger extraction, and unchanged files return instantly.
One file per wizard
Content Sync creates one wizard per external_id. If your docs are split across many small files, consider syncing only the pages that make sense as standalone step-by-step guides rather than every file in your repo.
Error Codes
| Status | Description |
|---|---|
401 | Missing or invalid API key. |
403 | Wizard limit reached. Delete a wizard or upgrade your plan. |
404 | Wizard not found (GET endpoints). |
409 | Wizard is currently generating. Response includes wizard_id and retry_after (seconds). Poll the status endpoint and retry after extraction completes. |
422 | Validation error: missing external_id, both url and content provided (or neither), or content too short. |