WEBHOOKS · REAL-TIME EVENTS

React to events
the moment they happen.

Webhooks deliver real-time HTTP POST notifications to your server whenever Prynt identifies a visitor, triggers a rule, or detects suspicious activity. No polling required.

🔑 Prynt account with API keys
🌐 Publicly accessible HTTPS endpoint
🛡️ Server to verify signatures
In this guide
1
Section 1
Overview
~2 min
Webhooks are HTTP callbacks that Prynt sends to your server in real time. Instead of repeatedly polling the API to check for new events, your application receives a POST request the instant something happens.

With webhooks, you can:

Receive instant notification when a visitor is identified, without polling
Trigger automated workflows when rules fire (block bots, challenge VPN users)
Pipe events into external systems like Slack, PagerDuty, Datadog, or your SIEM
Build real-time dashboards and alerting without API rate limit concerns
Maintain an audit trail by logging every event to your own data warehouse
📌
Polling vs. Webhooks. Polling requires your server to repeatedly call GET /v1/events on a schedule, wasting bandwidth and introducing latency. Webhooks push data to your server in under 500ms, reducing API calls by 95%+ and ensuring you never miss an event.

How the flow works:

Webhook FlowDiagram
1. Visitor lands on your site 2. Client SDK calls prynt.identify() 3. Prynt processes identification & runs rules 4. Prynt sends HTTP POST to your webhook endpoint 5. Your server receives the event payload 6. Your server responds with 200 OK 7. Process event asynchronously (log, alert, block, etc.) Client ──> Prynt API ──> Your Webhook Endpoint identify() process POST /webhooks/prynt └── rules engine, ML scoring, signals
2
Section 2
Setting up webhooks
~3 min
You can create webhooks through the Prynt Dashboard or programmatically via the API. Both methods support the same configuration options.

Option A: Via the Dashboard

1 Navigate to Webhooks in your Prynt dashboard
2 Click Create Webhook and enter your endpoint URL
3 Select which event types you want to subscribe to
4 Copy the generated signing secret and store it securely
5 Click Send Test Event to verify your endpoint works

Option B: Via the API

