ActionBoxDOCS

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-Event
  • X-Actionbox-Timestamp: Unix seconds
  • X-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-After may 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.

On this page