Skip to content
Academy Menu

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.

deep-diveDeveloperOps9 min read

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:

  1. Verify the signature first, return 401/403 immediately on failure (terminal, no retry).
  2. Validate the payload shape, return 400 on malformed input (terminal, no retry).
  3. Persist enough to finish processing asynchronously, then return 2xx fast — don’t make the pipeline logic itself block the response.
  4. Use X-Dailybot-Delivery as an idempotency key: if the same delivery id arrives twice (the one retry, or a client-side re-click before destroy_button disables the button), don’t double-trigger the deploy.

Cross-references

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.