Guide

Webhooks: 22 events, delivered signed

Which events Zuuna sends, how to verify the signature (HMAC-SHA256 over the raw body), and what happens when a delivery fails.

Webhooks are the API in reverse: instead of your script asking every few minutes whether anything changed, Zuuna calls you the moment it does. For a chat bot, a pipeline or an n8n workflow, that is the difference between "runs immediately" and "runs eventually".

What fires an event

You register a URL and pick the events you care about. When one happens, Zuuna sends a POST with a JSON body to that URL — signed, so you can check it really came from us.

The event catalogue

22 events in five groups:

EventMeaning
card.createdA card was created
card.movedA card moved to another column
card.assignee_addedSomeone was assigned
card.label_addedAn epic was attached
card.priority_changedPriority changed
card.due_approachingA due date is approaching
card.due_arrivedA due date arrived
card.comment_addedA comment was posted
card.archivedA card was archived
card.restoredA card was restored
card.deletedA card was deleted
git.branch_createdA branch was created
git.commit_pushedA commit was linked
git.pr_openedA pull request was opened
git.pr_mergedA pull request was merged
ci.passedA build passed
ci.failedA build failed
time.loggedTime was logged
release.createdA release was created
release.publishedA release was published
release.unpublishedA release was unpublished
release.deletedA release was deleted

The headers

Every delivery carries three headers: X-Zuuna-Event with the event name, X-Zuuna-Delivery with an id unique to that delivery, and X-Zuuna-Signature with the signature.

Verifying the signature

The signature is an HMAC-SHA256 over the raw body, keyed with the endpoint's secret, formatted as sha256=<hex>.

# Node — verify X-Zuuna-Signature against the RAW body
import { createHmac, timingSafeEqual } from "crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(rawBody)          // the raw bytes, NOT JSON.parse'd and re-stringified
    .digest("hex");
  const a = Buffer.from(header ?? "", "utf8");
  const b = Buffer.from(expected, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

Over the raw body, not over re-serialised JSON. Parse the body and stringify it again and you may change key order or whitespace — the signature then never matches, and the failure looks exactly like a wrong secret. And compare in constant time, not with ===.

Retries and failures

Answer 2xx as soon as you have accepted the body — not once your processing has finished. Failed deliveries are retried, and what actually went out and came back is in the delivery log in the app. There is a test call that sends a sample delivery to your URL.

Next

The REST API is the polling direction; the CLI covers the git case outright. Zapier, Make and n8n need no dedicated integration — a URL and a secret are enough; see integrations.

FAQ

How do I verify a webhook really came from Zuuna?

Via the X-Zuuna-Signature header: an HMAC-SHA256 over the raw body keyed with the endpoint's secret, formatted sha256=hex. The RAW body matters — parse and re-serialise it and the signature will never match.

Which events exist?

22 of them, grouped by card (created, moved, assigned, commented, archived …), git (branch, commit, PR opened and merged), CI (passed, failed), time and release.

What happens if my endpoint is down?

The delivery is retried. What went out and what came back is in the delivery log in the app, including the status code and response body.

Do Zapier or n8n need a dedicated integration?

No. Both accept a signed webhook and can call the REST API. A URL, a secret and the events you care about are enough.

Ready to make it simpler?

Boards, sprints, docs and time tracking in one place — GDPR-compliant, hosted in the EU.