Wiring CI/CD approval gates with signed callbacks
Pause a pipeline at a deploy gate, post approve/reject buttons, and verify the signed callback your server receives — full contract, retries, and timeouts.
A release pipeline that pauses and asks a human is a two-way contract: Dailybot posts the approval request, and your CI system’s webhook receives the click. Get the signature verification and retry handling wrong and you either block deploys on false negatives or — worse — trust an unverified request. This is the exact contract, end to end.
The gate: two buttons, one callback URL
Post the gate as an interactive message with an approve and a reject button, both pointing at the same callback_url, both carrying a bearer token via callback_auth so your webhook can authenticate the transport on top of the mandatory signature:
curl -sS -X POST 'https://api.dailybot.com/v1/send-message/' \
-H 'X-API-KEY: $DAILYBOT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"message": "Deploy 4.12.0 to production — approve?",
"target_channels": ["C0123456789"],
"buttons": [
{"label": "Approve", "button_type": "interactive", "value": "approved",
"callback_url": "https://ci.example.com/hooks/deploy",
"callback_auth": {"type": "bearer", "token": "$DEPLOY_CALLBACK_TOKEN"},
"response": {"message": "Approved — deploying now."}},
{"label": "Reject", "button_type": "interactive", "value": "rejected",
"callback_url": "https://ci.example.com/hooks/deploy",
"callback_auth": {"type": "bearer", "token": "$DEPLOY_CALLBACK_TOKEN"},
"response": {"message": "Rejected."}}
]
}'
The CLI shorthand for exactly this pattern:
dailybot chat send -c C0123456789 -m "Deploy 4.12.0 to production — approve?" \
--approve-button "Approve=approved" \
--reject-button "Reject=rejected" \
--callback-url "https://ci.example.com/hooks/deploy" \
--callback-bearer "$DEPLOY_CALLBACK_TOKEN"
--approve-button / --reject-button take "Label=value" (equals-sign separator); --callback-url and --callback-bearer apply to both. Prefer --callback-bearer "$TOKEN" over a literal token so nothing lands in shell history or process lists. Note the immediate response in the curl version: it fires in parallel with the outbound POST, not gated on your webhook’s reply — the acknowledgment is instant, and the actual deploy decision happens on your side, asynchronously (the slow-approval pattern). If you need to update the message after your pipeline finishes, edit it later by re-POSTing the same bot_message_id within 72 hours.
What your webhook receives
Dailybot POSTs this exact shape to callback_url on click:
POST https://ci.example.com/hooks/deploy
Content-Type: application/json
User-Agent: Dailybot-Chatbot/1.0
X-Dailybot-Event: button_click
X-Dailybot-Delivery: 5e2d1a9c-...-uuid
X-Dailybot-Timestamp: 1782001872
X-Dailybot-Signature: t=1782001872, v1=6d9a3e...hex_hmac
Authorization: Bearer $DEPLOY_CALLBACK_TOKEN
{
"event": "button_click",
"bot_message_id": "$db/ae007b43-dde2-4fa9-bce3-71fb0975a249",
"button": {"id": "$btn/...", "value": "approved"},
"user": {"id": "...", "email": "...", "display_name": "...", "external_id": "U01ABCDEFG"},
"organization": {"id": "...", "name": "..."},
"platform": "slack",
"channel": {"id": "C0123456789", "type": "channel"},
"modal_fields": null,
"metadata": {},
"sent_at": "2026-07-22T14:30:00Z",
"clicked_at": "2026-07-22T14:31:12Z"
}
Read button.value ("approved" or "rejected") to decide what your pipeline does next; use user.display_name / user.email to attribute the decision in your deploy log; bot_message_id lets you edit the original message afterward to reflect the outcome.
Verifying the signature — never skip this
X-Dailybot-Signature is HMAC-SHA256 computed over the ASCII string "{unix_timestamp}.{raw_body}" using your organization’s callback signing secret (auto-generated the first time you send a message with callback_url; retrieve it from your org admin — it’s never returned by the public API). Verify it on every request, callback_auth or not:
const crypto = require('crypto');
function verify(rawBody, sigHeader, secretB64) {
const secret = Buffer.from(secretB64, 'base64url');
const parts = Object.fromEntries(sigHeader.split(',').map(s => s.trim().split('=')));
const ts = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-minute replay window
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'));
}
Read the raw request body — before any JSON parsing — to compute the signature; parsing first and re-serializing will produce a different byte string and a false signature mismatch.
callback_auth — additive, not a replacement
callback_auth is only valid together with callback_url (400 button_callback_auth_invalid on any other combination). Three types, exactly their own fields:
type |
Fields | Sent as |
|---|---|---|
bearer |
token (≤ 4096) |
Authorization: Bearer <token> |
basic |
username, password |
Authorization: Basic <base64> |
custom_header |
header_name, header_value |
<header_name>: <header_value> |
header_name must be a valid RFC 7230 token and cannot be host, content-length, content-type, transfer-encoding, connection, user-agent, or anything starting with x-dailybot-. Credentials are write-only — never returned by any read API, never logged. Treat callback_auth as a second lock on the door your gateway already requires, not a substitute for the HMAC check above.
Retry and timeout behavior
Dailybot retries the outbound callback POST once, with a 500 ms backoff, on a 5xx response, 429, or a network error. A successful 2xx completes the dispatch. Any other 4xx is treated as terminal and is not retried — if your webhook validates the payload and finds it malformed, return 4xx deliberately to stop Dailybot from retrying a request that will never succeed. Design your endpoint to:
- Verify the signature first, return
401/403immediately on failure (terminal, no retry). - Validate the payload shape, return
400on malformed input (terminal, no retry). - Persist enough to finish processing asynchronously, then return
2xxfast — don’t make the pipeline logic itself block the response. - Use
X-Dailybot-Deliveryas an idempotency key: if the same delivery id arrives twice (the one retry, or a client-side re-click beforedestroy_buttondisables the button), don’t double-trigger the deploy.
Cross-references
- The send-message API, end to end for the full button/callback contract this gate builds on.
/developers/api/messaging#send-messagefor the structured field reference./developers/clifor the full CLI flag set.
Browse the Solutions hub for the product-facing tour of this same flow, with the live simulated-chat demo.
FAQ
- How do I gate a deploy on a human click from Dailybot?
- Send a message with two interactive buttons that both set callback_url to your CI webhook and callback_auth for a bearer token. Your pipeline waits for the signed POST your webhook receives, reads button.value (e.g. "approved" or "rejected"), and resumes or cancels accordingly.
- How do I verify the callback POST actually came from Dailybot and not a spoofed request?
- Every callback_url POST carries X-Dailybot-Signature: t={timestamp}, v1={hex_hmac}. Recompute the HMAC-SHA256 over "{timestamp}.{raw_body}" with your organization's callback signing secret, compare in constant time, and reject anything older than a 5-minute window.
- What happens if my callback endpoint is briefly down when someone clicks approve?
- Dailybot retries the outbound POST once with a 500ms backoff on 5xx responses, 429, or network errors. A successful 2xx completes the dispatch; any other 4xx is treated as terminal and dropped — so a 4xx from your endpoint will not be retried.
- Can callback_auth replace signature verification?
- No. callback_auth (bearer, basic, or custom_header) is additive static transport auth for gateways that require it — it does not replace the HMAC signature, which is always present and must always be verified.
- Is callback_auth valid on every button type?
- No — callback_auth is only valid together with callback_url. Setting it alongside callback_form, callback_command, callback_prompt, or callback_workflow returns 400 button_callback_auth_invalid.