In headless mode DevStride sends nothing to your customers. Every email it would have sent a requester (a staff reply, an acknowledgement, a status update, a sign-in link) is delivered to your webhook endpoint instead, as one customer.message event. Your own tool sends it, from your own domain, in your own branding.
Everything else keeps working: inbound email still creates tickets, the API still works, SLA clocks keep running, and staff see and do exactly what they did before.
customer.message, and make sure it is enabled. Headless mode refuses to turn on without one, so customer messages can never go nowhere.id, as for every webhook: see Verifying Signatures and Delivery, Retries & Ordering.While headless mode is on, DevStride also refuses to delete, disable or unsubscribe the last enabled endpoint receiving customer.message. Subscribe a second endpoint first, or turn headless mode off.
customer.message eventEach event is one message for one customer. subject, text and html are already written and ready to send. They contain no DevStride logo, image, button, footer or link. html is plain paragraphs and lists, plus, for a reply, the reply's own formatting. text is the same message with the tags removed.
| Field | Type | Description |
|---|---|---|
data.kind | string | What the message is: one of the six kinds below. |
data.message | object | The facts specific to that kind (below). {} when there are none. |
data.itemNumber | string or null | The ticket it is about, for example I4821. null for login_link and portal_redirect, which are about no single ticket. |
data.itemTitle | string or null | That ticket's title. null when itemNumber is. |
data.requester | object | Who to send it to: { "id", "email", "name" }. email is lower case; name can be null. Read when the message is sent, so it is always the requester's current address. |
data.companyId | string or null | The requester's company, or null. |
data.subject | string | The subject line to use. |
data.text | string | The message as plain text. |
data.html | string | The message as HTML. |
Send it to data.requester.email. Nothing else in the event needs to reach the customer.
kind | Sent when | data.message | What you do |
|---|---|---|---|
reply | A customer-visible reply is added to a ticket by staff in DevStride, or by an automation (for example the auto-close notice). | commentId, attachments (an array of file URLs the reply links to or embeds). | Send it. Report the outcome with a delivery receipt for commentId if you like. |
request_received | A new ticket is created from a customer's email. | {} | Send the acknowledgement, or drop it if your tool sends its own. |
request_confirmation | A customer submitted a request DevStride holds until they confirm their email address. | verificationToken | Ask the customer to confirm their address in your tool, then call the confirm API with the token. |
status_change | A ticket moves to a status with a different customer-facing label. Not sent when Email customers on status change is off. | fromLabel (or null), toLabel | Send it, or update your own portal instead. |
login_link | A customer asked to sign in to the DevStride customer portal. | magicLinkUrl, otpCode | Send it only if you still offer the DevStride portal. |
portal_redirect | A customer asked to sign in at the wrong company's portal and is pointed to their own. | companyName, portalLoginUrl | Send it only if you still offer the DevStride portal. |
The status labels are open, in_progress, awaiting_your_reply, solved and closed. The rendered text uses their display form: Open, In progress, Awaiting your reply, Solved, Closed.
login_link and portal_redirect carry DevStride portal addresses, and are sent only while the customer portal is open. If you close the portal (Customer portal off), they stop.
origin follows the webhook rules:
reply reads member, with the username of the member who wrote it in actorId (for an automation's reply, the automation's creator);system, with actorId null.A reply you post through the API never comes back as a customer.message. A staff reply you record is one your tool already delivered, so DevStride does not hand it back to you to send again. The comment.created event for it still fires, with origin.kind api.
A staff reply with an attachment:
{
"id": "4b8e2f1c-7d3a-5c9e-b1f0-2a6d8e4c9b17",
"type": "customer.message",
"apiVersion": "2026-10-16",
"occurredAt": "2026-10-24T15:12:40.218Z",
"organizationId": "8f14e45f-ceea-4e7a-9b1d-3c5a2f6e9d10",
"origin": {
"kind": "member",
"actorId": "jordan.lee",
"correlationId": "e3c1a7b9-2f4d-4e6a-9b8c-1d5f7a3e2c60"
},
"data": {
"kind": "reply",
"message": {
"commentId": "c2f7a9d1-6e3b-4a8c-b5d0-9e1f4a7c3b28",
"attachments": [
"https://media.example.com/attachments/printer-settings.png"
]
},
"itemNumber": "I4821",
"itemTitle": "Printer on floor 3 is offline",
"requester": {
"id": "7a3d9c2e-1b5f-4e8a-a6c4-3f9e2d7b1c05",
"email": "sam.rivera@example.com",
"name": "Sam Rivera"
},
"companyId": "5e9b1d3f-8c2a-4f6e-b7d1-0a4c8e2f6b93",
"subject": "Re: Printer on floor 3 is offline",
"text": "Thanks Sam, it is back online now.\n\nThe settings we changed are attached.",
"html": "<p>Thanks Sam, it is back online now.</p><p>The settings we changed are <a href=\"https://media.example.com/attachments/printer-settings.png\">attached</a>.</p>"
}
}
The other kinds have the same envelope, with origin.kind system and actorId null. Their data:
{
"kind": "request_received",
"message": {},
"itemNumber": "I4822",
"itemTitle": "VPN drops every hour",
"requester": { "id": "7a3d9c2e-1b5f-4e8a-a6c4-3f9e2d7b1c05", "email": "sam.rivera@example.com", "name": "Sam Rivera" },
"companyId": "5e9b1d3f-8c2a-4f6e-b7d1-0a4c8e2f6b93",
"subject": "We received your request “VPN drops every hour”",
"text": "We received your request “VPN drops every hour”.\n\nWe will reply as soon as we can.",
"html": "<p>We received your request “VPN drops every hour”.</p><p>We will reply as soon as we can.</p>"
}
{
"kind": "request_confirmation",
"message": { "verificationToken": "EXAMPLE-TOKEN-not-a-real-value" },
"itemNumber": "I4823",
"itemTitle": "New laptop for a starter",
"requester": { "id": "1c6e8a4b-3d9f-4b2e-8a7c-5f0d2b9e6a41", "email": "alex.kim@example.com", "name": null },
"companyId": null,
"subject": "Confirm your email to submit “New laptop for a starter”",
"text": "Please confirm your email address to submit your request “New laptop for a starter”.\n\nIf you did not make this request, you can ignore this message.",
"html": "<p>Please confirm your email address to submit your request “New laptop for a starter”.</p><p>If you did not make this request, you can ignore this message.</p>"
}
{
"kind": "status_change",
"message": { "fromLabel": "in_progress", "toLabel": "solved" },
"itemNumber": "I4821",
"itemTitle": "Printer on floor 3 is offline",
"requester": { "id": "7a3d9c2e-1b5f-4e8a-a6c4-3f9e2d7b1c05", "email": "sam.rivera@example.com", "name": "Sam Rivera" },
"companyId": "5e9b1d3f-8c2a-4f6e-b7d1-0a4c8e2f6b93",
"subject": "Your request \"Printer on floor 3 is offline\" is now Solved",
"text": "Your request “Printer on floor 3 is offline” is now Solved.",
"html": "<p>Your request “Printer on floor 3 is offline” is now Solved.</p>"
}
{
"kind": "login_link",
"message": { "magicLinkUrl": "https://app.devstride.com/portal/example-co/login/verify?token=EXAMPLE", "otpCode": "123456" },
"itemNumber": null,
"itemTitle": null,
"requester": { "id": "7a3d9c2e-1b5f-4e8a-a6c4-3f9e2d7b1c05", "email": "sam.rivera@example.com", "name": "Sam Rivera" },
"companyId": "5e9b1d3f-8c2a-4f6e-b7d1-0a4c8e2f6b93",
"subject": "Sign in to Example Co support",
"text": "Use this link to sign in to Example Co support: https://app.devstride.com/portal/example-co/login/verify?token=EXAMPLE\n\nOr enter this code: 123456\n\nIf you did not ask to sign in, you can ignore this message.",
"html": "<p>Use this link to sign in to Example Co support: https://app.devstride.com/portal/example-co/login/verify?token=EXAMPLE</p><p>Or enter this code: 123456</p><p>If you did not ask to sign in, you can ignore this message.</p>"
}
{
"kind": "portal_redirect",
"message": { "companyName": "Northwind", "portalLoginUrl": "https://app.devstride.com/portal/northwind/login" },
"itemNumber": null,
"itemTitle": null,
"requester": { "id": "7a3d9c2e-1b5f-4e8a-a6c4-3f9e2d7b1c05", "email": "sam.rivera@example.com", "name": "Sam Rivera" },
"companyId": "5e9b1d3f-8c2a-4f6e-b7d1-0a4c8e2f6b93",
"subject": "Your Example Co support portal",
"text": "Your requests with Example Co are in the Northwind support portal.\n\nSign in there: https://app.devstride.com/portal/northwind/login",
"html": "<p>Your requests with Example Co are in the Northwind support portal.</p><p>Sign in there: https://app.devstride.com/portal/northwind/login</p>"
}
The portal addresses in the last two samples are illustrations; use the URLs exactly as they arrive.
A reply's files stay where DevStride stores them. data.message.attachments lists the public URLs the reply's html already links to or embeds, the same ones DevStride's own emails use. Link to them, or download and attach them in your tool. Files from your customers go in the other direction through the upload operations.
No message is sent to a requester who is blocked or erased. Automatic notices (request_received, request_confirmation, status_change) are not sent to a no-reply address. The same rules govern DevStride's own email.
The four switches are one call, PATCH /service-desk/settings, with the Manage Service Desk Settings permission. Send only the keys you change.
| Key | Default | Meaning |
|---|---|---|
headlessMode | false | DevStride sends nothing to customers and emits customer.message instead. Turning it on is refused (400) unless an enabled endpoint is subscribed to customer.message. |
customerPortalEnabled | true | The customer portal is open. When false, it answers as if it did not exist, no email or message offers a portal link, and login_link and portal_redirect stop. |
helpCentreEnabled | true | The help centre (the portal's knowledge base) is open. When false, its pages answer as if they did not exist. |
notifyCustomerOnStatusChange | true | Customers are told when their ticket's customer-facing status changes, by email or, in headless mode, by a status_change message. |
curl -X PATCH "https://api.devstride.com/v1/organizations/$ORG_ID/service-desk/settings" \
-u "$DEVSTRIDE_API_KEY:$DEVSTRIDE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{ "headlessMode": true, "customerPortalEnabled": false, "helpCentreEnabled": false }'
The response is the updated organization. To read the switches at any time, GET /v1/organizations/{organizationId} returns the organization under data, with serviceDeskHeadlessMode, serviceDeskCustomerPortalEnabled, serviceDeskHelpCentreEnabled and serviceDeskNotifyCustomerOnStatusChange and their defaults filled in.
sla.at_risk and sla.breached still fire.subject, text and html are written in English. Write your own copy from kind and message if you need another language.reply messages can take one.Attachments and a Full Round Trip
Attach files to API messages, and see how webhooks and the Partner API fit together from a customer's first email to a delivered reply.
Confirming Requesters
In headless mode, release a request DevStride is holding until the customer confirms their email address: pass the token from the request_confirmation message to the confirm API.