Skip to content

Developers

Webhooks

Send issue, comment, message and project events to Zapier, Make, n8n or your own services as they happen, signed so you can verify them.

Webhooks tell other tools about changes in Deckbell as they happen: Deckbell sends an HTTPS POST with a JSON body to a URL you choose. This is how Zapier, Make, n8n and your own services react to new issues, comments and messages.

Workspace owners and admins manage webhooks in Settings → Webhooks.

Add a webhook

  1. Open Settings → Webhooks and choose Add webhook.
  2. Paste the URL to send events to. It must be a public https:// address; local and private network addresses aren't allowed.
  3. Tick the events to send, or All events (which also includes events added later).
  4. Copy the signing secret (it starts with whsec_). You can reveal it again in the webhook's details, and Rotate secret replaces it.

Choose Send test in the webhook's details to send a ping event and see what your endpoint answered.

Events

EventSent whendata contains
issue.createdAn issue is createdissue
issue.updatedIts title, description, status, priority, assignee, labels, estimate, parent, sprint or archived state changesissue
issue.deletedAn issue is deletedissue (as it was)
comment.createdSomeone comments on an issuecomment, issue
message.createdA message is posted in a public channelmessage, channel
project.createdA project is createdproject
project.updatedA project is renamed or editedproject
project.archived, project.restoredA project is archived or restoredproject
project.deletedA project is deletedproject (as it was)

Events are sent whatever made the change: someone in the app, the API, an AI assistant over MCP, or an agent. Messages in private channels and direct messages are never sent. Issues brought in by an import don't send one event each.

The objects in data have the same fields as in the REST API.

Payload

{
  "id": "evt_01jbx3m8k2…",
  "type": "issue.updated",
  "created_at": "2026-10-29T12:00:00+00:00",
  "workspace": { "id": 7, "name": "Acme", "slug": "acme" },
  "actor": { "id": 12, "name": "Dana Dev", "username": "dana-dev", "avatar": null },
  "data": {
    "issue": { "identifier": "WEB-42", "title": "Checkout button is broken", "state": { "name": "In Progress", "category": "started" }, … }
  },
  "changes": {
    "state": { "from": { "id": 3, "name": "Todo", "category": "unstarted" }, "to": { "id": 4, "name": "In Progress", "category": "started" } },
    "priority": { "from": "none", "to": "high" },
    "labels": { "added": [{ "id": 9, "name": "Bug" }], "removed": [] }
  }
}
  • actor is who made the change, or null for agents and automatic changes.
  • changes is only on *.updated events. Several edits made together arrive as one event.
  • id is the same on every retry, so you can skip events you've already handled.

Each request also has these headers:

  • Deckbell-Event: the event type, e.g. issue.created
  • Deckbell-Delivery: a unique id for the delivery
  • Deckbell-Signature: the signature (below)

Verify the signature

Deckbell-Signature looks like t=1730203200,v1=5257a869…. v1 is an HMAC-SHA256 of the timestamp, a dot and the raw request body, keyed with your signing secret. Check it before trusting a request, and reject old timestamps (say, more than 5 minutes) to stop replays.

Node.js:

import crypto from 'node:crypto';

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}

Python:

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts.get("v1", ""))

Use the body exactly as received: parsing and re-encoding the JSON changes it and the signature won't match.

Retries and failures

Answer with any 2xx status within 10 seconds. Anything else (or no answer) counts as a failure, and Deckbell tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. Redirects aren't followed.

If 15 deliveries in a row fail every attempt, Deckbell turns the webhook off and notes why in its details (and in the audit log). Fix the endpoint, then switch the webhook back on.

Delivery log

Open a webhook to see its recent deliveries from the last 30 days: the event, whether it was delivered, the response code, and how many attempts it took. Select one to see the exact payload and your endpoint's response, and Redeliver to send it again.

Zapier, Make and n8n

Use the tool's "catch webhook" trigger (Zapier: Webhooks by Zapier → Catch Hook; Make: Webhooks → Custom webhook; n8n: Webhook node), copy the URL it gives you into a new Deckbell webhook, and choose Send test so the tool learns the payload's fields. To make changes in Deckbell from those tools, call the REST API with an API token.

Still stuck? Email us and we’ll help.