Quickstart: Webhooks

This guide demonstrates how to set up and test Persona webhooks.

What you’ll build

You’ll create a simple local webhook server with one endpoint that receives webhook events. You’ll configure Persona to send events to this endpoint.

Security note

The server implementation below is meant for learning purposes only—not production usage.

For demonstration purposes, we will keep the server simple:

  • The server will not implement authentication or access controls.
  • The server will be exposed to the internet via a third-party tunneling service (ngrok or a similar tool).

When you test webhooks that contain real production data, your server should run in a secure environment you control with appropriate authentication, validation, and logging.

Prerequisites

You’ll need access to:

  • A Persona account
  • An ngrok account—the free plan is sufficient
    • Feel free to use any similar tunneling tool. ngrok is just one tool commonly used during development.
  • A local development environment for either NodeJS or Python
    • This guide provides code for NodeJS and Python, but you can adapt the code to another language.

Before you start, you should:

Step 1: Create a local webhook server

Create a new directory called webhook-demo/.

Add the sample code below to create a local server:

  1. Create webhook-demo/webhook-server.js:
webhook-server.js
1const express = require('express');
2const app = express();
3app.use(express.json());
4
5// Webhook endpoint
6app.post('/webhook', (req, res) => {
7 console.log('📩 Webhook received:');
8 console.log('Headers:', req.headers);
9
10 if (req.headers['persona-signature'] != null) {
11 const data = req.body.data;
12 console.log('Event type: ', data.attributes.name);
13 console.log('Payload entity type: ', data.attributes.payload.data.type);
14 console.log('Payload entity ID: ', data.attributes.payload.data.id);
15 } else {
16 console.log('Body:', JSON.stringify(req.body, null, 2));
17 }
18
19 // Send a success response
20 res.status(200).json({
21 success: true,
22 message: 'Webhook received successfully',
23 });
24});
25
26// Health check endpoint
27app.get('/', (req, res) => {
28 res.send('Webhook server is running. Send POST requests to /webhook');
29});
30
31// Start the server
32const PORT = process.env.PORT || 3000;
33app.listen(PORT, () => {
34 console.log(`Webhook server listening on port ${PORT}`);
35 console.log(`Webhook endpoint: http://localhost:${PORT}/webhook`);
36});
  1. Create webhook-demo/package.json:
package.json
1{
2 "name": "persona-webhook-demo-server",
3 "version": "1.0.0",
4 "description": "Simple webhook server for receiving webhook events",
5 "main": "webhook-server.js",
6 "scripts": {
7 "start": "node webhook-server.js"
8 },
9 "dependencies": {
10 "express": "^4.22.1"
11 }
12}
  1. Install dependencies:
$cd webhook-demo/
>npm install
  1. Start the server:
$npm start

Now, test the server:

$curl -X POST http://localhost:3000/webhook \
>-H "Content-Type: application/json" \
>-d '{"event": "test", "data": {"message": "Hello from webhook!"}}'

You should receive “Webhook received successfully” from the server. In the server logs, you should see the request headers and body.

Step 2: Expose the webhook endpoint with ngrok

Follow ngrok’s documentation to install ngrok locally and configure it with your ngrok authtoken.

Then expose your webhook server to the internet:

$ngrok http 3000

