Consuming Taskbox callbacks
When a workspace member approves or rejects a task, Taskbox POSTs a JSON callback to the callback_url you supplied at task creation. This doc covers the wire shape your endpoint receives, how to verify the signature, and how to handle retries cleanly.
How delivery works
Each resolution produces a single delivery event with a stable event ID. Taskbox sends the request as POST with Content-Type: application/json. Your endpoint should respond as quickly as it can - a 2xx response confirms receipt; anything else is treated as a delivery failure and retried.
Headers your endpoint receives
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: taskbox-callback/1
X-Event-Id: 0193c8e2-7e44-7b21-9f31-3a6b8d4e0a10
X-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08X-Signature- HMAC-SHA256 of the raw request body, hex-encoded, prefixed withsha256=. Computed with the signing secret you received when the API key was created.X-Event-Id- unique ID for this delivery event. Stable across retries of the same resolution; use it to dedup.User-Agent-taskbox-callback/1.
Request body
{
"version": "1",
"event_id": "0193c8e2-7e44-7b21-9f31-3a6b8d4e0a10",
"task_id": "0193c8e1-1aa4-7b21-9f31-2c6b8d4e0a10",
"task_title": "Approve refund for order #4821",
"status": "approved",
"comment": "Within policy.",
"resolved_by": "[email protected]",
"resolved_at": "2026-05-03T14:42:11Z",
"metadata": {"order_id": "4821", "amount_usd": 240}
}version string RequiredWire contract version. Currently "1". Bumped if the shape ever changes.
event_id UUID string RequiredSame value as the X-Event-Id header. Persist it on your side and ignore replays.
task_id UUID string RequiredThe task that was resolved. Distinct from event_id.
task_title string RequiredTitle at the time of resolution.
status approved | rejected RequiredTerminal status the resolver chose.
resolved_by string (email) RequiredEmail of the workspace member who resolved the task.
resolved_at RFC3339 timestamp RequiredWhen the resolution happened.
comment string OptionalOptional note left by the resolver. Omitted from the body when empty.
metadata JSON object OptionalThe metadata object you supplied at task creation, echoed back verbatim. Omitted from the body when the original task had no metadata.
Verifying the signature
Always verify X-Signature before trusting the body. Compute HMAC-SHA256 of the raw request bytes (do not re-serialize the parsed JSON) using the signing secret for the API key that created the task, hex-encode it, and compare it - in constant time - to the value after sha256=.
// Pseudocode - works in any language with HMAC-SHA256.
raw = read_request_body_as_bytes()
expected_hex = hex(hmac_sha256(signing_secret, raw))
received = request.header("X-Signature") // "sha256=<hex>"
ok = constant_time_equal(received, "sha256=" + expected_hex)
if !ok { return 401 }
event = json_parse(raw)Reject any request whose signature does not match. If you rotate or revoke an API key, callbacks already in flight stay signed with the secret that was active when the task was created - keep old secrets verifiable for long enough to drain pending deliveries.
Idempotency
A delivery may be retried, so the same event_id can arrive more than once. Treat the first event_id you see as authoritative and discard later ones - typically by recording it in a deduplication table or cache before doing any side effects.
Retries and failures
A 2xx response ends delivery. Connection errors, timeouts, 408, 429, and 5xx responses trigger a retry; other non-2xx responses are considered permanent failures and stop further attempts. Deliveries that exhaust their retry budget surface on the in-app Monitoring page once you sign in.
Practical guidance: respond fast, do real work asynchronously, and make your handler safe to call repeatedly with the same event_id.
Next steps
Need to push tasks in the first place? See Create tasks via API.