> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantiva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for device and attestation events

Webhooks send HTTP POST requests to your server when events occur in Grantiva. Available on **Pro, Business, and Enterprise** plans.

## Events

| Event                       | Description                                                                                                |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `device.new`                | First attestation from a new device                                                                        |
| `device.attested.first`     | A device completes its first successful attestation                                                        |
| `device.high_risk`          | Device risk score exceeds your threshold                                                                   |
| `device.attestation_failed` | A device fails attestation validation                                                                      |
| `attestation.anomaly`       | Unusual attestation pattern detected                                                                       |
| `flag.created`              | A feature flag was created                                                                                 |
| `flag.updated`              | A feature flag's value or state changed                                                                    |
| `flag.deleted`              | A feature flag was deleted                                                                                 |
| `subscription.changed`      | A subscription entitlement changed (Enterprise — see [Subscription Claims](/concepts/subscription-claims)) |
| `subscription.expired`      | An entitlement expired or was revoked                                                                      |
| `subscription.refunded`     | An entitlement was refunded                                                                                |

## Setup

Create webhook endpoints from the dashboard under **Settings > Webhooks**.

1. Enter your endpoint URL (must be HTTPS)
2. Select which events to subscribe to
3. Save — Grantiva generates a signing secret (`whsec_...`)

## Payload format

```json theme={null}
{
  "event": "device.high_risk",
  "timestamp": "2025-03-10T12:00:00Z",
  "data": {
    "device_id": "abc123",
    "risk_score": 82,
    "jailbreak_detected": true,
    "device_model": "iPhone15,2",
    "os_version": "18.0"
  }
}
```

## Verifying signatures

Every webhook request includes an `X-Grantiva-Signature` header containing an HMAC-SHA256 signature of the request body, signed with your endpoint's secret.

```python theme={null}
import hmac
import hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)
```

```swift theme={null}
import Crypto

func verifyWebhook(body: Data, signature: String, secret: String) -> Bool {
    let key = SymmetricKey(data: Data(secret.utf8))
    let mac = HMAC<SHA256>.authenticationCode(for: body, using: key)
    let expected = "sha256=" + mac.map { String(format: "%02x", $0) }.joined()
    return expected == signature
}
```

Always verify signatures before processing webhook payloads.

## Subscription events

`subscription.changed` fires whenever a [subscription entitlement](/concepts/subscription-claims) materially changes (tier, status, interval, expiry, auto-renew, or product); identical redeliveries from Apple/Stripe do not re-fire it. `subscription.expired` additionally fires when status becomes `expired` or `revoked`, and `subscription.refunded` on refunds.

```json theme={null}
{
  "event": "subscription.changed",
  "timestamp": "2026-06-22T19:00:00Z",
  "data": {
    "subject_id": "<sharing-unit UUID>",
    "tier": "plus",
    "status": "active",
    "interval": "annual",
    "source": "apple",
    "product_id": "com.example.plus.annual",
    "expires_at": "2026-12-01T00:00:00Z",
    "source_subscription_id": "2000000xxxxxxxxx",
    "environment": "production",
    "auto_renew": "true",
    "event_id": "<uuid>"
  }
}
```

Handling notes:

* **Dedupe on `data.event_id`** and key any cache eviction by `data.subject_id`.
* `expires_at` is **ISO-8601 here but epoch seconds in the JWT claim** — each surface follows its own convention.
* Treat the webhook as a cache-bust nudge: re-read the claim from the next JWT rather than trusting the webhook body as authoritative.

## Retries

Failed deliveries (non-2xx response or timeout) are retried up to **3 times** with exponential backoff. You can view delivery history and response details in the dashboard.

## Testing

Send a test event from the webhook detail page in the dashboard (or `POST /api/v1/org/webhooks/:id/test`). This synchronously delivers a synthetic `attestation.completed` payload — signed like a real event — and reports the HTTP status your endpoint returned, so you can verify reachability and signature handling.
