Gareth Wilson Gareth Wilson

Guide to Voiceflow Webhooks: Features and Best Practices

Published


Voiceflow is a platform for building AI agents, organised into projects with environments that teams branch, publish and merge. In a larger organisation you may want those changes reflected elsewhere: an audit log, an observability dashboard, a test run after each publish. Voiceflow's organisation events webhooks send each project and environment change to a URL you choose.

AspectValue
MethodPOST, JSON body
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
ScopeOne URL per organisation; every organisation event is sent to it
Acknowledgement and retriesVoiceflow states no acknowledgement, timeout or retry policy

What are Voiceflow webhooks?

Organisation events webhooks are configured once per organisation. After you set a URL, every organisation event goes to it as a JSON POST. Svix delivers and signs them, and Voiceflow's own documentation page links to Svix's guide for verification.

Voiceflow also has a project-level session lifecycle webhook. It's a separate feature with its own secret, and its docs don't describe this signing scheme, so this guide doesn't cover it.

Voiceflow webhook features

FeatureDetails
ConfigurationSettings > Organization, one URL
Event selectionNone; every organisation event is sent
SignatureStandard Webhooks HMAC-SHA256, via Svix
Signature headersvix-signature
SecretGenerated when you enter the URL. Format not shown
RotationOld secret stays valid for 24 hours
BodyJSON
Event routingBody field type
Retries and timeoutNot stated
Source IPsSvix's addresses

Events and payloads

Every event has the same outer shape:

FieldMeaning
typeThe event name, e.g. organization.project.created
dataEvent details: organizationID, projectID, projectMetadata.name, workspaceID, plus event-specific fields
resourceorganization-<organizationID>
timeEvent time as a Unix timestamp in milliseconds

Voiceflow sends six event types:

EventTriggered WhenKey data fields
organization.project.createdA new project is createdcreatedBy, projectID, projectMetadata.name
organization.project.deletedA project is deleteddeletedBy, projectID, projectMetadata.name
organization.project.environment.createdAn environment is created within a projectcreatedProjectEnvironmentID, createdProjectEnvironmentMetadata, source.environmentID (the environment it was branched from)
organization.project.environment.publishedAn environment is publishedpublishedProjectEnvironmentID, publishedVersionIDBefore, publishedVersionIDAfter
organization.project.environment.mergedOne environment is merged into anothersourceProjectEnvironmentID, targetProjectEnvironmentID, sourceProjectEnvironmentRemoved (source deleted as part of the merge)
organization.project.environment.deletedAn environment is deleteddeletedProjectEnvironmentID, deletedBy

The actor field changes name with the event (createdBy, deletedBy, publishedBy or mergedBy) and always has the form { "type": "user", "userEmail": "..." }. For organization.project.environment.published, publishedProjectEnvironmentMetadata includes alias, isLive, name and trafficPercentage, so you can tell a publish of the live Main environment from one of a branch that takes no traffic.

This is the organization.project.created sample from the webhook samples library. time is in milliseconds:

{
  "data": {
    "createdBy": {
      "type": "user",
      "userEmail": "user@voiceflow.com"
    },
    "organizationID": "LVkmPy8AE2",
    "projectID": "6a16f4549e4196323d8a49b9",
    "projectMetadata": {
      "name": "My new project"
    },
    "workspaceID": "95kwAZ63nO"
  },
  "resource": "organization-LVkmPy8AE2",
  "time": 1779889237608,
  "type": "organization.project.created"
}

Setting up Voiceflow webhooks

Hookdeck's Voiceflow source supports organisation event webhooks only. The project-level session webhook doesn't document this signing scheme and may fail verification.

  1. In the Voiceflow dashboard, click Settings in the bottom left corner, then select Organization.
  2. Enter the URL to send events to, for example https://example.com/webhooks/voiceflow.
  3. Copy the webhook secret that appears and store it in VOICEFLOW_WEBHOOK_SECRET. Voiceflow doesn't show an example value; Svix secrets start with whsec_, so keep the whole string.

From then on, every organisation event is sent to that URL. If you'd rather not write code, Voiceflow suggests a no-code tool such as Make to receive them, in which case verification is up to that tool.

If your endpoint sits behind a restrictive firewall, allow Svix's IP addresses; events don't come from Voiceflow's.

