Endpoint limits
Creating an endpoint on the Free plan returns a 403.
Events
Device and attestation events fire on paid plans only.
The high-risk threshold is your organization’s compliance
maxRiskScore, defaulting to 50 when unset. The event payload carries the threshold that was applied.Setup
Create webhook endpoints from Webhooks in the dashboard, or withPOST /api/v1/org/webhooks.
- Enter your endpoint URL — use HTTPS, since the signing secret only protects integrity, not confidentiality
- Select which events to subscribe to (at least one)
- Save — Grantiva generates a signing secret (
whsec_...) and returns it once, at creation
Payload format
Every delivery is a JSON object with three top-level keys:timestamp is ISO-8601 UTC. The body is serialized with sorted keys, so the bytes you receive are the bytes that were signed.
Fields per event
key_id is the App Attest key id — the stable per-device identifier, and the same value as the sub claim in the attestation JWT. device_id is the optional device id supplied on the challenge request; the iOS SDK does not send it, so it is usually an empty string. Key your own records on key_id.
device_model and os_version fall back to the literal string "unknown" when the SDK did not report them.
Request headers
Verifying signatures
The signature is an HMAC-SHA256 of the raw request body keyed with your endpoint’s secret, hex-encoded and prefixed withsha256=.
Subscription events
subscription.changed fires whenever a subscription entitlement materially changes — tier, status, interval, expiry, auto-renew, or product. Identical redeliveries from Apple or Stripe do not re-fire it. subscription.expired additionally fires when status becomes expired or revoked, and subscription.refunded on refunds. All three carry the same data.
- Dedupe on
data.event_idand key any cache eviction bydata.subject_id. expires_atis ISO-8601 here but epoch seconds in the JWT claim — each surface follows its own convention. It is an empty string when the source reports no expiry.- Treat the webhook as a cache-bust nudge: re-read the claim from the next JWT rather than trusting the webhook body as authoritative.
Delivery, retries, and history
A delivery counts as successful on any 2xx response. Anything else — a non-2xx status or a connection error — is a failure. Grantiva makes at most 3 delivery attempts:
After the third failure the delivery is marked permanently failed. Retries are picked up by a background sweep, so the delays are lower bounds rather than exact times.
Grantiva records the HTTP status, the first 1000 characters of your response body, and the error for every attempt. Read the history from the dashboard or
GET /api/v1/org/webhooks/:id/deliveries, and re-drive a failed one with POST /api/v1/org/webhooks/:id/deliveries/:deliveryId/retry — a manual retry resets the attempt counter and gets a fresh budget of 3.
Delivery is fire-and-forget: a webhook that fails never fails the attestation or dashboard action that triggered it.
Testing
Send a test event from the webhook detail page in the dashboard, or withPOST /api/v1/org/webhooks/:id/test. This synchronously delivers a synthetic attestation.completed payload — signed like a real event — and reports the HTTP status, response body, and latency your endpoint returned, so you can verify reachability and signature handling.
attestation.completed is a test-only event name. You cannot subscribe to it, and it is never delivered outside a test. The test delivery is not recorded in delivery history and is not retried.