Webhooks
Receive real-time notifications when events occur in your PRIV integration.
Overview
Webhooks allow your application to receive real-time HTTP notifications when events occur in the PRIV platform.
Real-time Updates
Get instant notifications instead of polling the API.
Secure Delivery
All webhooks are signed with HMAC-SHA256 for verification.
Setting Up Webhooks
1. Create a Webhook Endpoint
First, create an endpoint in your application to receive webhook events:
// app/api/webhooks/priv/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
const WEBHOOK_SECRET = process.env.PRIV_WEBHOOK_SECRET!
export async function POST(request: NextRequest) {
const body = await request.text()
const signature = request.headers.get('x-priv-signature')
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex')
if (signature !== `sha256=${expectedSignature}`) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
const event = JSON.parse(body)
// Handle the event
switch (event.type) {
case 'user.created':
await handleUserCreated(event.data)
break
case 'contribution.validated':
await handleContributionValidated(event.data)
break
case 'payout.completed':
await handlePayoutCompleted(event.data)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}2. Register the Webhook
Register your endpoint in the PRIV dashboard:
- Go to Settings > Webhooks
- Click Add Endpoint
- Enter your endpoint URL
- Select events to subscribe to
- Copy the signing secret
3. Configure Events
Choose which events to receive:
| Event | Description |
|---|---|
user.created | New user signed up |
user.updated | User profile changed |
contribution.submitted | User submitted data |
contribution.validated | Data passed validation |
contribution.rejected | Data failed validation |
payout.pending | Payout scheduled |
payout.completed | Payout processed |
payout.failed | Payout failed |
api_key.created | New API key generated |
api_key.revoked | API key revoked |
Event Payload Structure
All webhook events follow a consistent structure:
interface WebhookEvent {
id: string // Unique event ID
type: string // Event type
created_at: string // ISO 8601 timestamp
data: Record<string, unknown> // Event-specific data
api_version: string // API version
}Example Payloads
user.created
{
"id": "evt_abc123",
"type": "user.created",
"created_at": "2026-01-22T10:30:00Z",
"data": {
"user_id": "user_xyz789",
"email": "user@example.com",
"wallet_address": "0x1234...abcd",
"created_at": "2026-01-22T10:30:00Z"
},
"api_version": "2026-01-01"
}contribution.validated
{
"id": "evt_def456",
"type": "contribution.validated",
"created_at": "2026-01-22T11:00:00Z",
"data": {
"contribution_id": "contrib_123",
"user_id": "user_xyz789",
"type": "image",
"quality_score": 0.95,
"payout_amount": "0.25",
"payout_currency": "PRIV"
},
"api_version": "2026-01-01"
}payout.completed
{
"id": "evt_ghi789",
"type": "payout.completed",
"created_at": "2026-01-22T12:00:00Z",
"data": {
"payout_id": "payout_456",
"user_id": "user_xyz789",
"amount": "25.50",
"currency": "PRIV",
"transaction_hash": "0xabc...def",
"chain": "base"
},
"api_version": "2026-01-01"
}Webhook Security
Signature Verification
Always verify webhook signatures to ensure requests come from PRIV:
import crypto from 'crypto'
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expectedSignature)
)
}Timestamp Validation
Prevent replay attacks by checking the timestamp:
function isTimestampValid(timestamp: string, toleranceSeconds = 300): boolean {
const eventTime = new Date(timestamp).getTime()
const currentTime = Date.now()
const diff = Math.abs(currentTime - eventTime)
return diff < toleranceSeconds * 1000
}Complete Verification Example
import crypto from 'crypto'
interface WebhookHeaders {
'x-priv-signature': string
'x-priv-timestamp': string
}
function verifyWebhook(
payload: string,
headers: WebhookHeaders,
secret: string
): boolean {
// 1. Check timestamp
const timestamp = headers['x-priv-timestamp']
if (!isTimestampValid(timestamp)) {
throw new Error('Webhook timestamp too old')
}
// 2. Verify signature
const signature = headers['x-priv-signature']
const signedPayload = `${timestamp}.${payload}`
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expectedSignature)
)
}Handling Webhooks
Idempotency
Webhook events may be delivered more than once. Use the event ID for idempotency:
import { db } from '@/lib/db'
async function handleWebhookEvent(event: WebhookEvent) {
// Check if already processed
const existing = await db.processedEvents.findUnique({
where: { event_id: event.id }
})
if (existing) {
console.log(`Event ${event.id} already processed`)
return
}
// Process the event
await processEvent(event)
// Mark as processed
await db.processedEvents.create({
data: { event_id: event.id, processed_at: new Date() }
})
}Async Processing
For long-running operations, acknowledge the webhook immediately and process asynchronously:
import { Queue } from 'bullmq'
const webhookQueue = new Queue('webhooks')
export async function POST(request: NextRequest) {
const body = await request.text()
// Verify signature (as shown above)
if (!verifyWebhook(body, headers, secret)) {
return NextResponse.json({ error: 'Invalid' }, { status: 401 })
}
const event = JSON.parse(body)
// Queue for async processing
await webhookQueue.add('process', event)
// Return immediately
return NextResponse.json({ received: true })
}Error Handling
Return appropriate status codes:
| Status | Meaning | PRIV Behavior |
|---|---|---|
200 | Success | Event delivered |
4xx | Client error | No retry |
5xx | Server error | Retry with backoff |
export async function POST(request: NextRequest) {
try {
// Process webhook
await handleWebhook(request)
return NextResponse.json({ received: true })
} catch (error) {
console.error('Webhook error:', error)
// Return 500 to trigger retry
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
)
}
}Retry Policy
PRIV retries failed webhooks with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7+ | 24 hours (max 3 days) |
After 3 days of failures, the webhook is disabled and you'll receive an email notification.
Testing Webhooks
Local Development
Use ngrok or similar to expose your local server:
# Install ngrok
brew install ngrok
# Expose local port
ngrok http 3000
# Use the HTTPS URL in PRIV dashboard
# https://abc123.ngrok.io/api/webhooks/privTest Events
Send test events from the dashboard:
- Go to Settings > Webhooks
- Select your endpoint
- Click Send Test Event
- Choose an event type
CLI Testing
# Using curl to simulate a webhook
curl -X POST http://localhost:3000/api/webhooks/priv \
-H "Content-Type: application/json" \
-H "x-priv-signature: sha256=test_signature" \
-H "x-priv-timestamp: 2026-01-22T10:00:00Z" \
-d '{
"id": "evt_test123",
"type": "user.created",
"created_at": "2026-01-22T10:00:00Z",
"data": {
"user_id": "user_test",
"email": "test@example.com"
},
"api_version": "2026-01-01"
}'Webhook Logs
View webhook delivery logs in the dashboard:
- Go to Settings > Webhooks
- Select your endpoint
- Click View Logs
Each log entry shows:
- Event ID and type
- Delivery status
- Response code and body
- Response time
- Retry attempts
TypeScript Types
// types/webhooks.ts
export interface WebhookEvent<T = unknown> {
id: string
type: WebhookEventType
created_at: string
data: T
api_version: string
}
export type WebhookEventType =
| 'user.created'
| 'user.updated'
| 'contribution.submitted'
| 'contribution.validated'
| 'contribution.rejected'
| 'payout.pending'
| 'payout.completed'
| 'payout.failed'
| 'api_key.created'
| 'api_key.revoked'
export interface UserCreatedData {
user_id: string
email: string
wallet_address?: string
created_at: string
}
export interface ContributionValidatedData {
contribution_id: string
user_id: string
type: 'image' | 'video' | 'audio' | 'document'
quality_score: number
payout_amount: string
payout_currency: string
}
export interface PayoutCompletedData {
payout_id: string
user_id: string
amount: string
currency: string
transaction_hash: string
chain: string
}