PolderAPI

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.deleted carries only {"issue": {"id": 41, "team_id": 3}}; there is no row left to describe.
  • triage.received names the team, not the issue: it announces that the queue needs a look. The same arrival also produced an issue.created if 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 2xx quickly and do the work afterwards. A receiver that processes inline and takes twelve seconds will be retried while it is still working.