Skip to main content
Every Wassist phone number has a default routing mode, and every conversation can optionally override it. The resolved mode — the effective routing — decides what happens when a customer sends a message.

The routing modes

ModeWhat happens on inboundWhen to use it
agentThe connected agent replies as normalDefault for numbers wired to an agent.
webhookThe message is forwarded to a single webhook; the agent is not runWhen you want to handle replies in your own service.
nullThe message is stored, nothing else happensPause routing without disconnecting the agent or webhook.
sandboxInternal sandbox flow (shared system number)System-managed only — see the note below.
“No routing” is represented as null on the wire — there is no "none" string value. A number or conversation with defaultRouting: null (or routing: null on the conversation) simply stores incoming messages and does nothing else.
sandbox is a system-managed mode for shared Wassist test numbers (numbers with no WhatsApp Business Account). It lets you chat with yourself for testing. You cannot set sandbox from the API or dashboard; any attempt to do so returns 400. Sandbox numbers also ignore per-conversation routing overrides.

How effective routing is resolved

In code terms:
  1. If the number is a sandbox number → sandbox, full stop.
  2. Otherwise, if the conversation has its own non-null routing, use it.
  3. Otherwise, fall back to the number’s defaultRouting (which itself may be null — meaning “no routing”).
The same precedence applies to the webhook itself: conversation.webhookOverride wins over whatsappNumber.defaultWebhook.

Subscription lifecycle events

Whenever a conversation’s effective (routing, webhook) pair transitions to or away from a webhook, Wassist fires lifecycle events to the affected webhook:
TransitionEvent
not-webhook → webhook Wsubscription.activated on W
webhook W → not-webhooksubscription.revoked on W
webhook W1 → webhook W2subscription.revoked on W1, subscription.activated on W2
same → same(none)
The triggers are:
Phone-number-level routing changes (the /phone-numbers/{n}/subscribe, /connect-agent, and /unsubscribe endpoints) do not fire subscription lifecycle events, even when applyToExisting is true. Number-wide changes are bulk admin actions — if your service needs to know about every conversation, subscribe to it individually.

Inbound message dispatch

Once a conversation is in webhook routing:
  • subscription.message.received is dispatched only to the assigned webhook (no fan-out).
  • The agent pipeline is skipped entirely.
  • The legacy message.received event still fan-outs to every active webhook subscribed to it, so existing integrations keep working.

Service window lifecycle

WhatsApp’s 24-hour customer service window is tracked per conversation in webhook mode:
  • subscription.service_window.expiring fires roughly 1 hour before the window closes for a given conversation, once per window.
  • subscription.service_window.closed fires when the window has closed.
  • The tracker resets the moment a new inbound user message lands.

Event payloads

All subscription.* events use the same envelope as message.received, with two extra fields:
{
  "event": "subscription.message.received",
  "timestamp": "2026-06-23T16:48:00.000Z",
  "phoneNumber": "+447700900100",
  "from": "+447700900200",
  "contact": { "name": "Alex", "phoneNumber": "+447700900200" },
  "message": { /* same shape as message.received */ },
  "conversationId": "5a3f...e2",
  "routing": "webhook",
  "webhookId": "9c1c...a0"
}
For lifecycle events (subscription.activated, .revoked, the service window pair) the message field is null — only conversation context is sent.

Setting routing from your code

import { WassistClient } from "@wassist/sdk";

const client = new WassistClient({ apiKey: process.env.WASSIST_API_KEY! });

// Subscribe a single conversation to a webhook
await client.conversations.subscribe(conversationId, { webhookId });

// Clear the override (falls back to the number's defaultRouting)
await client.conversations.unsubscribe(conversationId);

// Or use the general-purpose endpoint
await client.conversations.setRouting(conversationId, { mode: "agent" });

// Pass null (or "inherit") to clear the override
await client.conversations.setRouting(conversationId, { mode: null });
To change the number-wide default, do it from the dashboard or via the three dedicated phone-number endpoints. Each one atomically sets the routing mode and clears the unrelated FK (agent vs. webhook), so the number can’t end up in an inconsistent state:
// Route the number to a webhook (drops any connected agent)
await client.phoneNumbers.subscribe("447700900100", {
  webhookId,
  applyToExisting: true,
});

// Connect an agent (drops any subscribed webhook)
await client.phoneNumbers.connectAgent("447700900100", {
  agentId,
  applyToExisting: true,
});

// Disable routing entirely (drops both)
await client.phoneNumbers.unsubscribe("447700900100", {
  applyToExisting: true,
});
applyToExisting: true materialises the change onto every existing conversation on this number — activeAgent is overwritten with the new default (or cleared, for subscribe / unsubscribe) and any in-flight session is dropped. No subscription lifecycle events are dispatched.
Webhooks must be created in the dashboard. There is no API for webhook creation — the routing endpoints only let you point conversations at webhooks that already exist.

Validation rules

  • mode='sandbox' is rejected on every public endpoint.
  • Any routing change on a sandbox number is rejected (the number is shared and system-managed).
  • webhookId must reference a webhook owned by the requesting user.
  • webhookId is required when (and only when) mode === 'webhook'.