Core API

Webhook callbacks for signed operator decisions

Validate signed Contro1 callbacks and safely resume AI workflows after approvals, rejections, expirations, or escalations.

Webhooks are the handoff point between human review and agent execution, so signature verification and idempotency are mandatory.

Key takeaways

  • Every callback is signed with HMAC-SHA256 over timestamp plus body.
  • Reject callbacks older than 5 minutes to prevent replay.
  • Return 200 even on idempotent duplicates so Contro1 stops retrying.
  • Map approved, denied, timed_out, and cancelled outcomes to explicit branches.
  • Callbacks include policy_context when the original request included policy or risk evidence.

Why webhook verification matters

If your callback path accepts forged decisions, your approval layer is not real.

Every workflow that resumes after human review should verify timestamp and signature before applying the response.

What your handler should do

  • Verify X-CentCom-Signature and X-CentCom-Timestamp
  • Use X-CentCom-Request-Id for correlation in logs
  • Reject stale callbacks
  • Deduplicate by delivery or request ID
  • Map approved, denied, timed_out, and cancelled outcomes explicitly

Payload shape

The primary fields for new webhook consumers are request_id, status, response, responded_by, responded_at, metadata, risk_level, policy_trigger, policy_context, and approval_comment_required.

structured_response duplicates the operator response in protocol terms, and protocol_response contains the full canonical Contro1Response for SDK adapters. If you are writing a simple handler, prefer response plus status.

webhook_payload.json
{
  "request_id": "req_abc123",
  "state": "answered",
  "status": "approved",
  "response": {
    "approved": true,
    "comment": "Approved for this customer."
  },
  "responded_by": "Ariel Navon",
  "responded_at": "2026-04-26T19:01:08.984Z",
  "metadata": {
    "case_id": "refund-8842"
  },
  "risk_level": "high",
  "policy_trigger": "Vendor transfers above $10,000 require finance approval.",
  "policy_context": {
    "source": "custom_rules",
    "policy_name": "finance-transfer-controls",
    "rule_id": "vendor-transfer-over-10000",
    "rule_reason": "Vendor transfers above $10,000 require finance approval.",
    "policy_version": "git:8f42c1a",
    "enforcement": "require_approval"
  },
  "approval_comment_required": true,
  "message": "Approved for this customer.",
  "structured_response": {
    "approved": true,
    "comment": "Approved for this customer."
  },
  "resolved_at": "2026-04-26T19:01:08.984Z",
  "protocol_response": {
    "request_id": "req_abc123",
    "status": "approved",
    "message": "Approved for this customer.",
    "structured_response": {
      "approved": true,
      "comment": "Approved for this customer."
    },
    "resolved_at": "2026-04-26T19:01:08.984Z"
  }
}

Verification example

verify.ts
import { verifyWebhook } from '@contro1/sdk';

const isValid = verifyWebhook(
  rawBody,
  req.headers['x-centcom-signature'],
  req.headers['x-centcom-timestamp'],
  process.env.CENTCOM_WEBHOOK_SECRET || ''
);

if (!isValid) {
  res.status(401).json({ error: 'Invalid signature' });
  return;
}
verify.py
import hmac, hashlib, time, os

def verify_webhook(body: bytes, signature: str, timestamp: str) -> bool:
    secret = os.environ["CENTCOM_WEBHOOK_SECRET"].encode()
    if abs(time.time() - int(timestamp)) > 300:
        return False
    signed = f"{timestamp}.".encode() + body
    expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Delivery states

  • answered - the operator submitted a response
  • callback_pending - Contro1 is attempting to deliver the signed callback
  • callback_delivered - the callback URL returned a successful 2xx response
  • callback_failed - retries were exhausted or delivery failed permanently
  • closed - the request lifecycle is complete after successful callback delivery

Frequently asked questions

What should happen if callback delivery fails?

Your workflow should be able to recover by reading request state from the API and replaying the final decision safely.

Can I resume workflows synchronously instead of with a webhook?

You can poll in simple setups, but signed callbacks are the better pattern for long-running or multi-team production flows.

How many retries will Contro1 attempt?

Up to 5 retries with exponential backoff. Your handler should be idempotent - the same delivery ID may arrive more than once.

Do I need to respond quickly?

Return 200 within 10 seconds. If your downstream workflow is slow, acknowledge immediately and process asynchronously.