Home/Docs

Deploy hooks

Per-domain webhook URLs that trigger a fresh scan from your CI pipeline. One curl call after every deploy — pair with score.dropped webhooks to alert on regressions.

Pro & Agency

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

  1. Open Domains.
  2. Find the domain you want to monitor; click the chevron to expand its row.
  3. 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.
  4. 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_… vs sxp_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:write can 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:

  1. CI deploys → fires the deploy hook → scan starts.
  2. Scan finishes → if the new score is 5+ points below the previous scan's score, we POST a score.dropped webhook to the URL you configured.
  3. 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.