Webhooks and Callbacks
Verify ActionBox callback signatures, deduplicate delivery, and handle retries safely.
Callbacks are durable deliveries created when an Action with a callback_url reaches a terminal state. They are queued with the terminal event; a callback failure never changes the Action outcome.
Approval Policies also emit signed non-terminal action.approval_recorded and action.approvals_invalidated events, plus action.approval_overridden when applicable. Only the terminal transition emits action.resolved. Each policy event includes the current version, fingerprint, and safe approval progress without reviewer email addresses.
Signature headers
X-Actionbox-EventX-Actionbox-Timestamp: Unix secondsX-Actionbox-Signature:v1=<hex HMAC-SHA256>
Compute HMAC-SHA256 over "{timestamp}." + raw_request_body using the per-Source webhook secret returned when the Source is created or its secret is rotated.
import hashlib
import hmac
import time
def verify(
timestamp: str,
raw_body: bytes,
signature: str,
webhook_secret: str,
max_age_seconds: int = 300,
) -> bool:
try:
age = abs(time.time() - int(timestamp))
except (TypeError, ValueError):
return False
if age > max_age_seconds:
return False
expected = hmac.new(
webhook_secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, f"v1={expected}")The example rejects timestamps more than five minutes from the receiver's
clock. Keep a replay window appropriate for your system, use a constant-time
comparison, and deduplicate the event id from the JSON body. Parse JSON only
after verifying the exact raw bytes.
Delivery behavior
- HTTP 2xx marks the delivery delivered.
- Network errors, timeouts, HTTP 429, and HTTP 5xx are retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 1 day, 2 days, 4 days, and 7 days, capped by the plan's retry window and adjusted with bounded positive jitter.
- Other non-2xx responses fail permanently.
- A valid
Retry-Aftermay delay the next retry. - Consumers must be idempotent because delivery is at least once.
For approval-gated side effects, persist the action_version and fingerprint
returned at creation. Resume only when a terminal callback has
status: "resolved", resolved_by: "user", and the same binding. Treat
Source/system resolutions, changed bindings, rejection, cancellation, and
expiration as fail-closed outcomes. Do not treat context or control callbacks
as decisions.
Hosted callback URLs must use public HTTPS, cannot contain credentials, are revalidated before delivery, and do not follow redirects. Keep the webhook secret out of URLs and logs.
Context-request callbacks
Most callbacks report a terminal Action. One non-terminal event lets the
review loop continue: action.context_requested. It is sent when an approver
asks a context_request_mode: callback Source for missing context and contains the Action binding,
plain-language question, requested field hints, requester, and request time.
Verify it exactly like every other callback, fetch the latest Action, then
update that same Action with decision_context or typed context blocks. See
Decision context for the complete lifecycle.