Webhooks

Events & Payloads

The JSON envelope every DevStride webhook carries and every field of item.created, item.status_changed, comment.created, sla.at_risk and sla.breached, with when each fires and a realistic example of each.

Every webhook is an HTTP POST whose body is one JSON envelope. The envelope is the same for every event; its data object depends on the event type.

The envelope

{
  "id": "6c1e9f3a-2b7d-5a40-8e15-4f9b0c2d7a63",
  "type": "item.created",
  "apiVersion": "2026-10-16",
  "occurredAt": "2026-10-17T14:03:11.482Z",
  "organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
  "origin": {
    "kind": "member",
    "actorId": "jordan.lee",
    "correlationId": "b7e2c4d1-9a3f-4e8b-a6d2-5c1f0e9b3a74"
  },
  "data": { "item": { "...": "..." } }
}
FieldTypeDescription
idstring (UUID)The event's id. Unique per event and identical on every retry and replay, and the same for every endpoint that receives the event. It is also sent in the X-DevStride-Event-Id header. Deduplicate on it.
typestringitem.created, item.status_changed, comment.created, sla.at_risk or sla.breached; ping for a test send.
apiVersionstringThe contract version, currently "2026-10-16". See Compatibility.
occurredAtstring (ISO-8601, UTC)When the change happened in DevStride, to the millisecond. Not when the delivery was sent: a retried delivery keeps its original occurredAt.
organizationIdstring (UUID)The organization the change happened in.
originobjectWho or what made the change. See Origin.
dataobjectThe payload for this type, described below.

Origin

origin tells you who caused the change, so you can route it, or drop the echo of a change your own integration made.

FieldTypeDescription
kindstringmember, customer, api, system or automation (below).
actorIdstring or nullWho acted: a member's username for member and api, the requester id for customer, and null for system or whenever the change does not record who made it.
correlationIdstringThe id of the request or job that produced the change. Events caused by the same action share it, which lets you group them.
kindMeansExamples
memberA person working in DevStrideAn item created in the app; a status moved on a board; an internal note.
customerA Service Desk requesterA reply by email or in the customer portal; a requester moving their own ticket in the portal.
apiA member writing through the public APIToday, only a comment a member (not a requester) wrote on the api channel. Most API writes read member; see below.
systemDevStride itselfAn SLA timer; a status reassigned mechanically after a board or status-collection change; a test ping.
automationReserved for changes made by automation rulesNot sent yet.

item.created

Fires when a work item is created: in the app, through the API or the DevStride MCP server, and when a Service Desk ticket arrives by email, from the portal or from a request form. It covers every work type, tickets included. Creating a workstream does not fire it.

The payload is a fixed subset of the item, not the whole record. Every key is always present; a value that is not set is null.

FieldTypeDescription
data.item.idstringThe item's id, in the form <organizationId>:<number>.
data.item.numberstringThe item number shown in DevStride, for example I4821.
data.item.titlestringThe title. For a ticket from an inbound email, this is the email's subject.
data.item.workTypeIdstringThe item type's id.
data.item.parentNumberstringThe number of the workstream or item it was created under, for example F37.
data.item.statusIdstring or nullIts status.
data.item.boardIdstring or nullThe board it is on.
data.item.priorityIdstringIts priority.
data.item.assigneeUsernamestring or nullThe assignee's username.
data.item.labelsarray of stringsLabel ids. Empty when it has none.
data.item.requesterIdstring or nullThe Service Desk requester. Set only on a ticket: a non-null value is how you tell a ticket from ordinary work.
data.item.channelAddressIdstring or nullThe inbound channel address a ticket arrived at. null for ordinary work.
data.item.archivedbooleanWhether it was created archived.
{
  "id": "6c1e9f3a-2b7d-5a40-8e15-4f9b0c2d7a63",
  "type": "item.created",
  "apiVersion": "2026-10-16",
  "occurredAt": "2026-10-17T14:03:11.482Z",
  "organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
  "origin": {
    "kind": "member",
    "actorId": "jordan.lee",
    "correlationId": "b7e2c4d1-9a3f-4e8b-a6d2-5c1f0e9b3a74"
  },
  "data": {
    "item": {
      "id": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10:I4821",
      "number": "I4821",
      "title": "Invoice PDF download returns a blank page",
      "workTypeId": "2d5b8e1f-7c3a-4b9e-8f20-6a1d4c7e9b35",
      "parentNumber": "F37",
      "statusId": "c3a9e7d2-1f4b-4e6a-9d8c-7b2e5f1a0c46",
      "boardId": "a41f6c2e-8d3b-4f7a-b5e9-0c6d2a8f1e57",
      "priorityId": "e9d2b6a4-3c7f-4a1e-8b5d-2f9c6e0a7d18",
      "assigneeUsername": null,
      "labels": ["5f3c8a1d-6e2b-4d9f-a7c4-1b8e3f6d2a90"],
      "requesterId": "0b7d4e9a-5c2f-4a8e-9d1b-6e3f8c2a5d71",
      "channelAddressId": "d8a5c2f7-4e1b-4c9d-8a6e-3f0b7d4c1e29",
      "archived": false
    }
  }
}

