Webhook Endpoints
Subscribe to events and receive real-time HTTP notifications when things happen in your organization.
Webhook endpoints let you receive HTTP callbacks when events occur in your organization -- for example, when a ticket is created or a status changes. You register a URL, choose which events to subscribe to, and Spedy sends a signed POST request to your URL whenever a matching event fires.
Webhook endpoints require a Pro plan subscription.
Supported Events
GET /api/v1/webhook-endpoints/eventsReturns the list of event types you can subscribe to.
Available Events
| Event | Description |
|---|---|
ticket.created | A ticket was created |
ticket.updated | A ticket was updated |
ticket.deleted | A ticket was deleted |
ticket.status_changed | A ticket's status changed |
ticket.assigned | A ticket was assigned or reassigned |
ticket.claimed | An agent (or a person) took an exclusive lease on a ticket |
ticket.claim_released | A lease was handed back |
ticket.claim_expired | A lease ran out without being released and the ticket became claimable again |
ticket.reported | An agent reported a run back via the tickets_report MCP tool |
comment.created | A comment was added to a ticket |
pr.opened | A pull request appeared for the first time |
pr.status_changed | A pull request's state, pipeline status or review status changed |
pr.merged | A pull request was merged |
pipeline.updated | A CI/CD run was ingested or changed status |
Payloads
Every delivery has the same envelope; only data differs per event:
{
"id": "evt_...",
"event": "ticket.claimed",
"timestamp": "2026-09-02T10:00:00.000Z",
"organizationId": "org_...",
"data": { }
}The whole body is signed with the endpoint's secret and sent as
X-Spedy-Signature-256: sha256=<hex>; X-Spedy-Event and X-Spedy-Delivery
carry the event name and the delivery id.
data carries identifiers and state, never content. A webhook receiver
sits outside Spedy's permission model, so no comment body and no ticket
description ever leaves through this channel -- comment.created gives you a
commentId and an isSecret flag, and you read the comment through the API
with a token that is actually authorised for it.
| Event | data fields |
|---|---|
ticket.* (all) | ticketId, boardId, projectId (legacy alias of boardId), organizationId |
ticket.updated | + actorId |
ticket.status_changed | + oldStatusId, newStatusId, actorId |
ticket.assigned | + oldAssigneeId, newAssigneeId |
ticket.claimed | + claimedById, claimExpiresAt, claimCount |
ticket.claim_released | + releasedById, previousClaimedById, reason |
ticket.claim_expired | + previousClaimedById, claimExpiresAt |
ticket.reported | + actorId, outcome, summary, prUrl, branch, statusChanged, claimReleased, clientName |
comment.created | + commentId, authorId, isSecret |
pr.opened / pr.status_changed / pr.merged | prId, ticketId, boardId, organizationId, provider, repositoryFullName, externalNumber, title, url, sourceBranch, state, pipelineStatus, reviewStatus, requiresHumanReview |
pipeline.updated | pipelineId, boardId, provider, repositoryFullName, providerPipelineId, ref, status, webUrl, pullRequestId |
ticketId on the PR events is null when no ticket key could be resolved from
the branch or title. pr.opened fires once, on the first webhook that creates
the pull-request record -- the periodic background sync deliberately emits
nothing, so connecting a repository with 200 open PRs does not fire 200 events.
Waking an agent loop
ticket.assigned is the intended wake-up signal for a loop that would
rather not poll: assign a ticket to an agent user, receive the event, and have
the loop claim the ticket immediately instead of waiting for the next
tickets_list { claimable: true } tick. Combine it with ticket.claimed /
ticket.claim_expired if you run several loops and want to see the handover.
See Agent loops.
List Webhook Endpoints
GET /api/v1/webhook-endpointsPermission required: webhooks:view
Returns all webhook endpoints configured for your organization.
Example Response
[
{
"id": "wh_abc123",
"name": "CI/CD Pipeline",
"url": "https://ci.example.com/hooks/spedy",
"events": ["ticket.status_changed", "ticket.created"],
"isActive": true,
"createdAt": "2025-06-01T10:00:00Z",
"updatedAt": "2025-06-01T10:00:00Z"
}
]Create Webhook Endpoint
POST /api/v1/webhook-endpointsPermission required: webhooks:manage
Creates a new webhook endpoint. The response includes a secret field -- store this securely, as it is only returned once. Use the secret to verify the signature of incoming payloads.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name (max 100 characters) |
| url | string | Yes | The HTTPS URL to receive webhook payloads |
| events | string[] | Yes | Array of event types to subscribe to |
Example Request
{
"name": "CI/CD Pipeline",
"url": "https://ci.example.com/hooks/spedy",
"events": ["ticket.created", "ticket.status_changed"]
}Example Response
{
"id": "wh_abc123",
"name": "CI/CD Pipeline",
"url": "https://ci.example.com/hooks/spedy",
"events": ["ticket.created", "ticket.status_changed"],
"isActive": true,
"secret": "whsec_a1b2c3d4e5...",
"createdAt": "2025-06-01T10:00:00Z",
"updatedAt": "2025-06-01T10:00:00Z"
}Get Webhook Endpoint
GET /api/v1/webhook-endpoints/{id}Permission required: webhooks:view
Returns details for a specific webhook endpoint.
Update Webhook Endpoint
PATCH /api/v1/webhook-endpoints/{id}Permission required: webhooks:manage
Updates the name, URL, or subscribed events of a webhook endpoint.
Delete Webhook Endpoint
DELETE /api/v1/webhook-endpoints/{id}Permission required: webhooks:manage
Returns 204 No Content.
Ping Webhook Endpoint
POST /api/v1/webhook-endpoints/{id}/pingPermission required: webhooks:manage
Sends a test ping to the webhook URL to verify connectivity. Returns 204 No Content.
List Deliveries
GET /api/v1/webhook-endpoints/{id}/deliveriesPermission required: webhooks:view
Returns recent delivery attempts for a webhook endpoint.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Number of deliveries to return (default: 50, max: 200) |
Example Response
[
{
"id": "dlv_xyz789",
"endpointId": "wh_abc123",
"eventType": "ticket.created",
"statusCode": 200,
"success": true,
"attempt": 1,
"error": null,
"createdAt": "2025-06-15T14:30:00Z"
}
]Redeliver
POST /api/v1/webhook-endpoints/{id}/deliveries/{deliveryId}/redeliverPermission required: webhooks:manage
Re-sends a previous webhook delivery. Returns 204 No Content.