To use Hookdeck, create a source with the Voiceflow type, paste the secret, and enter the source URL in Voiceflow's settings. Forward it during development with the Hookdeck CLI:

npx hookdeck-cli listen 3000 voiceflow --path /webhooks/voiceflow

Creating a throwaway project in the organisation sends organization.project.created, and deleting it sends organization.project.deleted.

Securing Voiceflow webhooks

Voiceflow points to Svix's manual verification steps: sign svix-id, a dot, svix-timestamp, another dot and the raw body with HMAC-SHA256, using the base64-decoded secret after whsec_, and compare with each v1, entry in svix-signature. This function does that and also accepts webhook-* header names:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

After you regenerate the secret with the refresh button on the settings page, the old one stays valid for 24 hours. During that period a delivery can carry a v1, entry for each secret, and the function accepts either, so roll out the new value before the 24 hours end.

Voiceflow sets no timestamp tolerance. The Svix libraries use five minutes, but Hookdeck has no tolerance and keeps the original svix-timestamp on retries and replays, so behind Hookdeck the age check is left out.

The receiver records each change keyed by svix-id:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// orgAuditLog.write(messageId, event) records the change once and resolves when durable.
function createVoiceflowReceiver(orgAuditLog) {
  const app = express();
  app.post('/webhooks/voiceflow', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.VOICEFLOW_WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    try {
      await orgAuditLog.write(req.headers['svix-id'] || req.headers['webhook-id'], event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

A Hookdeck Voiceflow source replies 200 regardless of the signature. A request that fails is marked as failed verification and isn't delivered.

Make Voiceflow webhooks production-ready. Hookdeck Event Gateway splits Voiceflow's single organisation feed by event type, keeps a history of every publish and merge, and retries your services.

Voiceflow webhook limitations and pain points

One URL, every event

The Problem: Your publish-triggered test runner also receives project deletions and environment merges.

Why It Happens: Voiceflow allows one URL per organisation and sends every organisation event to it, with no per-event selection.

Workarounds:

  • Switch on type in the receiver and acknowledge the events you don't need.

How Hookdeck Can Help: Hookdeck filters on type send organization.project.environment.published to one service and everything else to another.

No stated delivery policy

The Problem: You don't know whether events sent during an outage will be retried.

Why It Happens: Voiceflow states no acknowledgement window, timeout or retry policy. Svix delivers the events, but Voiceflow doesn't say which Svix settings apply.

Workarounds:

  • Return 2xx quickly after storing the event, and compare your records with the projects in Voiceflow after an outage.

How Hookdeck Can Help: Hookdeck accepts every event as it arrives and retries your service on a schedule you control.

Session webhooks are different

The Problem: Requests from a project's session webhook fail verification.

Why It Happens: The project-level session lifecycle webhook has its own secret, and its docs don't describe the Svix scheme.

Workarounds:

  • Use separate endpoints for organisation events and session webhooks, and only apply this verifier to organisation events.

How Hookdeck Can Help: Point session webhooks at a separate Hookdeck source without Voiceflow verification, so they don't mix with signed organisation events.

Secret format isn't shown

The Problem: You can't tell from the docs whether the secret should be base64-decoded.

Why It Happens: Voiceflow doesn't print an example secret. Svix secrets start with whsec_, but Voiceflow doesn't confirm it.

Workarounds:

  • Check the value on the settings page before configuring a manual verifier.

How Hookdeck Can Help: Hookdeck flags failed verification on the first delivery, so a wrong secret is obvious straight away.

Best practices

Track what each publish changed

Store publishedVersionIDBefore and publishedVersionIDAfter so you can see exactly which version went live.

Distinguish live publishes

Check publishedProjectEnvironmentMetadata.isLive before triggering production checks, and use trafficPercentage to see how much traffic the environment serves.

Handle merges that remove environments

When sourceProjectEnvironmentRemoved is true, the merged environment no longer exists. Clean up anything you keyed on it.

Make processing idempotent

Use svix-id as the key. Implementing webhook idempotency explains the storage pattern.

Conclusion

Voiceflow's organisation events webhooks give you an audit trail of how agents are created, published and merged. Everything goes to one URL, so filter by type before acting.

Hookdeck Event Gateway filters, records and retries Voiceflow's organisation events. Sign up and start free.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.