A webhook is useful when the worker should not hold an HTTP connection open while a person decides. The safe pattern is still the same: create one durable approval, persist the caller's state, verify the signed callback, and resume the work exactly once after the typed decision arrives.
The callback path
The callback should be fast and idempotent. Acknowledge the event after signature verification and enqueue the actual work; do not perform a slow deployment or refund inside the webhook request itself.
Create the Action with a callback
The CLI is convenient for a scheduled job or a small integration:
actionbox ask "Approve the production deploy?" \
--option approve="Ship it" \
--option reject="Hold" \
--callback-url "https://ops.example.com/actionbox/deploy" \
--context-json '[{"type":"key_value","items":{"environment":"production","commit":"abc123f","run_id":"deploy-4821"}}]'For a backend service, use the SDK and record the Action ID before returning to the queue:
import os
from actionbox import Actionbox
box = Actionbox(os.environ["ACTIONBOX_API_KEY"])
action = box.create(
title="Approve production deploy?",
options=[
{"id": "approve", "label": "Ship it", "style": "primary"},
{"id": "reject", "label": "Hold", "style": "destructive"},
],
callback_url="https://ops.example.com/actionbox/deploy",
idempotency_key="deploy:4821:approval",
)
save_pending_run(run_id="deploy-4821", action_id=action.id)The callback URL is a delivery mechanism, not the source of truth. If delivery fails, the worker should be able to retrieve the Action by ID and reconcile the pending run.
Verify before you enqueue
Use the webhook secret and the official verification procedure for your Actionbox client or deployment. Verify the signature against the raw request bytes before parsing JSON, reject old or replayed events, and store the event ID with a unique constraint.
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
@app.post("/actionbox/deploy")
async def actionbox_callback(
request: Request,
actionbox_signature: str | None = Header(default=None),
):
raw = await request.body()
if not actionbox_signature:
raise HTTPException(status_code=400, detail="missing signature")
# Call the Actionbox SDK/verifier here with the raw bytes, secret, and
# signature header. Do not parse or trust the body before verification.
event = verify_actionbox_event(raw, actionbox_signature)
if not claim_event_once(event["id"]):
return {"ok": True, "duplicate": True}
enqueue_resume(
run_id=event["data"]["metadata"]["run_id"],
action_id=event["data"]["action_id"],
decision=event["data"].get("decision"),
)
return {"ok": True}The verifier name and header vary by client implementation, so keep that code in one tested adapter instead of copying cryptographic logic into every route. Never accept an unsigned browser request as a decision.
Resume exactly once
The queue consumer should use the Action ID and run ID as a deduplication key. Re-read the Action, check that it is the expected version and decision, and atomically mark the pending run as resumed before starting the side effect.
def resume_pending(run_id: str, action_id: str) -> None:
pending = claim_pending_run_once(run_id, action_id)
if pending is None:
return # another delivery already resumed it
action = box.get(action_id)
if action.decision != "approve":
mark_stopped(run_id, "rejected or expired")
return
try:
deploy(pending.payload)
except Exception:
action.report_outcome("failed", reason_code="deploy_error")
raise
else:
action.report_outcome("success")A callback can be delivered more than once, arrive after a worker restart, or race with a manual reconciliation. Idempotency belongs both at the event-consumer boundary and at the side-effecting operation.
For long-running framework state, combine this pattern with durable execution and the human-in-the-loop AI agent architecture. For the decision and audit contract, see AI agent audit trails and the Actionbox docs.
Create a free Source · Human approval API · Read the docs
Sources and further reading
- Actionbox integration docs — callbacks, typed responses, and outcomes.
- Human approval API — create the first decision request.
- Human-in-the-loop API vs building your own — delivery and maintenance trade-offs.