REST API
curl -X POST https://api.prynt.io/v1/webhooks \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/webhooks/prynt", "events": [ "identification.complete", "verdict.block", "verdict.challenge", "rule.matched" ], "description": "Production fraud alerts", "enabled": true }'
201 Created
POST /v1/webhooks
{ "id": "wh_3kR8mNqT7xP2v", "url": "https://yourapp.com/webhooks/prynt", "events": ["identification.complete", "verdict.block", "verdict.challenge", "rule.matched"], "signingSecret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "enabled": true, "createdAt": "2026-02-14T10:30:00Z" }
⚠️
Save your signing secret immediately. The signingSecret is only returned once when the webhook is created. Store it in an environment variable (PRYNT_WEBHOOK_SECRET). You will need it to verify that incoming requests are genuinely from Prynt.
💡
HTTPS required. Webhook endpoints must use HTTPS with a valid TLS certificate. Self-signed certificates are not accepted. For local development, use a tunneling tool like ngrok or cloudflared to expose your local server.
3
Section 3
Event types
~2 min
Subscribe only to the events you need. Each event type corresponds to a specific action in the Prynt pipeline. You can update your subscriptions at any time via the Dashboard or API.
EventCategoryDescription
identification.completeIdentificationFired every time a visitor is successfully identified. Contains the full identification result including visitor ID, confidence score, device data, and all Smart Signals. This is the most common event type.
verdict.allowVerdictFired when the rules engine allows the visitor through. Contains the full identification context. Useful for logging all traffic decisions.
verdict.blockVerdictFired when the rules engine determines the visitor should be blocked. Includes the rule that triggered the block and the full identification context.
verdict.challengeVerdictFired when the rules engine determines the visitor should be challenged (e.g., MFA, CAPTCHA). Contains the recommended challenge type and triggering rule.
rule.matchedRuleFired whenever any rule in the rules engine matches, regardless of the verdict. Useful for monitoring rule performance and debugging rule logic.
visitor.newVisitorFired the first time a visitor is ever seen. Useful for tracking new device registrations and onboarding flows. Not fired on subsequent visits.
visitor.returningVisitorFired when a previously identified visitor returns. Contains the visitor's history summary including first seen date, visit count, and previous verdicts.
signal.bot.detectedSignalFired when the bot detection signal triggers on an identification. Contains the bot type, detection method, and confidence level.
list.entry.addedListFired when an entry is added to a list (manually or via a rule side effect). Contains the list ID, entry value, and source of the addition.
list.entry.expiredListFired when a list entry expires due to its TTL. Contains the list ID, expired entry value, and original TTL duration.
alert.triggeredAlertFired when a configured alert threshold is exceeded (e.g., spike in bot traffic, velocity limit breach, anomaly detection). Contains the alert definition and current metric values.
📌
Wildcard subscriptions. Use "events": ["*"] to subscribe to all event types. This is useful for development and debugging but not recommended for production, as high-traffic sites can generate thousands of events per minute.
4
Section 4
Webhook payload
~3 min
Every webhook delivery is an HTTP POST with a JSON body. The payload follows a consistent envelope structure with the event-specific data nested inside the data field.

Full payload example (identification.complete):

POST
https://yourapp.com/webhooks/prynt
{ "webhookId": "wh_3kR8mNqT7xP2v", "eventId": "evt_9fJ3kL7mNpR2x", "eventType": "identification.complete", "apiVersion": "2026-01-15", "createdAt": "2026-02-14T11:45:22.341Z", "idempotencyKey": "idk_a7f2c9e1b3d4", "data": { "requestId": "req_1707832921_a7f2c9", "visitorId": "pv_8kX2mNqR3jT7p", "visitorFound": true, "confidence": 0.995, "firstSeenAt": "2025-09-14T08:12:33Z", "lastSeenAt": "2026-02-14T11:45:22Z", "visitCount": 47, "ip": "203.0.113.42", "geo": { "city": "San Francisco", "region": "California", "country": "US", "latitude": 37.7749, "longitude": -122.4194, "timezone": "America/Los_Angeles" }, "device": { "platform": "macOS", "browser": "Chrome 121", "gpu": "Apple M3 Pro", "type": "desktop", "screenRes": "2560x1440", "language": "en-US" }, "signals": { "bot": { "detected": false, "type": null }, "vpn": { "detected": false, "provider": null }, "proxy": { "detected": false, "type": null }, "tor": false, "incognito": false, "tampered": false, "emulator": false, "rootedDevice": false }, "scores": { "abuse": 0.03, "ato": 0.01, "bot": 0.02, "suspect": 4 }, "verdict": "allow", "matchedRules": [] } }

Envelope fields:

FieldTypeDescription
webhookIdstringThe ID of the webhook subscription that triggered this delivery.
eventIdstringUnique identifier for this specific event. Use this for logging and correlation.
eventTypestringThe type of event (e.g., identification.complete, verdict.block).
apiVersionstringThe API version used to format the payload. Matches your account's API version setting.
createdAtISO 8601Timestamp when the event was created, in UTC with millisecond precision.
idempotencyKeystringStable key for deduplication. Remains the same across retries of the same event.
dataobjectEvent-specific payload. Structure varies by eventType (see examples above).
💡
HTTP Headers. Each webhook request includes these headers: Content-Type: application/json, X-Prynt-Signature: sha256=..., X-Prynt-Event: identification.complete, X-Prynt-Delivery: evt_9fJ3kL7mNpR2x, and X-Prynt-Timestamp: 1707832922.
5
Section 5
Signature verification
~3 min
Always verify webhook signatures before processing a payload. Prynt signs every webhook delivery with HMAC-SHA256 using your signing secret. This ensures the request genuinely originated from Prynt and has not been tampered with.

The verification process works in three steps:

1 Extract the X-Prynt-Signature and X-Prynt-Timestamp headers from the request
2 Compute HMAC-SHA256 of timestamp.rawBody using your signing secret
3 Compare the computed signature against the header value using a timing-safe comparison

Node.js / Express verification:

webhook-handler.ts
import crypto from 'crypto'; import express from 'express'; const app = express(); const WEBHOOK_SECRET = process.env.PRYNT_WEBHOOK_SECRET; // IMPORTANT: Use raw body for signature verification app.use('/webhooks/prynt', express.raw({ type: 'application/json' })); function verifyWebhookSignature(req) { const signature = req.headers['x-prynt-signature']; const timestamp = req.headers['x-prynt-timestamp']; // Reject requests older than 5 minutes (replay protection) const age = Math.abs(Date.now() / 1000 - parseInt(timestamp)); if (age > 300) { throw new Error('Webhook timestamp too old'); } // Compute expected signature const payload = `${'${'}timestamp}.${req.body}`; const expected = 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(payload) .digest('hex'); // Timing-safe comparison to prevent timing attacks if (!crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) )) { throw new Error('Invalid webhook signature'); } return true; } app.post('/webhooks/prynt', (req, res) => { try { verifyWebhookSignature(req); } catch (err) { console.error('Webhook verification failed:', err.message); return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body); console.log('Verified event:', event.eventType, event.eventId); // Respond 200 immediately, process async res.status(200).json({ received: true }); // Process the event asynchronously processWebhookEvent(event).catch(console.error); });
🚨
Never skip verification. Without signature verification, an attacker could send fake webhook payloads to your endpoint, potentially triggering false blocks or allowing fraudulent activity. Always verify before trusting the data.
📌
Use the raw request body. You must use the raw (unparsed) request body for HMAC computation. If your framework auto-parses JSON, the re-serialized body may differ from the original, causing signature mismatches. In Express, use express.raw() for the webhook route.
6
Section 6
Retry policy
~1 min
If your endpoint does not respond with a 2xx status code within 10 seconds, Prynt considers the delivery failed and automatically retries with exponential backoff. After all retries are exhausted, the event is marked as failed and can be retried manually from the Dashboard.
1
Initial delivery
Sent immediately when the event is created. Must respond with 2xx within 10 seconds.
t = 0
2
First retry
Sent 5 minutes after the initial failure. Same payload, new signature with updated timestamp.
t + 5 min
3
Second retry (final)
Sent 30 minutes after the first retry. This is the final automatic attempt before marking as failed.
t + 35 min
⚠️
Automatic disabling. If your endpoint fails to respond successfully for 24 consecutive hours, Prynt will automatically disable the webhook and send you an email notification. Re-enable it from the Dashboard once the issue is resolved.

Responses treated as failures:

Any HTTP status code outside the 2xx range (e.g., 400, 403, 500, 502, 503)
Connection timeout (endpoint unreachable within 10 seconds)
TLS handshake failure (invalid or expired certificate)
DNS resolution failure (domain does not resolve)
Connection reset or closed before response is sent
7
Section 7
Best practices
~2 min
Follow these practices to ensure reliable webhook processing at any scale. These patterns are especially important for high-traffic applications handling thousands of events per minute.
Respond with 200 immediately. Return a 200 OK response as fast as possible, ideally within 1-2 seconds. Do not perform heavy processing before responding. Prynt will time out after 10 seconds.
Process events asynchronously. Queue incoming events (using Redis, SQS, RabbitMQ, or a database) and process them in a background worker. This decouples receipt from processing and prevents timeouts.
Implement idempotency. Use the idempotencyKey field to deduplicate events. The same event may be delivered more than once during retries. Store processed keys and skip duplicates.
Verify signatures on every request. Never skip verification, even in development. Use timing-safe comparison functions to prevent timing attacks against your signature check.
Monitor webhook health. Track your endpoint's response times and error rates. Set up alerts for sustained failures. Use the Prynt Dashboard's webhook logs to inspect delivery history.
Handle unknown event types gracefully. Prynt may add new event types in the future. Your handler should ignore unrecognized event types rather than returning an error, to avoid unnecessary retries.

Recommended handler pattern:

Async Processing Patternwebhook-worker.ts
import { Queue } from 'bullmq'; const webhookQueue = new Queue('prynt-webhooks'); app.post('/webhooks/prynt', async (req, res) => { // 1. Verify signature (see Section 5) verifyWebhookSignature(req); const event = JSON.parse(req.body); // 2. Check idempotency — skip if already processed const alreadyProcessed = await redis.get(`wh:${'${'}event.idempotencyKey}`); if (alreadyProcessed) { return res.status(200).json({ skipped: true }); } // 3. Mark as received await redis.set(`wh:${'${'}event.idempotencyKey}`, '1', 'EX', 86400); // 4. Respond immediately res.status(200).json({ received: true }); // 5. Queue for async processing await webhookQueue.add(event.eventType, event, { jobId: event.eventId, removeOnComplete: 1000, removeOnFail: 5000, }); });
8
Section 8
Example: Slack integration
~3 min
A common use case is sending real-time fraud alerts to a Slack channel. This example listens for verdict.block and alert.triggered events and posts a rich notification to Slack using an Incoming Webhook.
slack-alerts.ts
import express from 'express'; import crypto from 'crypto'; const app = express(); const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL; const WEBHOOK_SECRET = process.env.PRYNT_WEBHOOK_SECRET; app.use('/webhooks/prynt', express.raw({ type: 'application/json' })); async function sendSlackAlert(event) { const { data } = event; const emoji = event.eventType === 'verdict.block' ? '🛑' : '⚠️'; const message = { blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `${'${'}emoji} *Prynt Alert: ${'${'}event.eventType}*`, }, }, { type: 'section', fields: [ { type: 'mrkdwn', text: `*Visitor:*\n\`${'${'}data.visitorId}\`` }, { type: 'mrkdwn', text: `*IP:*\n${'${'}data.ip}` }, { type: 'mrkdwn', text: `*Location:*\n${'${'}data.geo.city}, ${'${'}data.geo.country}` }, { type: 'mrkdwn', text: `*Suspect Score:*\n${'${'}data.scores.suspect}/100` }, { type: 'mrkdwn', text: `*Verdict:*\n${'${'}data.verdict}` }, { type: 'mrkdwn', text: `*Bot Detected:*\n${'${'}data.signals.bot.detected}` }, ], }, { type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: 'View in Dashboard' }, url: `https://prynt.id/app/event-detail?id=${'${'}data.requestId}`, }], }, ], }; await fetch(SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message), }); } app.post('/webhooks/prynt', (req, res) => { verifyWebhookSignature(req); // see Section 5 const event = JSON.parse(req.body); // Respond immediately res.status(200).json({ received: true }); // Send Slack alert for block and alert events only if (['verdict.block', 'alert.triggered'].includes(event.eventType)) { sendSlackAlert(event).catch(console.error); } }); app.listen(3000, () => console.log('Webhook server running on :3000'));
💡
Rate limit Slack posts. If you process high traffic, consider batching alerts or filtering by scores.suspect threshold to avoid flooding your Slack channel. For example, only alert when suspect > 50.
9
Section 9
Troubleshooting
~2 min
Common issues you may encounter when integrating webhooks, along with solutions. Check the Webhooks → Delivery Logs page in your Prynt Dashboard for detailed request/response information for each delivery attempt.
IssueSymptomsSolution
Signature mismatchVerification fails; your handler returns 401. Deliveries show as failed in the Dashboard.Ensure you are using the raw request body (not a parsed/re-serialized JSON object) for HMAC computation. Check that your signing secret matches the one returned when the webhook was created. Make sure your framework is not modifying the body before your handler runs.
Timeout (10s)Deliveries marked as failed with "timeout" status. All 3 attempts fail.Your endpoint is taking too long to respond. Return 200 OK immediately and move all processing to a background queue. Check for slow database queries, external API calls, or blocking operations in your handler.
SSL/TLS errorsDeliveries fail with "TLS handshake failure" or "certificate verify failed".Your endpoint must have a valid, non-expired TLS certificate issued by a trusted CA. Self-signed certificates are not accepted. Verify your certificate chain is complete (includes intermediates). Use openssl s_client -connect yourapp.com:443 to debug.
Payload too largeYour server returns 413 Request Entity Too Large.Webhook payloads can be up to 256 KB. Ensure your web server and framework are configured to accept request bodies of at least this size. In Express: express.raw({ limit: '1mb' }). In Nginx: client_max_body_size 1m;.
Duplicate eventsSame event processed multiple times, causing duplicate actions.Implement idempotency using the idempotencyKey field. Store processed keys in Redis or your database with a 24-hour TTL and skip events you have already handled.
Webhook disabledEvents stop arriving. Dashboard shows webhook status as "disabled".Prynt auto-disables webhooks after 24 hours of consecutive failures. Fix the underlying endpoint issue, then re-enable from the Webhooks page. Use the "Send Test Event" button to verify before re-enabling.
Firewall blockingDeliveries fail with connection refused or timeout. Endpoint works in browser.Whitelist Prynt's webhook delivery IP ranges in your firewall. Current ranges are published at api.prynt.io/.well-known/webhook-ips.
📌
Test with the CLI. Use the Prynt CLI to send test events to your local endpoint during development: prynt webhooks test --url http://localhost:3000/webhooks/prynt --event identification.complete. This sends a realistic sample payload and displays the response.

Useful debugging commands:

Prynt CLITerminal
# List all webhooks prynt webhooks list # View delivery history for a specific webhook prynt webhooks deliveries wh_3kR8mNqT7xP2v --limit 20 # Inspect a specific delivery attempt (headers, body, response) prynt webhooks inspect del_x9K2mNpR7jT3q # Send a test event to your endpoint prynt webhooks test \ --url https://yourapp.com/webhooks/prynt \ --event identification.complete # Retry all failed deliveries for the past 24 hours prynt webhooks retry wh_3kR8mNqT7xP2v --since 24h # Stream webhook deliveries in real time (tail -f style) prynt webhooks stream wh_3kR8mNqT7xP2v

Related guides:

💬
Need help? Check the full API reference, browse integration examples, or reach out to our engineering team at support@prynt.io. We typically respond within 2 hours.

Ready to go real-time?

Set up your first webhook in under 3 minutes. Free plan includes 1,000 events/month.

Create Free Account → View Full API Docs