Webhooks

Register an endpoint on a project, and what Elixion sends it

A webhook is an HTTPS endpoint of yours that Elixion, the platform for Engineering Intelligence™, posts events to. Webhooks belong to a project, so what an endpoint receives is scoped to that project's activity.

Registering one

  1. Open Project Settings and pick the Integrations tab.
  2. Go to the Webhooks tab.
  3. Press Add Webhook.
  4. Fill in:
    • Webhook Name
    • URL, for example https://example.com/webhook
    • Events, as checkboxes. At least one is required.
  5. Create.

Each webhook then appears as a card showing its events, its success rate, its total deliveries and when it last fired, with Test, Logs and Delete actions.

The API accepts more than the dialog collects. Creating a webhook over the API you can also set a description, an HTTP method, custom headers, filters, a signing secret, an API key, a timeout, and retry settings. See Delivery below.

Event types

| Group | Events | |-------|--------| | Issue | issue.created, issue.updated, issue.deleted, issue.assigned, issue.status_changed, issue.priority_changed, issue.commented | | Sprint | sprint.created, sprint.started, sprint.completed, sprint.updated, sprint.deleted | | Project | project.created, project.updated, project.deleted, project.member_added, project.member_removed | | User | user.registered, user.updated, user.deactivated | | Board | board.created, board.updated, board.column_moved | | Meeting | meeting.scheduled, meeting.started, meeting.ended, meeting.cancelled | | Document | document.created, document.updated, document.shared | | Automation | workflow.triggered, workflow.completed, workflow.failed | | Integration | integration.connected, integration.disconnected, integration.error |

What reaches an endpoint today

Two sources produce deliveries: the Test action on a webhook's card, and activity arriving from a connected GitHub or GitLab repository, which Elixion dispatches as issue.created, issue.updated and issue.status_changed.

If you are building against Elixion webhooks, use Test to develop your handler and connect a repository to see live traffic.

The payload

Every delivery posts JSON with this shape:

{
  "event_type": "issue.status_changed",
  "event_id": "3f1c7f2e-0b42-4f1f-9a0e-7c2b8d9a5e11",
  "timestamp": "2026-03-14T09:21:04.512Z",
  "project_id": "b8a1e6d4-2f37-4a1c-9c55-1de0f4c7a9b2",
  "triggered_by": "7c9e4a11-55b3-4e2a-bb17-3f0c9d2e5a84",
  "data": {},
  "event_metadata": {}
}

data carries the event's own fields. event_metadata carries anything the dispatcher attached, for example the source of an inbound repository event.

Delivery

Headers

| Header | Value | |--------|-------| | Content-Type | application/json | | User-Agent | The Elixion webhook agent and version | | X-Webhook-Event | The event type | | X-Webhook-Delivery | A unique id for this delivery attempt | | X-Webhook-Timestamp | Unix seconds when the attempt was sent | | X-Signature | Present when the webhook has a secret | | Authorization | Bearer <api key>, when the webhook has an API key |

Any custom headers you configured are sent too.

Verifying the signature

When a secret is set, X-Signature is sha256= followed by the hex HMAC SHA256 of the raw request body, keyed with the secret.

import hmac, hashlib

def verify(body: str, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Compare with a constant time comparison, and verify against the body exactly as received, before parsing it.

Timeouts and retries

  • Timeout is per webhook. It defaults to 30 seconds and can be set anywhere from 1 to 300 seconds.
  • Retry attempts default to 3 and can be set from 0 to 10.
  • Backoff is exponential from a base you set, default 60 seconds, doubling each attempt and capped at one hour. With the defaults that is roughly one minute, then two, then four.

Failed deliveries are marked for retry and picked up by a background sweep when their next attempt is due. The sweep runs about once a minute, so an attempt goes out shortly after its backoff elapses rather than exactly on it. A delivery that used up its attempts is left as failed and is not tried again.

Two other things put an event in the retry queue rather than in front of your endpoint straight away, and the sweep is what sends them: a deployment that restarts a pod in the middle of an attempt, and a burst large enough that the platform defers the delivery instead of adding to the queue of calls already in flight. Both are recorded as a delivery you can see, with error_code CANCELLED or DISPATCH_REFUSED, and both are sent by the same sweep, on the same Retry attempts allowance as any other failure. With Retry attempts set to 0 there is no allowance, so both are recorded as failed straight away and neither is sent again.

A deferred delivery is written down a moment after it is deferred rather than in the same breath. If the pod is restarted inside that moment, which is rare and needs the restart to land in the same second as a burst, the event is lost with no record of it. Everything else in this section holds whatever happens to the pod.

Return a 2xx quickly and do your work afterwards: a handler that takes longer than the timeout is recorded as a failure even when it succeeded.

Filters

A webhook can carry filters, matched against the event's data. A filter is a map of field name to one of:

  • A value, matched for equality.
  • A list, matched as "is one of".
  • An object {"op": ..., "value": ...} where op is eq, ne, in or nin.

Every filter must match for the delivery to be made. A field named by a filter and absent from the event does not match.

{
  "priority": ["CRITICAL", "HIGH"],
  "status": { "op": "ne", "value": "BACKLOG" }
}

Watching deliveries

The Activity Logs tab on the same Integrations page lists webhook deliveries alongside sync runs. Pick a webhook and you get its totals across the top, total, succeeded, failed and success rate, then a row per delivery with its status, event type, HTTP response code and time.

Over the API, GET /api/v1/webhooks/{id}/deliveries returns the delivery history and GET /api/v1/webhooks/{id}/stats returns the totals. POST /api/v1/webhooks/{id}/toggle enables or disables a webhook without deleting it, and GET /api/v1/webhooks/events/types returns the event catalogue.

Building a handler that behaves

  • Verify the signature first, on the raw body, before you parse.
  • Be idempotent. A retry re sends the same event, and event_id is stable across attempts, so record it and ignore repeats.
  • Answer fast. Acknowledge with a 2xx, then queue the work.
  • Do not trust order. Deliveries are independent and go out in the background, so a retried event, or simply a slower one, can arrive after a later event.
  • Subscribe narrowly. Every event you accept and discard is a delivery that could have failed.

Next