The event deliberately leaves out descriptions, custom fields and everything else that changes from release to release. When you need more, read the item from the API by its number (GET /v1/organizations/{organizationId}/work-items/{itemNumber}; see the API reference).

item.status_changed

Fires when an item moves to a different status: dragged on a board, changed in the item, set through the API, MCP or an automation, moved by a requester in the portal, or reassigned by DevStride itself.

FieldTypeDescription
data.itemIdstringThe item's id, <organizationId>:<number>.
data.itemNumberstringThe item number, for example I4821.
data.previousStatusIdstring or nullThe status it left.
data.newStatusIdstring or nullThe status it entered.
data.previousStatusTypestring or nullThe kind of status it left: new, custom or done.
data.newStatusTypestring or nullThe kind of status it entered: new, custom or done. A move into done is how you detect completion.
data.boardIdstring or nullThe board the item is on.
data.causestringWhy it moved (below).
causeMeansorigin.kind
directIt was moved deliberately: by a member in the app, through the API or MCP, or by an automation.member, with actorId when the change records who did it, otherwise null
customer-self-serviceThe requester moved their own ticket in the customer portal.customer, with actorId null
board-remapDevStride re-homed it because the item was attached to, detached from or moved between boards.system
migrationDevStride reassigned it mechanically, for example after its status was removed from a collection.system
{
  "id": "9a4d7e2c-6f1b-5c83-b0e9-3d5a8c1f6e27",
  "type": "item.status_changed",
  "apiVersion": "2026-10-16",
  "occurredAt": "2026-10-17T15:21:40.007Z",
  "organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
  "origin": {
    "kind": "member",
    "actorId": "jordan.lee",
    "correlationId": "e1c8a5f2-7d3b-4e9a-b6c1-8f4d2a7e0b93"
  },
  "data": {
    "itemId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10:I4821",
    "itemNumber": "I4821",
    "previousStatusId": "c3a9e7d2-1f4b-4e6a-9d8c-7b2e5f1a0c46",
    "newStatusId": "7e2f9b4c-0a6d-4c1e-9f3b-5d8a2c6e1f04",
    "previousStatusType": "new",
    "newStatusType": "custom",
    "boardId": "a41f6c2e-8d3b-4f7a-b5e9-0c6d2a8f1e57",
    "cause": "direct"
  }
}

To show status names rather than ids, read the organization's statuses once from the API and cache them.

comment.created

Fires when a comment is added to a work item or ticket, or a reply is added under one. It fires for public replies and internal notes alike, whichever way they arrived:

How the comment was writtenchannelvisibilityorigin.kind
A member in the DevStride app (a reply or an internal note)nullpublic or internalmember
A requester replying by emailemailusually publiccustomer
A requester replying in the customer portalportalpubliccustomer

channel can also read api for a message that came in through the Service Desk API, and sms is reserved for text messages. Comments on Knowledge Base articles do not fire the event.

FieldTypeDescription
data.comment.idstring (UUID)The comment's id.
data.comment.itemNumberstringThe item or ticket it belongs to, for example I4821. For a reply, the item of the comment it replies to.
data.comment.visibilitystringpublic (customer-visible on a ticket) or internal (staff only).
data.comment.channelstring or nullemail, sms, portal or api: the channel it arrived on or was sent through. null for a comment written in the DevStride app.
data.comment.authorUsernamestringThe member who wrote it. For a requester's comment this is DevStride's own system account, not a person: use externalAuthorId.
data.comment.externalAuthorIdstring or nullThe requester who wrote it; null when a member wrote it.
data.comment.fromExternalAuthorbooleantrue when a requester wrote it.
data.comment.htmlstringThe comment body as HTML. Empty when truncated is true.
data.comment.truncatedbooleantrue when the body is larger than 256 KB and was left out. Read the comment from the API to get it.
data.comment.attachmentsarray of { "url": string }Files and images stored in DevStride that the HTML links to or embeds: the same public URLs customer emails use. Links to other websites are not listed. Still present when truncated is true.
data.comment.createdAtstring (ISO-8601)When the comment was written.

