PRIV ProtocolPRIV Docs
Guides

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:

  1. Go to Settings > Webhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Copy the signing secret

3. Configure Events

Choose which events to receive:

EventDescription
user.createdNew user signed up
user.updatedUser profile changed
contribution.submittedUser submitted data
contribution.validatedData passed validation
contribution.rejectedData failed validation
payout.pendingPayout scheduled
payout.completedPayout processed
payout.failedPayout failed
api_key.createdNew API key generated
api_key.revokedAPI 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:

StatusMeaningPRIV Behavior
200SuccessEvent delivered
4xxClient errorNo retry
5xxServer errorRetry 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:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 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/priv

Test Events

Send test events from the dashboard:

  1. Go to Settings > Webhooks
  2. Select your endpoint
  3. Click Send Test Event
  4. 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:

  1. Go to Settings > Webhooks
  2. Select your endpoint
  3. 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
}

Next Steps