ioZen

ESEN

Start free
Integrations and API · Webhooks

    Guide · Integrations and API

    Get events from ioZen with webhooks

    A webhook tells your system when something happens in ioZen, like a completed submission, a new contact, or a case that changes phase or owner. Here's how to create one, test it and verify its signature.

    4 steps · 6 min · updated Nov 6, 2026Leer en español →

    A webhook is a URL of yours that ioZen calls when something happens in your workspace. Say someone finishes your carpentry shop's Intake Bot: your system gets the answers right away, without asking ioZen over and over.

    1. Open webhooks

    In the sidebar, go to “Integrations” and open the “Webhooks” tab.

    The webhooks tab, before you create the first one.
    The webhooks tab, before you create the first one.

    2. Create the endpoint

    Tap “Create Webhook” and fill in:

    • “Endpoint URL”: the address on your server that receives the events. It starts with https://.
    • “Description”: optional, so your team knows what it's for.
    • “Events”: check at least one event.
    • “Intake bots”: leave it empty to get events from all your bots, or check only the ones you care about.

    Tap “Create Webhook” to save it.

    3. Save the secret

    ioZen shows you the “Signing Secret”, which starts with whsec_. Copy it and store it on your server: you won't see it again. It's how you check that each call really comes from ioZen.

    4. Send a test

    In the endpoint's menu, tap “Send Test”. On a phone, the button is on the endpoint's card. ioZen immediately sends one of the events you picked, with "is_test": true inside data, and tells you whether your server answered correctly.

    The events

    Event In the app When it arrives
    submission.completed “Submission Completed” Someone finishes an Intake Bot.
    contact.created “Contact Created” A new contact is created.
    card.phase_changed “Case Phase Changed” A case moves to another phase on its Process Board.
    card.assigned “Case Assigned” A case's owners change, by hand or through an assignment rule.

    Limit an endpoint to certain bots

    If you check bots under “Intake bots”, the endpoint only gets events that come from those bots. That way a clinic can send appointments to one system and price questions to another.

    Two cases are worth knowing. A contact someone on your team creates by hand doesn't come from any bot, so it reaches every endpoint subscribed to “Contact Created”. And a case that didn't start in a bot only reaches endpoints with no bots checked.

    What your server receives

    Each event arrives as a POST with this JSON body:

    json
    {
      "id": "evt_8f2c41d07a9b4e3f9a6d1c5b7e0f2a34",
      "type": "submission.completed",
      "api_version": "2026-08-27",
      "created_at": "2026-11-06T15:04:05.000Z",
      "data": {}
    }

    data holds what happened and depends on the event. The full reference shows each one.

    These headers come with the body:

    Header What it holds
    X-IoZen-Event-Type The event type, same as type.
    X-IoZen-Event-Id The event id, same as id.
    X-IoZen-Timestamp When it was signed, in Unix seconds.
    X-IoZen-Signature The signature: sha256= followed by the hex HMAC-SHA256.

    Verify the signature

    ioZen signs every call with your secret. The signature is the HMAC-SHA256 of the text timestamp.body: the X-IoZen-Timestamp value, a dot, and the body exactly as it arrived. The key is the whole secret, whsec_ included.

    Compute it from the raw body, before you parse the JSON. If you parse it and write it back out, the signature won't match. In Node.js:

    js
    import { createHmac, timingSafeEqual } from 'node:crypto'
    
    // rawBody: the body exactly as received (text). secret: your whsec_...
    export function verifyIozenWebhook(rawBody, headers, secret) {
      const timestamp = headers['x-iozen-timestamp']
      const signature = headers['x-iozen-signature'] ?? ''
      const expected =
        'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`, 'utf8').digest('hex')
    
      const a = Buffer.from(expected)
      const b = Buffer.from(signature)
      if (a.length !== b.length || !timingSafeEqual(a, b)) return false
    
      // Reject stale calls (here, older than 5 minutes)
      return Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
    }

    Every attempt is signed again with the current time, so retries pass this check.

    Deliveries, retries and pausing

    Your server has 10 seconds to answer with a 2xx status. Any other response, a redirect, or no answer in time counts as a failure.

    If it fails, we retry up to 5 times. A retry carries the same id, so use it to avoid handling the same event twice.

    After 10 failed attempts in a row, we pause the endpoint and email the workspace owners and admins. A “Paused” endpoint gets no events. Once your server is back, tap “Resume”.

    In the endpoint's menu, “Deliveries” lists recent attempts with their status, HTTP code and how long they took. “Redeliver” sends that event again right away, built from the current data and with a new id; the row is marked “Manual”.

    Change or delete an endpoint

    In the endpoint's menu, “Edit” changes the events, the description and the bots. The URL can't change: create another endpoint with the new one. “Delete” removes it and stops sending it events.

    The full reference

    The webhooks reference, with what data holds for each event, is at app.iozen.ai/docs/webhooks. To create and manage endpoints from your own code, use the ioZen API with the “Manage Webhooks” scope.

    Frequently asked questions

    Which plan do I need for webhooks?
    Pro or higher. How many endpoints you can have depends on your plan; see Pricing.
    Can a team member create webhooks?
    No. Only the workspace owner or an admin sees the webhooks tab.
    What happens if my server is down?
    We retry each event up to 5 times. After 10 failed attempts in a row, we pause the endpoint and email the workspace owners and admins.
    Can I change an endpoint's URL?
    No. Create a new endpoint with the new URL and delete the old one.

    Rather start from a ready one?

    FlowApps to install

    All FlowApps →