Note your ngrok domain (e.g. http://abs123.ngrok-free.dev). You’ll need this in the next step.

Step 3: Set up webhook in Persona

In the Persona dashboard:

  1. Switch into your Sandbox environment
  2. Navigate to Webhooks > Webhooks
  3. Click Create webhook
  4. Submit the form with the following values:
    • URL: <your ngrok domain>/webhook
    • Enabled events: inquiry.created, inquiry.started, inquiry.completed, inquiry.approved
    • For other fields, feel free to provide any value.
  5. Click Enable and confirm you want to enable the webhook.

At the top of the resulting page:

  1. Note the “Webhook secret”
  2. Click the “eye” icon to reveal the value.
  3. Click Copy and save the value somewhere secure. You will use this value in Step 7.

Step 4: Trigger test events

You selected inquiry-related events in your webhook. Now, create an inquiry and interact with it to trigger events.

In the Persona dashboard (still in your Sandbox environment):

  1. Navigate to Inquiries > All Inquiries.
  2. Click Create inquiry and fill out the form (any values are fine).
  3. Load the newly-created inquiry link in a browser.
  4. Click through the verification flow.
    • Note: Do not provide any real personal information.
    • Keep the “Pass verifications” toggle (visible at the bottom of the flow) enabled to simulate you passing all checks.

Step 5: View events in your server logs

Look at your server logs. You should see events for: inquiry.created, inquired.started, and inquiry.completed.

You may also see an event for inquiry.approved if your inquiry template has an associated Workflow that takes action on completed inquiries.

Step 6: Learn about webhook headers for retries and authenticity

In the previous step, you saw the webhook request headers printed in your server logs:

{
host: '...',
'user-agent': '...',
'content-length': '...',
'content-type': 'application/json; charset=utf-8',
'persona-signature': 't=1764686826,v1=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
'persona-webhook-attempts-left': '7',
'persona-webhook-attempts-made': '1',
'persona-webhook-first-attempted-at': '1764686826',
'...',
}

Look at the persona- prefixed headers. These headers provide information about:

  • Retries: Persona retries webhooks if it doesn’t receive a successful response from your webhook endpoint within 5 seconds. Persona will attempt to deliver your webhook up to 7 additional times with an exponential backoff between attempts.
  • Authenticity: Persona includes a signature created from a secret that only you and Persona share. This signature lets you verify that the request is from Persona.

You need to implement logic in your server to check the Persona signature. Let’s do that next.

Step 7: Improve the server: check that webhook requests are authentic

The value of the persona-signature header consists of a timestamp (t=<unix_timestamp>) plus a signature (v1=<signature>).

The signature signature is computed from your webhook secret (which you found in Step 3) and a dot-separated string composed of the unix timestamp joined with the request body.

Update your server code to verify the signature:

  1. Update webhook-demo/webhook-server.js:
webhook-server.js
1const express = require('express');
2const crypto = require('crypto');
3const app = express();
4
5// TODO: Set your Persona webhook secret here
6// You can find this in your Persona Dashboard under Webhooks > <your specific webhook>
7const WEBHOOK_SECRET = process.env.PERSONA_WEBHOOK_SECRET || 'your_webhook_secret_here';
8
9// Middleware to capture raw body for signature verification
10app.use(express.json({
11verify: (req, res, buf, encoding) => {
12if (buf && buf.length) {
13 req.rawBody = buf.toString(encoding || 'utf8');
14}
15}
16}));
17
18function verifyPersonaSignature(rawBody, signatureHeader, webhookSecret) {
19try {
20// Parse the signature header
21// Format: "t=<timestamp>,v1=<signature>" or multiple space-separated pairs during rotation
22const signaturePairs = signatureHeader.split(' ');
23
24// Extract timestamp from first pair
25const firstPair = signaturePairs[0];
26const timestamp = firstPair.split(',')[0].split('=')[1];
27
28// Extract all signatures (in case of key rotation)
29const signatures = signaturePairs.map(pair => {
30 const v1Match = pair.match(/v1=([^,]+)/);
31 return v1Match ? v1Match[1] : null;
32}).filter(sig => sig !== null);
33
34// Compute expected signature
35const expectedSignature = crypto
36 .createHmac('sha256', webhookSecret)
37 .update(`${timestamp}.${rawBody}`)
38 .digest('hex');
39
40// Check if any of the signatures match the expected (for key rotation support)
41const isValid = signatures.some(signature => {
42 try {
43 return crypto.timingSafeEqual(
44 Buffer.from(expectedSignature),
45 Buffer.from(signature)
46 );
47 } catch (e) {
48 return false;
49 }
50});
51
52return isValid;
53} catch (error) {
54console.error('Error verifying signature:', error.message);
55return false;
56}
57}
58
59// Webhook endpoint
60app.post('/webhook', (req, res) => {
61console.log('📩 Webhook received:');
62console.log('Headers:', req.headers);
63const personaSignature = req.headers['persona-signature'];
64
65if (personaSignature) {
66const isValid = verifyPersonaSignature(req.rawBody, personaSignature, WEBHOOK_SECRET);
67if (!isValid) {
68 console.log('❌ Invalid signature - webhook rejected');
69 return res.status(401).json({
70 success: false,
71 message: 'Invalid webhook signature'
72 });
73}
74console.log('✅ Signature verified');
75
76const data = req.body.data;
77console.log('Event type:', data.attributes.name);
78console.log('Payload entity type:', data.attributes.payload.data.type);
79console.log('Payload entity ID:', data.attributes.payload.data.id);
80} else {
81console.log('Body:', JSON.stringify(req.body, null, 2));
82}
83
84res.status(200).json({
85success: true,
86message: 'Webhook received successfully'
87});
88});
89
90// Health check endpoint
91app.get('/', (req, res) => {
92res.send('Webhook server is running. Send POST requests to /webhook');
93});
94
95// Start the server
96const PORT = process.env.PORT || 3000;
97app.listen(PORT, () => {
98console.log(`Webhook server listening on port ${PORT}`);
99console.log(`Webhook endpoint: http://localhost:${PORT}/webhook`);
100});
  1. Set your webhook secret (from Step 3) as an environment variable:
$export PERSONA_WEBHOOK_SECRET=wbhsec_your_actual_secret_here

Or paste it directly into the code for testing only.

  1. Restart the server:
$npm start

Step 8: Trigger test events again and view results

Repeat Steps 4 and 5: Create another inquiry in Sandbox and complete it, then check your server logs for events.

This time, you should see the server log ”✅ Signature verified” for each event.

You can also send a fake webhook request, and check that the server logs ”❌ Invalid signature”:

$curl -X POST http://localhost:3000/webhook \
> -H "Content-Type: application/json" \
> -H "Persona-Signature: t=1234567890,v1=invalid_signature" \
> -d '{"test": "data"}'

Summary

In this tutorial, you:

  • Created a local webhook server in Node.js or Python
  • Configured webhooks in Persona’s dashboard
  • Triggered webhook events by completing an inquiry
  • Implemented signature verification in your server for security

Next steps

To learn more, check out the following resources: