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
- Create a scan via the API with a
callback_urlparameter - GeoScored generates a unique
webhook_secretand returns it in the response - When the scan completes (or fails), GeoScored POSTs a signed JSON payload to your callback URL
- 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_idto 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.