Why deploy hooks exist
The fastest way to catch a regression is to scan immediately after the deploy that caused it. Most websites don't — they scan on a weekly cron, get the bad news on Monday, and spend the week un-doing changes. Deploy hooks fix the loop.
A deploy hook is a single HTTP endpoint scoped to one of your registered domains. Hit it from your CI pipeline at the end of a deploy, we queue a scan, and (if you've set up a scan.completed or score.dropped webhook) we POST you back when it's done.
How to mint one
- Open Domains.
- Find the domain you want to monitor; click the chevron to expand its row.
- Click Mint deploy token. The plaintext token (
sxp_deploy_…) and the full pre-built curl URL appear in a yellow banner — copy both; we hash the token after. - Paste the URL into your CI's deploy-success step. That's the whole setup.
Why a separate token from the main API?
The CI surface is intentionally narrow — one operation (trigger a scan), scoped to one domain. Splitting it from the general-purpose API token means:
- Visually distinguishable:
sxp_deploy_…vssxp_live_…. A leak is easier to identify. - Per-domain rotation: rotating a deploy hook for one domain doesn't invalidate the others.
- Less powerful if leaked: a stolen deploy token can only burn scan quota on the one domain it's scoped to. A stolen API token with
scans:write+domains:writecan do more damage.
Triggering a scan
The hook URL has the token embedded:
POST https://seoxpert.io/api/webhooks/deploy/<domainId>?token=sxp_deploy_…
Optionally include trigger metadata in the JSON body — these fields are persisted to the resulting scan record so reports can later link back to the deploy that caused them:
curl -X POST \
"https://seoxpert.io/api/webhooks/deploy/<domainId>?token=sxp_deploy_…" \
-H "Content-Type: application/json" \
-d '{
"ref": "main",
"sha": "abc1234",
"environment": "production"
}'Successful response: { "ok": true, "scanId": "…", "triggeredBy": "deploy" }. The scan starts immediately; check status with an API token or wait for the scan.completed webhook if you set one up.
Rate limit: 1 scan / 5 min / domain
A chatty CI pipeline (multiple deploy stages, retry loops, blue-green) shouldn't burn your monthly scan quota in minutes. We rate-limit deploy-hook triggers to one scan per five minutes per domain. Subsequent calls inside that window get:
{
"ok": true,
"skipped": true,
"reason": "rate_limit",
"activeScanId": "<scan-currently-running>"
}The rate-limit gate fires before entitlement reservation, so a skipped call never moves your scan balance. You can fire the hook from every CI step without worrying about cost.
CI integration snippets
The dashboard hands you these same snippets pre-filled with your real URL when you mint a token (Settings → Domains → Mint deploy token). Copy whichever provider you use; the placeholder <YOUR_DEPLOY_HOOK_URL> below maps to the URL you got at mint time.
The recommended pattern for any CI is to store the URL as a secret(so it doesn't end up committed to a public repo) and fire the call as the LAST step of the deploy job — that way a failed build doesn't trigger a scan against a broken deploy.
curl · Run from any shell
# Trigger a Seoxpert scan after a deploy. # Rate-limited to 1 scan per 5 minutes per domain. curl -X POST "<YOUR_DEPLOY_HOOK_URL>" \ -H "User-Agent: my-ci/1.0"
GitHub Actions · .github/workflows/seoxpert.yml
name: Seoxpert post-deploy scan
on:
deployment_status:
types: [success]
jobs:
scan:
if: github.event.deployment_status.environment == 'production'
runs-on: ubuntu-latest
steps:
- name: Trigger Seoxpert scan
run: |
curl -X POST "${{ secrets.SEOXPERT_DEPLOY_HOOK }}" \
-H "User-Agent: github-actions/1.0"
env:
SEOXPERT_DEPLOY_HOOK: ${{ secrets.SEOXPERT_DEPLOY_HOOK }}
# Add the URL as a repo secret named SEOXPERT_DEPLOY_HOOK:
# <YOUR_DEPLOY_HOOK_URL>Vercel · package.json
{
"scripts": {
"// note": "Local 'npm run build' will also fire the scan. Use Vercel Integrations for a CI-only path.",
"build": "next build && curl -X POST \"<YOUR_DEPLOY_HOOK_URL>\" -H \"User-Agent: vercel-build/1.0\" || true"
}
}Netlify · netlify.toml
# Heads-up: local 'netlify build' will also fire the scan. # For a CI-only path use Netlify's Build hooks UI instead. [build] command = "next build && curl -X POST \"<YOUR_DEPLOY_HOOK_URL>\" -H \"User-Agent: netlify-build/1.0\" || true"
GitLab CI · .gitlab-ci.yml
seoxpert_scan:
stage: .post
image: curlimages/curl:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- curl -X POST "$SEOXPERT_DEPLOY_HOOK" -H "User-Agent: gitlab-ci/1.0"
# Add the URL as a CI/CD variable named SEOXPERT_DEPLOY_HOOK:
# <YOUR_DEPLOY_HOOK_URL>Don't see your CI? The curlsnippet runs from any shell. Wrap it in your provider's post-deploy hook however that provider does it.
Pairing with webhooks for round-trip alerting
The most useful pattern is deploy hook in, score.dropped webhook out:
- CI deploys → fires the deploy hook → scan starts.
- Scan finishes → if the new score is 5+ points below the previous scan's score, we POST a
score.droppedwebhook to the URL you configured. - Your webhook handler posts to Slack, opens a Jira ticket, or fails the next CI build.
Net effect: regressions are caught within minutes of the deploy, with the trigger metadata (ref, sha, environment) attached to the scan so you can blame the right commit.
Rotating a deploy token
From the same domain row in Domains, click Rotate. The old token stops working immediately; the new one is shown once. The rotation is atomic — there's no window where neither works.
Plan availability
Deploy hooks are a Pro and Agency feature. Free-tier users don't get them — same reasoning as API tokens: every triggered scan is a real cost.