Skip to content

Callbacks and Webhooks

When you register a platform, Ratify stores a callback_url for that platform or connection.

Ratify uses that URL to deliver asynchronous events back to your platform. This is how your app learns about connection lifecycle changes, delegation decisions, key rotation, and revocation state without polling.

The callback URL is part of the commercial surface operated by Identities AI. It complements the free protocol and SDKs; it does not replace them.

Use the callback URL to keep your platform in sync with Ratify-managed state:

  • record that a connection was created
  • persist the connection’s API key prefix and webhook secret
  • react when a delegation is approved or denied
  • invalidate caches when a delegation is revoked
  • update a connected workspace when a key rotates
  • deactivate a customer connection when it is removed

Think of it as a control-plane feed. It is not where proof verification happens. It is where your platform learns, “something changed, update your local state.”

If the callback succeeds, Ratify assumes your system has consumed the event. If the callback fails, Ratify retries later.

Current events include:

  • connection.created
  • connection.key_rotated
  • connection.removed
  • delegation.approved
  • delegation.denied
  • delegation.revoked

Each delivery is POSTed as JSON over HTTPS.

Every delivery includes:

  • Content-Type: application/json
  • X-Ratify-Signature
  • X-Ratify-Timestamp
  • X-Ratify-Delivery-ID

The JSON body always includes an event field and event-specific fields.

Your platform should:

  1. read the raw request body
  2. verify the signature using the shared webhook secret
  3. reject stale timestamps
  4. deduplicate by delivery ID
  5. parse event
  6. update local state for that event
  7. return 204 No Content or any other 2xx once the event is consumed

Do not do proof verification inside the callback handler. The callback is for lifecycle state, not for live authorization checks.

Your endpoint should:

  1. accept only HTTPS
  2. verify X-Ratify-Signature
  3. verify X-Ratify-Timestamp
  4. reject stale deliveries
  5. deduplicate using X-Ratify-Delivery-ID
  6. return a 2xx status to acknowledge success
  7. return a non-2xx status if you want Ratify to retry the delivery

Ratify signs the payload with HMAC-SHA256 using the platform’s webhook secret. The signature covers the timestamp and the body.

func verifyRatifyWebhook(secret string, timestamp string, sig string, body []byte) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
func handleRatifyWebhook(w http.ResponseWriter, r *http.Request, secret string) {
body, _ := io.ReadAll(r.Body)
timestamp := r.Header.Get("X-Ratify-Timestamp")
sig := r.Header.Get("X-Ratify-Signature")
deliveryID := r.Header.Get("X-Ratify-Delivery-ID")
if !verifyRatifyWebhook(secret, timestamp, sig, body) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
if isDuplicateDelivery(deliveryID) {
w.WriteHeader(http.StatusNoContent)
return
}
var event map[string]any
_ = json.Unmarshal(body, &event)
switch event["event"] {
case "connection.created":
markConnectionActive(event["connection_id"].(string), event["api_key"].(string))
case "connection.key_rotated":
updateAPIKeyPrefix(event["connection_id"].(string), event["api_key_prefix"].(string))
case "delegation.approved":
storeDelegation(event["cert_json"])
case "delegation.denied":
markRequestDenied(event["request_id"].(string))
case "delegation.revoked":
invalidateCert(event["cert_id"].(string))
case "connection.removed":
deactivateConnection(event["connection_id"].(string))
}
rememberDelivery(deliveryID)
w.WriteHeader(http.StatusNoContent)
}
expected = HMAC_SHA256(secret, timestamp + "." + raw_body)
accept only if expected matches X-Ratify-Signature
accept only if abs(now - timestamp) < 5 minutes

Use this to persist the platform connection and mark the integration active.

Important fields:

  • connection_id
  • platform_id
  • org_id
  • org_name
  • allowed_scopes
  • callback_url
  • api_key
  • webhook_signing_secret

Consumer action:

  • store the connection record
  • store the API key prefix or key reference
  • store the webhook secret securely
  • mark the integration as active
  • optionally pre-seed the workspace with the allowed scopes

Use this to replace the stored API key prefix or update connection state after key rotation.

Important fields:

  • connection_id
  • api_key_prefix
  • previous_prefix

Consumer action:

  • replace the stored key reference
  • invalidate any cached API auth material
  • notify operators if the platform relies on long-lived credentials

Use this to record that a human approved a delegation request and to provision downstream access if needed.

Important fields:

  • request_id
  • connection_id
  • org_id
  • agent_id
  • approved_scope
  • cert_json
  • delegation_key_statement

Consumer action:

  • persist the approved delegation certificate
  • mark the request as approved in your UI
  • let the agent runtime resume or continue the session
  • attach the cert to your audit log

Use this to stop pending authorization flows and surface the denial in your UI or audit log.

Important fields:

  • request_id
  • connection_id
  • org_id
  • agent_id

Consumer action:

  • stop the pending authorization flow
  • surface the denial reason in your UI
  • keep the agent blocked until a new request is approved

Use this to invalidate cached permissions immediately.

Important fields:

  • cert_id
  • agent_id
  • connection_id
  • org_id
  • reason

Consumer action:

  • remove the cert from your cache
  • reject any session or token derived from that cert
  • notify the connected user or admin that access was revoked

Use this to disable the integration and remove any tokens or local state associated with that connection.

Important fields:

  • connection_id
  • org_id
  • removed_at

Consumer action:

  • disable the integration locally
  • delete API credentials and cached scopes
  • stop accepting proofs for that connection

What your code should do after verification

Section titled “What your code should do after verification”

Treat the callback as a state update, not a request to do business logic synchronously.

Good handling:

  • update your local connection record
  • refresh cached scopes
  • record the event in your audit log
  • notify the workspace admin if a connection was removed or revoked

Bad handling:

  • trusting the body before verifying the signature
  • using the callback as the source of a user-facing action without idempotency
  • retrying forever on 4xx errors
  • return 204 No Content when you have processed the event
  • return another 2xx if your framework prefers it
  • return 401 only for bad signatures or stale timestamps
  • return 500/503 for temporary local failures so Ratify retries
  • never return 200 before your handler has stored or queued the event
POST /ratify-webhook
Headers:
X-Ratify-Signature: <hmac>
X-Ratify-Timestamp: <unix-seconds>
X-Ratify-Delivery-ID: <uuid>
Body:
JSON event payload
func handleRatifyWebhook(w http.ResponseWriter, r *http.Request, secret string) {
timestamp := r.Header.Get("X-Ratify-Timestamp")
sig := r.Header.Get("X-Ratify-Signature")
deliveryID := r.Header.Get("X-Ratify-Delivery-ID")
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(body)
if !hmac.Equal([]byte(sig), []byte(hex.EncodeToString(mac.Sum(nil)))) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
_ = deliveryID // use this for idempotency
w.WriteHeader(http.StatusNoContent)
}

The callback URL is your integration surface. Ratify is the sender. Your platform is the receiver. The SDKs are still used for proof creation and verification elsewhere in the flow.

If you are implementing the protocol yourself, host this endpoint in your own control plane. If you are using the managed product, configure it in the Identities AI app.