Webhooks (Inbound)
On this page
Polyant accepts inbound webhooks for two distinct purposes:
-
Channel inbound - the routes that deliver user messages from a messaging channel. Every one of them lives under
/webhooks/*, so the engine’s public HTTPS address must reach that path, with no sign-in in front of it: each route authenticates its caller itself.Channel Route Telegram POST /webhooks/telegram/<slug>(registered with Telegram by the engine)Slack POST /webhooks/slack/<slug>(the app’s Events API Request URL)WhatsApp (Twilio) POST /webhooks/twilio/<slug>/whatsapp, or.../whatsapp/<webhookSecret>in API Key modeEmail (managed services) POST /webhooks/email/<slug>/<webhookSecret>HubSpot (managed services) POST /webhooks/hubspot/<slug>(private app) orPOST /webhooks/hubspot(OAuth app)Voice (managed services) POST /webhooks/twilio/<slug>/voice(the number’s A call comes in webhook), then the WebSocket/webhooks/twilio/<slug>/voice/streamthat Twilio opens for the call’s audioTheir authentication and setup are in Channels.
-
Room events - external systems (Stripe, GitHub, custom apps) pushing events into a Room (see Room tab).
The rest of this page covers the second case: Room event webhooks.
Endpoint
POST /webhooks/<webhookToken>Where <webhookToken> is the unique token generated when you create an event source (the agent’s Webhooks section). The token is shown next to the source’s URL and can be regenerated.
Request
- Method:
POST. - Content-Type:
application/json(preferred) orapplication/x-www-form-urlencoded. - Body: any JSON payload up to 64 KB. Larger payloads still receive
200 OK(see Response), with body{"ok": false, "error": "payload too large"}.
Response
Webhooks return 200 OK even if no event definition matched, the token is unknown, or the payload was too large. The JSON body reports the outcome ({"ok": true} on success, {"ok": false, "error": "..."} otherwise). Processing is fire-and-forget so the external sender never sees a delay.
One case answers 401 instead: a source that declares an authKey requires Authorization: Bearer <key> on every call, compared in constant time, and refuses a mismatch. That is opt-in per source; without it the URL is the only credential.
If the backlog cap (100 pending events per agent, not per source) is full, new events are dropped silently. Use the Room → Backlog screen to monitor depth.
What happens internally
- Body parsed; size validated.
- The matcher (LLM tier
fast) evaluates definitions in priority order until one matches. It asks for a bare “yes” or “no” and reads the first word of the answer after stripping decoration, soYes.,**yes**,"yes",Answer: yesand the Italiansì/siall count as a match. An answer that is neither a yes nor a no (a hedge, or an argument before the verdict) is treated as no match for that definition and logged as a warning naming it; if no definition matches, the event is dropped. - What happens next depends on the matched definition’s action:
conversation- the event triggers a conversation turn immediately. It never touches the backlog, and it does not need the Room.backlog- the event is inserted intoevent_backlogwithstatus = pending. If the Room is disabled it is discarded instead, since nothing would ever collect it.
- For a backlogged event, the Room scheduler picks it up on the next 30 s tick (or immediately if a human reply on the outbound channel triggers
triggerImmediate()).
If no definition matches, the request is logged but no work is done.
Why everything is “silent”
Returning 200 OK even on no-match keeps webhook senders happy. Stripe, GitHub, and friends will mark the webhook as failed and back off if the response is anything else. Operationally you observe matching success/failure on the Backlog screen, not in HTTP responses.
Worked example: Stripe
Configure Stripe to send all events to:
https://<your-host>/webhooks/<webhookToken>Pick the events you care about (invoice.paid, customer.subscription.deleted, etc.). Stripe sends a JSON payload with type and data.object. In Polyant, define one event definition per business event you care about; the description should reference the discriminator (e.g. “type is invoice.paid”).
Stripe webhook URLs work like any other webhook URL - keep the <webhookToken> portion of the URL secret (rotate via the event source if it leaks). Stripe-style signature verification is not currently performed by the engine.