GeoScored Webhooks

Get notified when scans complete. GeoScored sends HMAC-signed HTTP POST callbacks to your endpoint, so you can wire GEO (Generative Engine Optimization) audits into CI/CD pipelines, Slack alerts, or custom dashboards.

How it works

  1. Create a scan via the API with a callback_url parameter
  2. GeoScored generates a unique webhook_secret and returns it in the response
  3. When the scan completes (or fails), GeoScored POSTs a signed JSON payload to your callback URL
  4. Your server verifies the HMAC signature and processes the result

Creating a scan with a webhook

curl -X POST https://geoscored.ai/api/v1/scans \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "callback_url": "https://your-server.com/webhooks/geoscored"
  }'

The response includes a webhook_secret (starts with whsec_). Store this securely. It is only returned once, on the initial creation response.

Webhook payload

GeoScored sends the following JSON body on scan.completed and scan.failed events:

{
  "event": "scan.completed",
  "scan_id": "abc123",
  "url": "https://example.com",
  "status": "complete",
  "overall_score": 72.5,
  "error_message": null,
  "created_at": "2026-01-15T10:30:00",
  "completed_at": "2026-01-15T10:31:30"
}

Headers

Header Description
X-GeoScored-Signature HMAC-SHA256 signature, prefixed with v1=
X-GeoScored-Timestamp Unix timestamp of when the webhook was sent
Content-Type application/json
User-Agent GeoScored-Webhook/1.0

Verifying signatures

Always verify the X-GeoScored-Signature header before processing a webhook. The signature is computed as HMAC-SHA256 of the raw request body using your webhook_secret.

Python

import hmac
import hashlib

def verify_webhook(payload_bytes: bytes, signature: str, secret: str) -> bool:
    expected = "v1=" + hmac.new(
        secret.encode(), payload_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Node.js

const crypto = require('crypto');

function verifyWebhook(payloadBuffer, signature, secret) {
  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(payloadBuffer)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func verifyWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := fmt.Sprintf("v1=%s", hex.EncodeToString(mac.Sum(nil)))
    return hmac.Equal([]byte(expected), []byte(signature))
}

curl (manual check)

echo -n '{"event":"scan.completed",...}' | \
  openssl dgst -sha256 -hmac "whsec_your_secret_here"

Retry policy

If your endpoint returns a non-2xx status code or the request times out (10 seconds), GeoScored retries up to 2 more times:

Attempt Delay
1 (initial) Immediate
2 (first retry) 5 seconds
3 (final retry) 30 seconds

Your endpoint should respond within 10 seconds. If all 3 attempts fail, the webhook is dropped. You can always poll GET /api/v1/scans/{id} as a fallback.

Testing your integration

Use the test endpoint to send a sample webhook to your server without running a real scan:

curl -X POST https://geoscored.ai/api/v1/webhooks/test \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/webhooks/geoscored"}'

Returns the test secret and payload so you can verify your signature checking code.

To debug your signature verification, use the verify endpoint:

curl -X POST https://geoscored.ai/api/v1/webhooks/verify \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "{\"event\":\"scan.test\",\"scan_id\":\"test_00000000\"}",
    "signature": "v1=abc123...",
    "secret": "whsec_your_secret"
  }'

CI/CD integration examples

GitHub Actions

# .github/workflows/geo-audit.yml
name: GEO Audit
on:
  push:
    branches: [main]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger GEO scan
        run: |
          RESPONSE=$(curl -s -X POST https://geoscored.ai/api/v1/scans \
            -H "X-API-Key: ${{ secrets.GEOSCORED_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d "{\"url\": \"https://your-site.com\"}")
          SCAN_ID=$(echo $RESPONSE | jq -r '.id')
          echo "scan_id=$SCAN_ID" >> $GITHUB_OUTPUT

      - name: Wait for scan
        run: |
          for i in $(seq 1 30); do
            STATUS=$(curl -s https://geoscored.ai/api/v1/scans/$SCAN_ID \
              -H "X-API-Key: ${{ secrets.GEOSCORED_API_KEY }}" | jq -r '.status')
            [ "$STATUS" = "complete" ] && exit 0
            [ "$STATUS" = "failed" ] && exit 1
            sleep 10
          done
          exit 1

GitLab CI

# .gitlab-ci.yml
geo-audit:
  stage: test
  script:
    - |
      RESPONSE=$(curl -s -X POST https://geoscored.ai/api/v1/scans \
        -H "X-API-Key: $GEOSCORED_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"url\": \"$CI_ENVIRONMENT_URL\"}")
      echo $RESPONSE | jq .

Best practices

  • 1. Always verify signatures. Never process a webhook without checking the HMAC signature first. This prevents spoofed requests.
  • 2. Respond quickly. Return a 200 status code immediately, then process the webhook asynchronously. GeoScored times out after 10 seconds.
  • 3. Handle duplicates. Use the scan_id to deduplicate. Retries may deliver the same event more than once.
  • 4. Use HTTPS. Callback URLs must use HTTPS. GeoScored rejects plain HTTP endpoints.
  • 5. Store secrets securely. The webhook secret is only returned once during scan creation. Store it in environment variables or a secrets manager, never in source code.
Questions? Email [email protected] or check the API docs.