Webhook events and payloads
Understand Sydnee webhook payloads through TypeScript examples, verify their signatures, and handle every available event.
Sydnee posts selected workspace events to your HTTPS endpoint as versioned JSON. Any language can receive them if it accepts HTTPS requests and parses JSON. This page uses TypeScript examples to describe the shared payload and event catalog.
This page uses portal user for a person with portal access. A client account is the account that person can access.
Understand the webhook body
Every live event uses the JSON payload shape shown in the TypeScript example below. Every key is present. Some values can be null or an empty array.
export type SydneeWebhookJsonValue =
| string
| number
| boolean
| null
| SydneeWebhookJsonValue[]
| { [key: string]: SydneeWebhookJsonValue };
export type SydneeWebhookEventCore = {
id: string;
event_type: SydneeWebhookEventType;
event_version: 1;
occurred_at: string;
operation_id: string | null;
account_id: string | null;
actor_type: "portalUser" | "teamUser" | "system";
actor_id: number | null;
object_type: SydneeWebhookObjectType;
object_id: string | number;
task_board_id: string | null;
parent_object_type: "task" | "file" | null;
parent_object_id: string | number | null;
resource_url: string | null;
};Event-specific fields use the same keys for every delivery:
export type SydneeWebhookEventDetails = {
request_field_id: string | null;
request_field_type: string | null;
request_field_label: string | null;
request_field_value: SydneeWebhookJsonValue;
status: string | null;
title: string | null;
email: string | null;
changes: string[];
conversation_id: string | null;
conversation_type: "account" | "channel" | "dm" | null;
message_origin: "web" | "rest" | "email" | "import" | null;
message_text: string | null;
message_has_attachment: boolean | null;
message_has_location: boolean | null;
};
export type SydneeWebhookEvent = SydneeWebhookEventCore &
SydneeWebhookEventDetails;The current object types are:
export type KnownSydneeWebhookObjectType =
| "account"
| "portalUser"
| "request"
| "task"
| "taskAttachment"
| "comment"
| "file"
| "deliverable"
| "taskBoardStatusUpdate"
| "liveChatMessage";
export type SydneeWebhookObjectType =
| KnownSydneeWebhookObjectType
| (string & {});Use these event-type groups to build the SydneeWebhookEventType union:
type AccountEventType =
| "account.created"
| "account.archived"
| "account.restored"
| "account.deleted";
type PortalUserEventType =
| "portalUser.added"
| "portalUser.removed"
| "portalUser.accessed";
type RequestEventType =
| "request.published"
| "request.unpublished"
| "request.completed"
| "request.field.completed"
| "request.reopened"
| "request.archived"
| "request.unarchived"
| "request.deleted";type TaskEventType =
| "task.created"
| "task.updated"
| "task.completed"
| "task.reopened"
| "task.archived"
| "task.unarchived"
| "task.deleted"
| "task.attachment.added";
type CommentEventType = "task.comment.created" | "file.comment.created";
type FileEventType =
| "file.uploaded"
| "file.version.uploaded"
| "file.renamed"
| "file.moved"
| "file.deleted";type DeliverableEventType =
| "deliverable.published"
| "deliverable.viewed"
| "deliverable.removed";
type OtherEventType =
| "service.requested"
| "service.ticket.created"
| "taskBoard.statusUpdate.created";
type LiveChatEventType = "liveChat.message.sent";
export type KnownSydneeWebhookEventType =
| AccountEventType
| PortalUserEventType
| RequestEventType
| TaskEventType
| CommentEventType
| FileEventType
| DeliverableEventType
| OtherEventType
| LiveChatEventType;
export type SydneeWebhookEventType =
| KnownSydneeWebhookEventType
| (string & {});Sydnee may add event types without changing version 1. In TypeScript, the string & {} branch keeps the type open while preserving editor hints for known events. Use GET /v1/webhook-event-types to load the current list.
Read each field
Use the event ID and version to control delivery. Store the ID before you start slower work. Use it to skip repeats. Read the event type and object ID together. The Type column uses TypeScript notation. Use the matching types in your language.
| Field | Type | Meaning |
|---|---|---|
id | string | Stable event ID. It matches X-Sydnee-Event-Id. Use it to detect duplicate deliveries. |
event_type | SydneeWebhookEventType | The change that occurred. |
event_version | 1 | Schema version for the JSON envelope. |
occurred_at | string | ISO 8601 time when the change occurred. |
operation_id | string | null | Groups events created by one operation. Treat it as opaque. |
account_id | string | null | Public account ID returned by the Accounts API when the event belongs to an account. |
actor_type | "portalUser" | "teamUser" | "system" | Who caused the event. |
actor_id | number | null | The account-scoped user ID returned by the account users API. Use actor_type to determine whether it identifies a portal user or team user. |
object_type | SydneeWebhookObjectType | Resource identified by object_id. |
object_id | string | number | Stable public ID for the resource named by object_type. Its JSON type matches the public API. |
task_board_id | string | null | Public Task Board ID for Task objects, Task comments, Task attachments, and Task Board status updates. Other events use null. |
parent_object_type | "task" | "file" | null | Parent resource type for a comment, Task attachment, or subtask. Other events use null. |
parent_object_id | string | number | null | Public Task ID or numeric file ID for the parent. Other events use null. |
resource_url | string | null | Absolute public API URL for an exact object GET. It is null when no exact read route is available. |
request_field_id | string | null | Completed field ID for request.field.completed. Other events use null. |
request_field_type | string | null | Request field type for request.field.completed. Other events use null. |
request_field_label | string | null | Request field label. It is set only when Include authored text and resource titles is on. |
request_field_value | SydneeWebhookJsonValue | Submitted non-sensitive Request answer. It is set only when Include authored text and resource titles is on. |
status | string | null | Resource state after the event. It is null when the resource has no status field. |
title | string | null | Resource name or title. It is set only when Include authored text and resource titles is on. |
email | string | null | Portal user email. It is set only when Include portal user email addresses is on. |
changes | string[] | Public API property names changed by the event. Other event families use an empty array. |
conversation_id | string | null | TalkJS conversation ID for liveChat.message.sent. Other events use null. |
conversation_type | "account" | "channel" | "dm" | null | Conversation type for liveChat.message.sent. Other events use null. |
message_origin | "web" | "rest" | "email" | "import" | null | How the portal user sent the Live Chat message. Other events use null. |
message_text | string | null | Up to 280 characters of portal-authored Live Chat text. It is set only when authored text is on. |
message_has_attachment | boolean | null | Whether the live chat message has an attachment. The attachment and its URL are not included. |
message_has_location | boolean | null | Whether the live chat message has a location. The coordinates are not included. |
Use actor_type to choose the teamUsers or portalUsers list. Then match actor_id directly to an id from GET /v1/accounts/{accountId}/users. The ID belongs to the client account in account_id. It is never a global team-user or portal-member ID.
For automated activity, actor_type is system and actor_id is null. Sometimes Sydnee knows the actor type but cannot safely find the account membership. In that case, actor_id is null. The actor_type remains portalUser or teamUser.
Look up the affected object
Use object_type and object_id together. Its JSON type matches the public API. Numeric resources use a number. String IDs and provider IDs use a string.
| Event family | Object identified by object_id | Follow-up lookup |
|---|---|---|
| Account | Public Account ID | Use resource_url while the account is readable. |
| Portal user | Numeric account-scoped user ID from the account users API | No exact-object GET exists. resource_url is null. |
| Request | Numeric Request ID | No public read route exists. resource_url is null. |
| Task | Public Task ID | Use resource_url. task_board_id contains the public Task Board ID. |
| Task attachment | Numeric Task attachment ID | Use resource_url to request a fresh signed download URL. The parent fields identify the Task. |
| Comment | Numeric comment ID | No exact-comment GET exists. Use the parent fields for context. |
| File | Numeric public file or file-version ID | Use resource_url while the file or version is readable. |
| Deliverable | Public Deliverable ID | No public read route exists. resource_url is null. |
| Service request | Public ID of the resulting Task | Use resource_url. task_board_id contains the public Task Board ID. |
| Task Board status update | Public status-update ID | No exact public read route exists. resource_url is null. |
| Live Chat message | TalkJS message ID | No public read route exists. resource_url is null. |
A non-null resource_url is an absolute URL on https://public-api.sydnee.app. Send the same Bearer API key you use for other public API requests. Sydnee sets this field only for an exact object GET. It supports accounts, Tasks, Task attachments, and files. Deleted or unreadable objects use null. Do not build a URL when this field is null. Those webhook payloads are self-contained.
For a comment, object_type is comment. The object_id is the numeric comment ID. parent_object_type is task or file. parent_object_id is the string Task ID or numeric file ID. A Task comment also includes task_board_id.
For a Task attachment, object_type is taskAttachment. Its object_id is the numeric attachment ID. The parent fields identify the Task. For a subtask, the parent fields identify the parent Task. Other events use null parent fields.
event_type describes the transition. status is the current persisted resource state after that transition.
| Resource | status values |
|---|---|
| Task lifecycle and update events | open or complete |
service.requested and service.ticket.created | open |
| Request | draft, scheduled, published, or completed |
| Task Board status update | ON_TRACK, AT_RISK, OFF_TRACK, ON_HOLD, COMPLETE, or DROPPED |
| Account, portal user, comment, Task attachment, file, Deliverable, and Live Chat message | null |
status never uses transition words such as created or deleted.
Choose what the endpoint receives
You can manage endpoints in Company Settings → Webhooks or through the /v1/webhooks API. The generated API reference lists each management route and request shape.
Each endpoint has four controls:
- Select one or more event types.
- Send events for every client account or only selected accounts.
- Send activity from all people, team members only, or portal users only.
- Choose whether to include authored text and portal user email addresses.
All people includes team members, portal users, and automated system activity. Team members only matches actor_type: "teamUser". Portal users only matches actor_type: "portalUser". Automated events use actor_type: "system". They are sent only when All people is selected.
An endpoint limited to selected client accounts receives only events with a matching account_id. The management API accepts those public IDs in accountIds. Accountless Live Chat channels and direct messages can go only to endpoints set to every account.
Handle event-specific fields
Most events need only the shared fields. These cases add more context:
request.field.completedsets the field ID and type. It also setschangesto["requestFieldValue"]. This matches the public API property name. When Include authored text and resource titles is enabled,request_field_labelandrequest_field_valuecontain the non-sensitive answer context. Theobject_idremains the Request ID.task.updatedadds one or more public Task property names tochanges. If you use TypeScript, keep this field typed asstring[]so new public properties remain forward compatible.- Task objects include
task_board_idand an exactresource_url. Subtasks identify their parent Task when the creating workflow supplies parent context. Comment and Task attachment events identify the child object and use the parent fields for context. portalUser.added,portalUser.removed, andportalUser.accessedcan includeemailwhen Include portal user email addresses is enabled for the endpoint.service.requestedidentifies the Task created when a portal user requests a service that is not yet active.service.ticket.createdidentifies the Task created when a portal user submits a ticket for an active service.- Both service events use
object_type: "task", the public Task ID inobject_id, and the public Task Board ID intask_board_id. liveChat.message.sentidentifies the TalkJS message inobject_idand adds conversation and message metadata. It is emitted only for portal-user messages.
The authored-text setting can add non-sensitive Request answers, resource titles, and portal-authored Live Chat text to supported events. Both content settings are off by default. A disabled setting returns null. It does not omit the key.
Sensitive Request answers and signature images are never included. The same rule applies to file-upload internals, comment text, file contents, direct storage URLs, location coordinates, and raw provider metadata. File uploads still produce the field-completed event. For those events, request_field_value remains null.
See an example payload
This example shows request.field.completed with authored text enabled and portal user email addresses turned off.
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"event_type": "request.field.completed",
"event_version": 1,
"occurred_at": "2026-09-15T18:42:11.000Z",
"operation_id": "event-1234567890123",
"account_id": "account_example_123",
"actor_type": "portalUser",
"actor_id": 2903,
"object_type": "request",
"object_id": 1234567890123,
"task_board_id": null,
"parent_object_type": null,
"parent_object_id": null,
"resource_url": null,
"request_field_id": "1234567890124",
"request_field_type": "email",
"request_field_label": "Personal email address",
"request_field_value": "client@example.com",
"status": "published",
"title": null,
"email": null,
"changes": ["requestFieldValue"],
"conversation_id": null,
"conversation_type": null,
"message_origin": null,
"message_text": null,
"message_has_attachment": null,
"message_has_location": null
}Use the current event catalog
Choose events in Company Settings → Webhooks, or get the live list from GET /v1/webhook-event-types.
Account events
Account events use object_type: "account". object_id is the public Account ID. A readable account uses https://public-api.sydnee.app/v1/accounts/{accountId} as resource_url. Archived, deleted, or otherwise unreadable accounts use null.
| Event type | Sent when |
|---|---|
account.created | A client account is created. |
account.archived | A client account is moved to the archive. |
account.restored | An archived client account is restored. |
account.deleted | A client account is deleted. |
Portal-user events
Portal user events use object_type: "portalUser". object_id is the affected user's numeric account-scoped membership ID. It matches an id returned by GET /v1/accounts/{accountId}/users. That API returns a list rather than one exact user. As a result, resource_url is null.
| Event type | Sent when |
|---|---|
portalUser.added | A portal user gains access to a client account. |
portalUser.removed | A portal user’s access to a client account is removed. |
portalUser.accessed | A portal user starts a new access session for a client account. |
Request events
Request events use object_type: "request". object_id is the numeric Request ID. resource_url is null because Requests do not have a public read route.
| Event type | Sent when |
|---|---|
request.published | A Request is published for portal users. |
request.unpublished | A published Request returns to an unpublished state. |
request.completed | A portal user submits a Request, or a team member marks it complete. |
request.field.completed | A portal user saves a response that completes a field. Sydnee sends this once. Later edits do not repeat it. |
request.reopened | A completed Request is reopened. |
request.archived | A Request is moved to the archive. |
request.unarchived | An archived Request is restored. |
request.deleted | A Request is deleted. |
Task events
Task lifecycle and update events use object_type: "task". object_id is the public Task ID, and task_board_id is the public Task Board ID. When the Task remains readable, resource_url has this form:
https://public-api.sydnee.app/v1/accounts/{account_id}/task-boards/{task_board_id}/tasks/{object_id}task.deleted uses resource_url: null. Other Task events include the URL, including events for an archived Task.
| Event type | Sent when |
|---|---|
task.created | A Task is created. |
task.updated | A supported Task detail or link changes. Sydnee queues the event when it saves the change. It does not add a wait. |
task.completed | A Task is completed. Sydnee schedules the event 45 seconds later so the user has 30 seconds to select Undo. |
task.reopened | A completed Task is reopened. Sydnee schedules the event 45 seconds later so the user has 30 seconds to select Undo. |
task.archived | A Task is moved to the archive. |
task.unarchived | An archived Task is restored. |
task.deleted | A Task is deleted. |
task.attachment.added | An attachment is added to a Task. |
Subtask events set parent_object_type: "task" and put the parent Task ID in parent_object_id whenever the Task has a parent.
task.attachment.added is a separate Task event. It uses object_type: "taskAttachment". The numeric attachment ID is in object_id, and the Task ID is in parent_object_id. Its resource_url calls the public API route that returns a fresh signed download URL:
https://public-api.sydnee.app/v1/accounts/{account_id}/task-boards/{task_board_id}/tasks/{parent_object_id}/attachments/{object_id}/downloadtask.updated covers:
- Title, details, due date, or priority
- Section or Task type
- Assigned people
- Tags or links between Tasks
- Portal user view or edit access
Each item in changes names what changed. These are the current values:
type TaskUpdatedChange =
| "assignees"
| "collaborators"
| "description"
| "due"
| "title"
| "sectionId"
| "clientAccess"
| "priority"
| "kind"
| "isBlocked"
| "tags"
| (string & {});Completion and reopening use their own events, not task.updated. Sydnee holds those events for 45 seconds. If the user selects Undo in that time, Sydnee cancels the event.
Comment events
Comment events use object_type: "comment" and put the actual comment ID in object_id. They identify the parent separately. Comment text is not included. The public API has no exact-comment GET, so resource_url is null.
| Event type | Parent type | Parent ID | Sent when |
|---|---|---|---|
task.comment.created | task | Public Task ID; also sets task_board_id | A comment is added to a Task. |
file.comment.created | file | Numeric public file ID | A comment is added to a file. |
File events
File events use object_type: "file". object_id is the numeric file or file-version ID. While it remains readable, resource_url points to the exact public API file route. file.deleted uses resource_url: null.
| Event type | Sent when |
|---|---|
file.uploaded | A new file is uploaded. |
file.version.uploaded | A new version is uploaded for an existing file. |
file.renamed | A file’s name changes. |
file.moved | A file moves to another folder. |
file.deleted | A file is deleted. |
Deliverable events
Deliverable events use object_type: "deliverable". object_id is the public Deliverable ID. resource_url is null because Deliverables do not have a public read route.
| Event type | Sent when |
|---|---|
deliverable.published | A folder is published as a Deliverable for portal users. |
deliverable.viewed | A portal user opens a published Deliverable for the first time. |
deliverable.removed | A Deliverable is removed. |
A view records that the Deliverable page opened. It does not record approval.
Service and status-update events
These events cover service requests from portal users and Task Board updates.
| Event type | Object type | Sent when |
|---|---|---|
service.requested | task | A portal user requests a service that is not yet active. |
service.ticket.created | task | A portal user creates a ticket for an active service. |
taskBoard.statusUpdate.created | taskBoardStatusUpdate | A status update is posted to a Task Board. |
For service.requested and service.ticket.created, object_id, task_board_id, and resource_url identify the resulting Task and its board. For taskBoard.statusUpdate.created, object_id is the public status-update ID. The task_board_id is the public Task Board ID, and resource_url is null.
The service event represents the complete Task creation. Follow resource_url to read its initial assignees and collaborators. Sydnee does not send separate task.updated events for those relationships during creation. Changes made afterward continue to send task.updated normally.
Live Chat events
liveChat.message.sent uses object_type: "liveChatMessage". object_id is the TalkJS message ID. resource_url is null because Live Chat messages do not have a public API read route.
| Event type | Sent when |
|---|---|
liveChat.message.sent | A portal user sends a Live Chat message. The message can contain text, an attachment, or a location. |
The event works with account chats. It also works with channels and direct messages. It skips team and system messages. It skips read receipts and typing state. It also skips notices and repaired past events.
For a channel or direct message without a client-account link, account_id is null. An endpoint may be limited to selected client accounts. It gets the event only when the chat belongs to one of those accounts. An endpoint set to every account can also get accountless chats.
message_has_attachment and message_has_location show the form of the message. They do not expose an attachment URL or location. message_text is null unless Include authored text and resource titles is on. When it is on, the field has a preview of up to 280 characters.
Verify the signature
Sydnee signs timestamp + "." + exact_raw_body with a hash-based message authentication code using SHA-256 (HMAC-SHA256). Check the signature before you parse or process the body.
Live events, tests, and checks include the headers below. The previous-signature header appears only during secret rotation.
| Header | Value |
|---|---|
X-Sydnee-Webhook-Timestamp | Unix time in seconds used for the signature. |
X-Sydnee-Signature | Current signature in v1=hex_digest form. |
X-Sydnee-Signature-Previous | Optional previous-secret signature during the 24-hour rotation overlap. |
X-Sydnee-Event-Id | Stable event ID. |
X-Sydnee-Delivery-Id | Delivery ID for this endpoint. |
X-Sydnee-Attempt-Id | Unique ID for this delivery attempt. |
This TypeScript example for Node.js checks the exact body bytes. It does not check timestamp age. Reject stale timestamps based on the clock skew your endpoint allows.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySydneeWebhook(
rawBody: Buffer,
timestamp: string,
signature: string,
secret: string,
) {
if (!/^v1=[0-9a-f]{64}$/i.test(signature)) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
const received = Buffer.from(signature.slice(3), "hex");
return (
received.length === expected.length && timingSafeEqual(received, expected)
);
}Keep the signing secret in a server-side secret store. Sydnee reveals a new secret once when you create an endpoint or rotate its secret.
Handle retries and duplicates
Webhook delivery is at least once. Your server may finish a request before a timeout is known. A later try can then send the same event.
- Verify the signature against the raw body.
- Store
idwith a unique constraint. - Return a
2xxresponse after the event is stored. - Process slower work from your own queue.
- Ignore an event whose
idwas already stored.
Sydnee retries network failures, timeouts, 408, 425, 429, and 5xx responses. After the first failed attempt, it can retry up to six times. The first waits are 1 minute, 5 minutes, and 30 minutes. Later waits are 2 hours, 8 hours, and 24 hours. A valid Retry-After header can delay the next retry. It cannot shorten the default wait. The delay is capped at 24 hours. Other 4xx responses end that delivery. Sydnee does not follow redirects.
Distinguish test and verification deliveries
Destination checks and Send test posts are callbacks, not SydneeWebhookEvent objects. Their body has zero bytes. They still include the signing and delivery headers. The signature covers that exact empty body. Verify the signature, but do not pass these callbacks to your JSON parser.
- A verification delivery uses
webhook.verificationin delivery history. ItsX-Sydnee-Event-Idstarts withverification_. - A test delivery uses
webhook.testin delivery history. ItsX-Sydnee-Event-Idstarts withtest_. - Return
2xxfor the empty request after its signature passes.
For an empty-body post, use X-Sydnee-Delivery-Id to detect a retry. A delivery ID stays the same across retries. X-Sydnee-Attempt-Id changes for each try.
Only live event posts contain a SydneeWebhookEvent JSON body.
Manage endpoint delivery
A new endpoint starts in verification. Sydnee sends an empty-body check to the destination. A 2xx response makes the endpoint active. A failed check appears in Event deliveries. Fix the destination, then retry the check there.
Custom destinations must use public HTTPS on port 443. Do not include credentials or URL fragments. The host cannot be local, private, or reserved. Sydnee checks the public address before delivery and does not follow redirects. The connection timeout is 3 seconds. The total request timeout is 10 seconds. Requests and responses have a 64 KB size limit.
The endpoint detail page has Overview and Event deliveries tabs. Delivery history keeps 30 days of data. The newest 50 deliveries load first. Scroll to load 50 more. While a loaded delivery is active, the table refreshes every 3 seconds until it finishes. A canceled row explains its reason when you hover over it or focus it with a keyboard.
Disabling an endpoint stops new deliveries and cancels queued work. Enabling it later does not replay missed events. Deleting an endpoint is permanent and also cancels queued work.
Rotating the signing secret shows the new secret once. The old secret remains valid for 24 hours, and Sydnee sends both signatures during that overlap.
When webhooks are unavailable
Outbound webhooks are in private testing and are not yet generally available. Workspace owners with access can manage endpoints. Premium and Power Team plans can configure up to five custom endpoints. Other eligible API plans can configure one. A free trial without a payment method cannot create or use webhooks.
Sydnee does not replay events that occurred before an endpoint was created or while delivery was disabled.
Report API errors from an AI agent
If an AI agent gets an error from the Sydnee Public API, encourage it to submit a report to POST https://public-api.sydnee.app/v1/agent-report. Include the endpoint and HTTP status. Also include the error response and the task the agent was trying to complete. This helps Sydnee find repeated failures and investigate them sooner.
The report endpoint does not require an API key. Send JSON with the required fields below. requestBody is optional.
{
"endpoint": "PATCH /v1/accounts/{accountId}",
"errorCode": "409",
"errorMessage": "Describe the API error response",
"context": "Describe what the agent was trying to complete"
}A valid report returns 200 with { "received": true }. Sydnee stores the complete JSON report in Sentry so the team can investigate the failure. This includes endpoint IDs and query strings, errorCode, errorMessage, context, all requestBody values, and any extra JSON fields. HTTP headers and cookies are not stored. Never include API keys, signing secrets, or client data because every JSON field you submit is retained.