Skip to content

Start typing to search the documentation.

Zendesk Channel API

AI-generated, awaiting review View as Markdown

Import from @flue/zendesk.

Exports

export {
  createZendeskChannel,
  InvalidZendeskInputError,
  InvalidZendeskTicketKeyError,
  type ChannelRoute,
  type JsonObject,
  type JsonValue,
  type ZendeskChannel,
  type ZendeskChannelOptions,
  type ZendeskDelivery,
  type ZendeskEvent,
  type ZendeskHandlerResult,
  type ZendeskTicketRef,
  type ZendeskWebhookHandlerInput,
};

createZendeskChannel()

function createZendeskChannel<E extends Env = Env>(
  options: ZendeskChannelOptions<E>,
): ZendeskChannel<E>;

Creates one stateless signed Zendesk event-subscription channel.

ZendeskChannelOptions

interface ZendeskChannelOptions<E extends Env = Env> {
  signingSecret: string;
  accountId?: string;
  webhookId?: string;
  bodyLimit?: number;
  webhook(input: ZendeskWebhookHandlerInput<E>): ZendeskHandlerResult;
}
FieldDescription
signingSecretZendesk webhook signing secret used for exact-body HMAC verification.
accountIdOptional expected payload and header account id.
webhookIdOptional expected X-Zendesk-Webhook-Id.
bodyLimitMaximum request-body size in bytes. Defaults to 1 MiB.
webhookReceives every verified, structurally valid event-subscription payload.

Configured secrets and ids must be non-empty. bodyLimit must be a positive integer.

Routes

interface ZendeskChannel<E extends Env = Env> {
  readonly routes: readonly ChannelRoute<E>[];
  ticketKey(ref: ZendeskTicketRef): string;
  parseTicketKey(id: string): ZendeskTicketRef;
}

routes contains one POST /webhook declaration. A file named channels/zendesk.ts is served at POST /channels/zendesk/webhook relative to the flue() mount.

Handler input

interface ZendeskWebhookHandlerInput<E extends Env = Env> {
  c: Context<E>;
  payload: ZendeskEvent;
  delivery: ZendeskDelivery;
}

c is the authentic Hono context. payload is the verified provider-native event-subscription envelope; delivery is the unsigned routing metadata read from the request headers. The callback runs only after content type, body limit, exact-body signature, UTF-8, JSON envelope, account consistency, and optional configured identity checks pass.

ZendeskEvent

The provider-native common event envelope, preserving Zendesk’s own snake_case field names, nesting, and discriminants.

interface ZendeskEvent<
  TDetail extends JsonObject = JsonObject,
  TEvent extends JsonObject = JsonObject,
> {
  account_id: string;
  id: string;
  type: string;
  subject: string;
  time: string;
  zendesk_event_version: string;
  event: TEvent;
  detail: TDetail;
  [key: string]: JsonValue;
}
FieldMeaning
account_idZendesk account id, normalized to a positive decimal string.
idProvider event id. Use it as a replay-resistant deduplication key.
typeOpen provider event type, e.g. zen:event-type:ticket.created.
subjectProvider resource subject, e.g. zen:ticket:<id>.
timeProvider event occurrence timestamp.
zendesk_event_versionOpen provider schema version, e.g. 2022-06-20.
eventProvider-native change object. Properties vary by event type.
detailProvider-native resource object. Properties vary by event domain.

type and zendesk_event_version remain open strings, and the index signature forwards any authenticated future or unmodeled fields. Verified future events reach the handler. Narrow detail and event through the TDetail/TEvent generics for the families you consume, and validate the fields you use.

JSON is parsed with lossless-json: safe numeric literals remain numbers, while unsafe integer literals retain their exact decimal strings. The required integer account_id is normalized to a positive decimal string and checked against the provider account header.

ZendeskDelivery

Unsigned provider delivery metadata from the request headers. Zendesk’s HMAC covers only the signature timestamp and request body, not these headers, so they are routing and attempt-correlation context, never an authorization capability.

interface ZendeskDelivery {
  webhookId: string;
  invocationId: string;
  signatureTimestamp: string;
}
FieldProvider sourceMeaning
webhookIdX-Zendesk-Webhook-IdWebhook configuration identity.
invocationIdX-Zendesk-Webhook-Invocation-IdUnsigned provider attempt-correlation identity.
signatureTimestampX-Zendesk-Webhook-Signature-TimestampExact timestamp included in the HMAC input.

Prefer the signed payload.id for deduplication; invocationId only correlates provider retry attempts.

Verification

POST /webhook requires application/json and non-empty:

  • X-Zendesk-Account-Id;
  • X-Zendesk-Webhook-Id;
  • X-Zendesk-Webhook-Invocation-Id;
  • X-Zendesk-Webhook-Signature;
  • X-Zendesk-Webhook-Signature-Timestamp.

The signature must be base64 HMAC-SHA256 over the exact signature timestamp concatenated directly with the exact request bytes. Verification occurs before decoding or parsing.

The HMAC covers the timestamp and body, not the identity headers. The package requires the headers, checks body and header account identity for consistency, and applies configured account and webhook restrictions. Header metadata is not an authorization capability.

Zendesk documents no signature timestamp age or clock-skew rule. The package does not reject an otherwise valid signature based on age.

Unsupported media types receive 415; malformed input, identity metadata, or signature-timestamp metadata receives 400; oversized bodies receive 413; missing, malformed, or changed signatures receive 401; configured identity mismatches receive 403.

Handler result

type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };

type ZendeskHandlerResult =
  | undefined
  | JsonValue
  | Response
  | Promise<undefined | JsonValue | Response>;

Returning nothing produces an empty 200. A JSON-compatible value becomes a JSON response. A normal Hono or Fetch Response passes through unchanged. A thrown callback or unsupported return value produces an empty 409, which Zendesk retries specifically.

Zendesk allows 12 seconds for the complete request. The channel does not enforce a deadline, because racing the callback against a timer cannot actually cancel JavaScript work that has already started. Admit durable work promptly (for example dispatch(...) then return) and rely on idempotency rather than blocking on slow work before acknowledging.

Ticket identity

interface ZendeskTicketRef {
  accountId: string;
  ticketId: string;
}

ticketKey() serializes canonical account-scoped identity. parseTicketKey() accepts only keys produced by the canonical format. The application must derive and validate ticketId from a ticket event it handles; the package does not claim every Zendesk event refers to a ticket.

Ticket keys identify application state. They do not authorize an outbound API request or select account credentials.

Errors

  • InvalidZendeskInputError, with structured field, is thrown for an invalid ticket reference.
  • InvalidZendeskTicketKeyError is thrown for a malformed or non-canonical ticket key.

Delivery and application boundary

Zendesk can duplicate or omit delivery. It retries 409 up to three times, conditionally retries 429 and 503 with a short Retry-After, retries timeouts up to five times, and can pause failing endpoints through its circuit breaker. Persist the signed payload.id in application-owned storage when duplicate admission is unacceptable. delivery.invocationId is unsigned metadata for attempt correlation.

This package supports provider-defined JSON event subscriptions. Custom trigger and automation payloads, Sunshine Conversations, and Zendesk AI Agent webhooks have different or incomplete contracts and are not accepted by this route.

Webhook creation, subscription selection, destination authentication, OAuth, token lookup, deduplication, persistence, ticket policy, outbound clients, and tools remain application concerns.

@flue/zendesk depends on Hono and lossless-json. It does not depend on a Zendesk SDK or @flue/runtime.

See Zendesk setup for project-owned Fetch composition, ticket-bound tools, retry behavior, and Node/workerd testing.