Webhooks
Receive a POST to your endpoint whenever a scan finishes, an AI Monitor run completes, or a Power-Up deploys. Every delivery is signed with HMAC-SHA256 so you can verify it came from GroundScore.
Create a subscription
/api/v1/webhooksscope: write:webhooksRegister a URL to receive event deliveries. The response includes a secret you must store immediately. It is shown only once and cannot be retrieved later.
Request body
| Field | Type | Description |
|---|---|---|
url | string | Must start with https://. Plain http is rejected. |
events | string[] | Array of event types to subscribe to. Use ["*"] to subscribe to all current and future events. |
Example request
Example response
Responds 201 Created on success.
The secret is 64 hexadecimal characters with no prefix. Use it exactly as returned when computing the HMAC.
List subscriptions
/api/v1/webhooksscope: read:webhooksReturns every subscription on this account, newest first, including per-subscription delivery health (lastDeliveryAt and failureCount). Auto-disabled subscriptions are listed too. The secret is never returned by this endpoint.
Example response
Revoke a subscription
/api/v1/webhooks/{subscriptionId}scope: write:webhooksPermanently revokes a subscription. No further deliveries will be attempted. The subscription's delivery history is deleted along with it, which cancels any retry still queued for a past failed delivery.
Example request
Example response
Event types
Three event types are available today. Every delivery wraps its payload in the same envelope:
scan.completed
Fired when a scheduled or manual scan finishes with status completed.
ai_check.completed
Fired when an AI Monitor run finishes scoring.
power_up.deployed
Fired when a Power-Up is successfully deployed to a connected site.
Delivery headers
Every delivery POST carries these headers alongside Content-Type: application/json.
| Header | Value |
|---|---|
X-GroundScore-Signature | HMAC-SHA256 of the raw body, formatted as sha256={hex digest}. |
X-GroundScore-Event | The event type, e.g. scan.completed. Matches the event field in the body. |
X-GroundScore-Delivery-Id | Identifier for this delivery. Stays the same across every retry of it. |
User-Agent | GroundScore-Webhooks/1.0 |
Signature verification
Every delivery includes an X-GroundScore-Signature header of the form sha256={hex digest}. The digest is HMAC-SHA256 of the raw request body using your subscription secret as the key.
Node.js / JavaScript
Python
Retry policy
Each delivery is attempted immediately when the source event fires. If your endpoint returns a non-2xx status, times out, or the connection fails, GroundScore schedules retries with exponential backoff:
| Attempt | Delay after previous failure |
|---|---|
| 1 | Immediate (queued) |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +12 hours |
| 7 | +24 hours |
After the 7th failed attempt, the delivery is marked permanently failed. Each delivery also has a 10-second timeout. Endpoints that take longer than 10s to respond fail the attempt.
Auto-disable
The failureCount on a subscription counts deliveries that exhausted all seven attempts, not individual attempts. A single successful delivery resets it to zero. Once failureCount reaches 100, the subscription is auto-disabled and queued deliveries are marked failed without being sent, so a dead endpoint stops being hammered.
A disabled subscription is not deleted. It keeps appearing in GET /api/v1/webhooks with its failureCount intact so you can see what broke. To start delivering again, revoke it and create a new subscription.
Best practices
- Always verify the signature. An unsigned POST to your endpoint is not a GroundScore event.
- Respond within 10 seconds. Acknowledge the delivery fast, then do any heavy work asynchronously (queue, background worker, etc.).
- Be idempotent. Retries can deliver the same event more than once. Use
X-GroundScore-Delivery-Idas your idempotency key: it is stable across every retry of a delivery and unique per delivery. The event's primary ID (scanId,runId,deploymentId) works too when you want to collapse repeats of the same underlying event. - Return 2xx on success. Any non-2xx status (4xx or 5xx) triggers the retry schedule above.
- Subscribe narrowly. Prefer
["scan.completed"]over["*"]unless you really want every future event type.
Errors
| Code | When |
|---|---|
not_found | Subscription does not exist or belongs to a different account. |
insufficient_scope | Key lacks the required scope (read:webhooks for GET, write:webhooks for POST/DELETE). |
invalid_body | The POST body was not valid JSON. |
invalid_url | url is missing, longer than 500 characters, not https://, or not a valid URL. |
invalid_events | events is missing, empty, not an array of strings, or names an event outside the list above. |