Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.
ReferenceWebhooks (Inbound)

Webhooks (Inbound)

On this page

Polyant accepts inbound webhooks for two distinct purposes:

  1. 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.

    ChannelRoute
    TelegramPOST /webhooks/telegram/<slug> (registered with Telegram by the engine)
    SlackPOST /webhooks/slack/<slug> (the app’s Events API Request URL)
    WhatsApp (Twilio)POST /webhooks/twilio/<slug>/whatsapp, or .../whatsapp/<webhookSecret> in API Key mode
    Email (managed services)POST /webhooks/email/<slug>/<webhookSecret>
    HubSpot (managed services)POST /webhooks/hubspot/<slug> (private app) or POST /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/stream that Twilio opens for the call’s audio

    Their authentication and setup are in Channels.

  2. 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) or application/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

  1. Body parsed; size validated.
  2. 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, so Yes., **yes**, "yes", Answer: yes and the Italian sì/si all 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.
  3. 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 into event_backlog with status = pending. If the Room is disabled it is discarded instead, since nothing would ever collect it.
  4. 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.

Last updated on