A requester's reply by email, with a screenshot:

{
  "id": "2f6c1d0a-8b4e-5c3f-9a71-0e5d4c3b2a19",
  "type": "comment.created",
  "apiVersion": "2026-10-16",
  "occurredAt": "2026-10-17T16:45:02.318Z",
  "organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
  "origin": {
    "kind": "customer",
    "actorId": "0b7d4e9a-5c2f-4a8e-9d1b-6e3f8c2a5d71",
    "correlationId": "f3b9d6a1-2c8e-4f7b-a0d5-9e1c4b7f2a68"
  },
  "data": {
    "comment": {
      "id": "4e8b1f6d-3a9c-4d2e-b7f0-1c5a8e3d9b42",
      "itemNumber": "I4821",
      "visibility": "public",
      "channel": "email",
      "authorUsername": "devstride",
      "externalAuthorId": "0b7d4e9a-5c2f-4a8e-9d1b-6e3f8c2a5d71",
      "fromExternalAuthor": true,
      "html": "<p>Still blank for me, screenshot attached.</p><p><img src=\"https://media.devstride.com/8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10/comments/blank-invoice.png\"></p>",
      "truncated": false,
      "attachments": [
        { "url": "https://media.devstride.com/8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10/comments/blank-invoice.png" }
      ],
      "createdAt": "2026-10-17T16:45:02.301Z"
    }
  }
}

An internal note from a member differs only where you would expect: "visibility": "internal", "channel": null, the member's authorUsername, "externalAuthorId": null, "fromExternalAuthor": false, and an origin of { "kind": "member", "actorId": "<username>" }.

sla.at_risk and sla.breached

Fire for Service Desk tickets that an SLA policy is timing:

  • sla.at_risk fires when a timer enters the last 20% of its allowed time without being met: your warning that a promise is about to be broken.
  • sla.breached fires when a target passes before it was met.

Timers count in the policy's business hours, so these fire on that clock. Both events carry the same three facts and always have origin.kind system with actorId null.

FieldTypeDescription
data.itemNumberstringThe ticket, for example I4821.
data.metricstringWhich promise: FIRST_RESPONSE, NEXT_RESPONSE or RESOLUTION.
data.targetAtstring (ISO-8601)When the target is (or was) due.
{
  "id": "d7b3e1a9-4c6f-5e2d-8a0b-6f9c3e5d1a84",
  "type": "sla.breached",
  "apiVersion": "2026-10-16",
  "occurredAt": "2026-10-17T18:00:00.512Z",
  "organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
  "origin": {
    "kind": "system",
    "actorId": null,
    "correlationId": "a6e0c3f8-5b2d-4a9e-81c7-4d6f9b2e0a35"
  },
  "data": {
    "itemNumber": "I4821",
    "metric": "FIRST_RESPONSE",
    "targetAt": "2026-10-17T18:00:00.000Z"
  }
}

An sla.at_risk event has exactly the same shape, with "type": "sla.at_risk" and a targetAt still in the future.

ping

ping is sent only by the Test button (or the test route of the API). It is signed like any other event, is not stored in the delivery log and is never retried. Its data is { "endpointId": "<the endpoint's id>" } and its origin.kind is system. You cannot subscribe to it; accept it with a 2xx and ignore it.

What does not send an event

The five types above are the complete list today. In particular there is no event for:

  • edits other than a status change (title, assignee, priority, fields, description);
  • deleting, archiving or restoring an item;
  • creating or changing workstreams, boards or other configuration;
  • comments on Knowledge Base articles.

Changes that load or correct history rather than happen now (for example items imported with their original creation dates) are not delivered either: a backfill of ten thousand items would otherwise send ten thousand webhooks.

Compatibility

The contract only ever grows:

  • New fields and new event types are added without notice. Ignore keys you do not recognize, and answer 2xx to an event type you do not handle: never reject it.
  • Nothing is removed or changes meaning without a new apiVersion, announced ahead of time.

Write your parser to read the fields it needs and tolerate everything else, and new releases will never break it.