webhookDevelop Custom Channels

Build custom Hexabot channels that parse webhooks, map subscribers, and send platform-specific messages.

A Hexabot channel is the adapter between an external messaging platform and Hexabot workflows. It receives platform webhook events, converts them into Hexabot inbound events, and converts Hexabot outgoing envelopes back into the platform format.

Most custom channels should start as HTTP webhook channels. Use a WebSocket channel only when you are building an interactive client that keeps a live socket connected, like the built-in web widget.

What A Channel Must Do

A working channel has four responsibilities:

  1. Define source settings with zod. These are the per-source credentials and options shown in the admin UI.

  2. Decode inbound payloads. Parse req.body, validate it, and return one or more ChannelInboundEvent instances.

  3. Resolve subscriber data. Map the platform user to SubscriberCreateDto.

  4. Send outbound messages. Convert StdOutgoingMessageEnvelope to the platform API payload and call the platform API.

The simplest route is:

  • Extend HttpChannelHandler.

  • Implement decode(), doSendMessage(), and getSubscriberData().

  • Override verifyWebhook() and verifySignature() only when the platform requires a handshake or signed webhooks.

Discovery Rules

Hexabot discovers channel providers dynamically at startup. Your compiled output must contain at least one file that matches one of these patterns:

  • Built-in API channels: node_modules/@hexabot-ai/api/dist/extensions/channels/**/*.channel.js

  • Installed channel packages: node_modules/hexabot-channel-*/**/*.channel.js

  • Local project channels: dist/extensions/channels/**/*.channel.js

For this starter project, put local channels under:

After build, that becomes:

If the channel is not returned by GET /api/channel, first check that the compiled .channel.js file exists and that the handler class is decorated with @Injectable().

For a real channel, keep the transport class small and move parsing and message formatting into helpers:

For a first version, index.channel.ts, settings.schema.ts, types.ts, and globals.d.ts are enough. Add split decoders, encoders, and services when the handler starts growing.

Source Settings

Source settings are per source. They belong to the channel handler constructor, not to *.settings.ts dynamic runtime settings files.

Use .meta() because Hexabot converts this zod schema into the JSON schema returned by GET /api/channel.

Use z.strictObject() for payloads you control. For third-party webhooks that may add fields without notice, prefer z.looseObject() around the external payload and validate only the fields you actually use.

Channel Attribute Typing

Channel attributes are the platform-specific data stored on the subscriber channel object. Type them once so event and subscriber code stays readable.

These attributes are not source settings. They describe the user/channel relationship, such as page id, account id, tenant id, or device id.

Platform Payload Types

Define external contracts with zod first, then infer TypeScript types.

The decoder should receive unknown, parse it immediately, and only work with the parsed type after that.

Minimal HTTP Channel

This is a compact text-only channel. It supports inbound text and payload messages, sends outbound text, and leaves rich messages for later.

Important behavior to understand:

  • HttpChannelHandler routes GET requests to verifyWebhook().

  • For POST, it runs verifySignature(), then decode().

  • After successful decode it sends HTTP 200, then dispatches events to the workflow pipeline.

  • Keep decode() cheap. Do not fetch profiles, download files, or call the platform API from decode().

When To Split Decoder And Encoder

The minimal handler is fine while the platform supports only text. Split the codec when you add several event types or rich outgoing formats.

The built-in web channel uses this pattern:

  • WebInboundEventDecoder parses the raw event and returns concrete inbound event classes.

  • WebOutboundMessageEncoder maps Hexabot envelopes to web channel payloads.

  • The handler composes both with @ExtensionInject().

A custom channel can use the same pattern:

@ExtensionInject() is useful for helper classes that should be created for the current channel handler at module initialization. Use normal Nest @Inject() for ordinary singleton services.

Webhook Verification And Signatures

Many platforms do two separate checks:

  • GET /api/webhook/:sourceRef verifies the webhook subscription.

  • POST /api/webhook/:sourceRef verifies every incoming payload.

Override verifyWebhook() for challenge-response handshakes. Override verifySignature() for HMAC or token verification.

Throw from verifySignature() to reject the request with HTTP 401.

Outgoing Message Support

Declare the platform limits in getCapabilities(). This prevents Hexabot from trying to send message types that the channel cannot handle.

Typical capability choices:

  • Start with text only.

  • Enable quick replies when the platform has a native equivalent.

  • Enable buttons only if you can preserve title, payload, and URL semantics.

  • Enable list/carousel only when you can map content fields reliably.

  • Set maxTextLength to the platform limit, or 0 if there is no known limit.

When you add rich messages, put the formatting logic in an outbound encoder that extends ChannelOutboundMessageEncoder. The encoder should use dispatchEnvelope() and handle every OutgoingMessageType, even if some handlers just throw because the platform does not support them.

Attachments

Inbound attachments usually need two steps:

  1. Decode the platform payload into an attachment inbound event.

  2. Implement getMessageAttachments(event) on the handler so Hexabot can download and persist the files.

Outbound attachments usually use:

The default URL is a signed Hexabot download URL under:

Override getAttachmentPublicUrl() if the platform requires files to be uploaded to its own media API first, or if the platform cannot access signed download URLs.

Building As An NPM Package

Use the hexabot-channel- package prefix so dynamic discovery can find it:

The compiled package must include a dist/**/*.channel.js file. The source settings schema should still be passed to the handler constructor with:

Do not put per-source channel settings in *.settings.ts files. That pattern is reserved for global runtime setting groups discovered by the settings module.

Registering And Testing

  1. Build the project:

  2. Start Hexabot:

  3. Confirm the channel is registered:

  4. Create or enable a source for the channel from the admin UI or source API. The platform webhook URL should use the source reference:

  5. Send a test payload:

If the request returns 400, the zod payload schema rejected the body. If it returns 401, verifySignature() rejected the request. If it returns 200 but no workflow runs, check that the source is active, has a default workflow or the URL contains a workflow id, and that the event has a sender foreign id.

Reference Implementations

Use these v3 implementations as references:

  • Built-in web channel in this project dependency: node_modules/@hexabot-ai/api/src/extensions/channels/web

  • Facebook channel package: https://github.com/Hexastack/hexabot-channel-facebook

The web channel is useful for WebSocket and history/session patterns. The Facebook channel is useful for HTTP webhook patterns: source settings, zod schemas, signature verification, batched inbound decoding, outbound rich message encoding, profile lookup, attachment download, and channel health checks.

Troubleshooting Checklist

  • Channel missing from GET /api/channel: build output does not contain dist/extensions/channels/**/*.channel.js, the class is not @Injectable(), or the file does not export the handler as the default export.

  • Settings missing in the admin UI: the handler did not pass the zod schema to super(name, schema).

  • Payload always rejected: parse only the fields you need, and use z.looseObject() for third-party payloads that may contain extra fields.

  • Platform retries webhooks: make sure signature verification and decoding pass quickly enough to return HTTP 200.

  • Messages are created but no replies are sent: verify getCapabilities() and doSendMessage(), then check the platform API response and credentials.

  • Subscriber data fails validation: return a valid source, channel, foreignId, labels, assignment fields, and profile fields from getSubscriberData().

Last updated

Was this helpful?