Backlog API
Webhooks
Let Backlog call you when an issue changes, signed so you can trust the sender.
Base URLhttps://backlog.polder.work
Polling answers "what is there now". Webhooks answer "tell me when it happens". Use the API to read, webhooks to
react. A webhook is a standing instruction to send a workspace's contents (issue titles, descriptions, customer names)
to a URL, which is closer to granting access than to setting a preference: administrators create them under
Settings → Webhooks, and the URL must be https.
Events
| Event | When |
|---|---|
issue.created |
An issue was created |
issue.updated |
An issue changed |
issue.deleted |
An issue was deleted |
issue.commented |
Somebody commented |
project_update.published |
A project update was posted |
triage.received |
Something arrived in a triage queue |
incident.declared |
An incident was declared |
incident.changed |
An incident changed status, severity, title, type or a role |
incident.update_posted |
A status update was posted on an incident |
incident.resolved |
An incident was resolved |
incident.closed |
An incident was closed |
The names are a promise: they are what you switch on. New events can be added; existing ones are not renamed.
Payload
{
"event": "issue.updated",
"issue": { "id": 41, "identifier": "ENG-12", "title": "Login times out" }
}
Incident events carry incident (the same row the incidents API returns), change (kind, data, at) and, for a posted update, update (id, body, impact, actions, next_update_at). A private incident is never sent.
Issue bodies have the same shape the REST API returns. Two exceptions:
issue.deletedcarries only{"issue": {"id": 41, "team_id": 3}}; there is no row left to describe.triage.receivednames the team, not the issue: it announces that the queue needs a look. The same arrival also produced anissue.createdif you want the contents.
Verify every delivery
Every request carries three headers:
X-Backlog-Event: issue.updated
X-Backlog-Delivery-Timestamp: 1755600000
X-Backlog-Signature: sha256=<hmac>
The signature is HMAC-SHA256 over "<timestamp>.<raw body>", keyed with the endpoint's signing secret.
Important Sign the raw bytes you received, not a re-encoded object: key order and whitespace change the digest. Compare in constant time, and reject a timestamp that is too old, or a captured request can be replayed forever.
import hmac, hashlib
def valid(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Delivery and failure
Deliveries are queued, so a slow receiver never holds up the person who caused the event. Any non-2xx response, timeout or connection error counts as a failure and is retried after 30 seconds, 2 minutes and 10 minutes. Every attempt is logged with its status code or error. After 20 consecutive failures the endpoint is switched off, not deleted, so you can see why and turn it back on once the URL works.
Tip Answer
2xxquickly and do the work afterwards. A receiver that processes inline and takes twelve seconds will be retried while it is still working.