Skip to content
Skip to content

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.

FieldTypeMeaning
idstringStable event ID. It matches X-Sydnee-Event-Id. Use it to detect duplicate deliveries.
event_typeSydneeWebhookEventTypeThe change that occurred.
event_version1Schema version for the JSON envelope.
occurred_atstringISO 8601 time when the change occurred.
operation_idstring | nullGroups events created by one operation. Treat it as opaque.
account_idstring | nullPublic account ID returned by the Accounts API when the event belongs to an account.
actor_type"portalUser" | "teamUser" | "system"Who caused the event.
actor_idnumber | nullThe 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_typeSydneeWebhookObjectTypeResource identified by object_id.
object_idstring | numberStable public ID for the resource named by object_type. Its JSON type matches the public API.
task_board_idstring | nullPublic Task Board ID for Task objects, Task comments, Task attachments, and Task Board status updates. Other events use null.
parent_object_type"task" | "file" | nullParent resource type for a comment, Task attachment, or subtask. Other events use null.
parent_object_idstring | number | nullPublic Task ID or numeric file ID for the parent. Other events use null.
resource_urlstring | nullAbsolute public API URL for an exact object GET. It is null when no exact read route is available.
request_field_idstring | nullCompleted field ID for request.field.completed. Other events use null.
request_field_typestring | nullRequest field type for request.field.completed. Other events use null.
request_field_labelstring | nullRequest field label. It is set only when Include authored text and resource titles is on.
request_field_valueSydneeWebhookJsonValueSubmitted non-sensitive Request answer. It is set only when Include authored text and resource titles is on.
statusstring | nullResource state after the event. It is null when the resource has no status field.
titlestring | nullResource name or title. It is set only when Include authored text and resource titles is on.
emailstring | nullPortal user email. It is set only when Include portal user email addresses is on.
changesstring[]Public API property names changed by the event. Other event families use an empty array.
conversation_idstring | nullTalkJS conversation ID for liveChat.message.sent. Other events use null.
conversation_type"account" | "channel" | "dm" | nullConversation type for liveChat.message.sent. Other events use null.
message_origin"web" | "rest" | "email" | "import" | nullHow the portal user sent the Live Chat message. Other events use null.
message_textstring | nullUp to 280 characters of portal-authored Live Chat text. It is set only when authored text is on.
message_has_attachmentboolean | nullWhether the live chat message has an attachment. The attachment and its URL are not included.
message_has_locationboolean | nullWhether 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 familyObject identified by object_idFollow-up lookup
AccountPublic Account IDUse resource_url while the account is readable.
Portal userNumeric account-scoped user ID from the account users APINo exact-object GET exists. resource_url is null.
RequestNumeric Request IDNo public read route exists. resource_url is null.
TaskPublic Task IDUse resource_url. task_board_id contains the public Task Board ID.
Task attachmentNumeric Task attachment IDUse resource_url to request a fresh signed download URL. The parent fields identify the Task.
CommentNumeric comment IDNo exact-comment GET exists. Use the parent fields for context.
FileNumeric public file or file-version IDUse resource_url while the file or version is readable.
DeliverablePublic Deliverable IDNo public read route exists. resource_url is null.
Service requestPublic ID of the resulting TaskUse resource_url. task_board_id contains the public Task Board ID.
Task Board status updatePublic status-update IDNo exact public read route exists. resource_url is null.
Live Chat messageTalkJS message IDNo 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.

Resourcestatus values
Task lifecycle and update eventsopen or complete
service.requested and service.ticket.createdopen
Requestdraft, scheduled, published, or completed
Task Board status updateON_TRACK, AT_RISK, OFF_TRACK, ON_HOLD, COMPLETE, or DROPPED
Account, portal user, comment, Task attachment, file, Deliverable, and Live Chat messagenull

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:

  1. Select one or more event types.
  2. Send events for every client account or only selected accounts.
  3. Send activity from all people, team members only, or portal users only.
  4. 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.completed sets the field ID and type. It also sets changes to ["requestFieldValue"]. This matches the public API property name. When Include authored text and resource titles is enabled, request_field_label and request_field_value contain the non-sensitive answer context. The object_id remains the Request ID.
  • task.updated adds one or more public Task property names to changes. If you use TypeScript, keep this field typed as string[] so new public properties remain forward compatible.
  • Task objects include task_board_id and an exact resource_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, and portalUser.accessed can include email when Include portal user email addresses is enabled for the endpoint.
  • service.requested identifies the Task created when a portal user requests a service that is not yet active.
  • service.ticket.created identifies 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 in object_id, and the public Task Board ID in task_board_id.
  • liveChat.message.sent identifies the TalkJS message in object_id and 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 typeSent when
account.createdA client account is created.
account.archivedA client account is moved to the archive.
account.restoredAn archived client account is restored.
account.deletedA 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 typeSent when
portalUser.addedA portal user gains access to a client account.
portalUser.removedA portal user’s access to a client account is removed.
portalUser.accessedA 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 typeSent when
request.publishedA Request is published for portal users.
request.unpublishedA published Request returns to an unpublished state.
request.completedA portal user submits a Request, or a team member marks it complete.
request.field.completedA portal user saves a response that completes a field. Sydnee sends this once. Later edits do not repeat it.
request.reopenedA completed Request is reopened.
request.archivedA Request is moved to the archive.
request.unarchivedAn archived Request is restored.
request.deletedA 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 typeSent when
task.createdA Task is created.
task.updatedA supported Task detail or link changes. Sydnee queues the event when it saves the change. It does not add a wait.
task.completedA Task is completed. Sydnee schedules the event 45 seconds later so the user has 30 seconds to select Undo.
task.reopenedA completed Task is reopened. Sydnee schedules the event 45 seconds later so the user has 30 seconds to select Undo.
task.archivedA Task is moved to the archive.
task.unarchivedAn archived Task is restored.
task.deletedA Task is deleted.
task.attachment.addedAn 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}/download

task.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 typeParent typeParent IDSent when
task.comment.createdtaskPublic Task ID; also sets task_board_idA comment is added to a Task.
file.comment.createdfileNumeric public file IDA 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 typeSent when
file.uploadedA new file is uploaded.
file.version.uploadedA new version is uploaded for an existing file.
file.renamedA file’s name changes.
file.movedA file moves to another folder.
file.deletedA 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 typeSent when
deliverable.publishedA folder is published as a Deliverable for portal users.
deliverable.viewedA portal user opens a published Deliverable for the first time.
deliverable.removedA 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 typeObject typeSent when
service.requestedtaskA portal user requests a service that is not yet active.
service.ticket.createdtaskA portal user creates a ticket for an active service.
taskBoard.statusUpdate.createdtaskBoardStatusUpdateA 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 typeSent when
liveChat.message.sentA 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.

HeaderValue
X-Sydnee-Webhook-TimestampUnix time in seconds used for the signature.
X-Sydnee-SignatureCurrent signature in v1=hex_digest form.
X-Sydnee-Signature-PreviousOptional previous-secret signature during the 24-hour rotation overlap.
X-Sydnee-Event-IdStable event ID.
X-Sydnee-Delivery-IdDelivery ID for this endpoint.
X-Sydnee-Attempt-IdUnique 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.

  1. Verify the signature against the raw body.
  2. Store id with a unique constraint.
  3. Return a 2xx response after the event is stored.
  4. Process slower work from your own queue.
  5. Ignore an event whose id was 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.verification in delivery history. Its X-Sydnee-Event-Id starts with verification_.
  • A test delivery uses webhook.test in delivery history. Its X-Sydnee-Event-Id starts with test_.
  • Return 2xx for 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.

Use webhook data with the API