V-PIN Webhook

Receive notifications when Veratad merges or retires V-PINs and learn how to verify webhook signatures.

Status

Purpose

Notify customers when Veratad determines that multiple V-PINs refer to the same individual and consolidates them into a single canonical V-PIN, or when a V-PIN is retired after being linked to fraudulent activity.


High-Level Overview

When a client is returned a V-PIN in any request across all Veratad services, that V-PIN is automatically added to a monitor for that client. If future intelligence indicates that multiple V-PINs actually represent the same person, Veratad will merge them and publish a vpin.merged webhook event to subscribed endpoints. If a V-PIN is determined to be synthetic or tied to a fake identity, Veratad will retire it and emit a vpin.retired event.

Customers should treat the canonical V-PIN as the ongoing identifier and migrate all references from the superseded V-PIN(s) to the canonical V-PIN. Retired V-PINs should be purged from systems and not used in future transactions.


Event Model

Event Types

  • vpin.merged — Sent when one or more V-PINs are merged into a canonical V-PIN.

  • vpin.retired — Sent when a V-PIN is permanently retired due to confirmed or suspected fraud.

  • vpin.merge.reverted — Sent if a prior merge is undone.

  • vpin.retire.reverted — Sent if a previously retired V-PIN is reinstated.

vpin.merged is required for monitoring. Other events, including vpin.retired, are opt‑in.

Event Envelope

All webhook deliveries share a standard envelope for parsing and idempotency.

{
  "id": "evt_01J6X9VQ8E2Q3RZ2KQYH3F7W2B",
  "type": "vpin.merged",
  "version": "2025-09-10",
  "created_at": "2025-09-10T14:22:31.840Z",
  "data": {},
  "trace": {
    "correlation_id": "cor_3b6f6d7e-4a6d-4f7a-b60a-2f8e3fef1c9a",
    "source": "veratad.vpin.monitoring"
  }
}
  • id — Unique event id used for idempotency.

  • version — Semantic version/date of payload contract.

  • trace.correlation_id — Stable id for cross-system troubleshooting.


vpin.merged — Payload Contract

{
  "canonical_vpin": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
  "superseded": [
    {
      "vpin": "a1a1d7a0-1111-4d4b-b2a5-54b5a24be001",
      "first_seen_at": "2024-06-02T11:05:09Z",
      "last_used_at": "2025-08-29T17:21:04Z"
    },
    {
      "vpin": "b2b2d7a0-2222-4d4b-b2a5-54b5a24be002",
      "first_seen_at": "2024-10-18T09:44:20Z",
      "last_used_at": "2025-09-01T12:10:33Z"
    }
  ],
  "effective_at": "2025-09-10T14:22:31Z",
  "reason": {
    "code": "NEW_DATA_AVAILABLE",
    "summary": "Trusted external source linked these identifiers",
    "signals": [
      { "name": "external_data_source", "value": "verified_partner" },
      { "name": "human_review", "value": true },
      { "name": "ai_agent_review", "value": true }
    ]
  },
  "actions": {
    "redirect_window_days": 90,
    "resolution_endpoint": "/v1/vpin/resolve/{vpin}",
    "replay_from": "2024-01-01T00:00:00Z"
  }
}
  • canonical_vpin — V-PIN that survives after the merge; use this going forward.

  • superseded — Array of V-PINs that should no longer be used.

  • effective_at — Timestamp from which the merge is authoritative.

  • reason — Machine and/or human justification. Common codes include NEW_DATA_AVAILABLE, HUMAN_REVIEW, and AI_AGENT_REVIEW.

  • actions.redirect_window_days — Period during which Veratad will auto-resolve superseded V-PINs.

  • actions.resolution_endpoint — Companion API to resolve any historical V-PIN to the canonical V-PIN.

  • actions.replay_from — Earliest recommended timestamp to re-key historical records, if needed.


vpin.retired — Payload Contract

{
  "vpin": "a1a1d7a0-1111-4d4b-b2a5-54b5a24be001",
  "retired_at": "2025-09-15T10:00:00Z",
  "reason": {
    "code": "HUMAN_REVIEW",
    "summary": "Manual review of external data flagged this V-PIN"
  }
}
  • vpin — V-PIN that has been retired and should no longer be used.

  • retired_at — Timestamp from which the retirement is authoritative.

  • reason — Machine and/or human justification for retirement. Examples: HUMAN_REVIEW, AI_AGENT_REVIEW, EXTERNAL_DATA_CONFLICT.


Resolution API

Resolve any V-PIN to its current canonical V-PIN.

GET /v1/vpin/resolve/{vpin}

Response 200

{
  "input": "b2b2d7a0-2222-4d4b-b2a5-54b5a24be002",
  "canonical_vpin": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
  "superseded": true,
  "effective_at": "2025-09-10T14:22:31Z"
}

Response 404

{ "error": "VPIN_NOT_FOUND" }

Webhook Delivery Mechanics

Registration

Include your API token in the Authorization header when registering.

POST /v1/vpin/webhooks/subscriptions

Headers

  • Authorization: Bearer {api_token}

Request

{
  "url": "https://api.yourcompany.com/veratad/webhooks",
  "event_types": ["vpin.merged", "vpin.retired"],
  "secret": "(one-time-generated secret)",
  "enabled": true
}

Response 201

{
  "id": "whsub_01J6XA2C3Y7D4",
  "url": "https://api.yourcompany.com/veratad/webhooks",
  "event_types": ["vpin.merged", "vpin.retired"],
  "status": "active",
  "created_at": "2025-09-10T13:11:21Z"
}

Security & Signing

All webhook requests are signed with an HMAC SHA‑256 using your subscription secret.

Headers

  • X-Veratad-Signature — Hex digest of HMAC SHA‑256 over body, using the subscription secret.

  • X-Veratad-Timestamp — Milliseconds since epoch when the signature was computed.

  • X-Veratad-Event-Id — Same as envelope id for idempotency.

Signature Base String

{timestamp}.{raw_request_body}

Compute HMAC_SHA256(secret, base_string) and compare (constant‑time) to X-Veratad-Signature.

Replay Protection

Reject requests where abs(now - X-Veratad-Timestamp) > 5 minutes.

Example: Verifying Signature

import crypto from 'crypto';

export function verifyWebhook({
  rawBody,
  timestamp,
  signature,
  secret,
}: {
  rawBody: string;
  timestamp: string;
  signature: string;
  secret: string;
}): void {
  const base = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(base)
    .digest('hex');

  const valid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );

  const age = Math.abs(Date.now() - Number(timestamp));
  if (!valid || age > 5 * 60 * 1000) {
    throw new Error('Invalid signature');
  }
}

Retries & Ordering

  • Retries: Up to five attempts with exponential backoff (1m, 5m, 15m, 60m, 120m).

  • Idempotency: Use X-Veratad-Event-Id to dedupe. We may deliver an event more than once.

  • Ordering: Best-effort in-order per subscription; do not rely on strict global ordering.

Response Expectations

Return 2xx to acknowledge receipt. Any non‑2xx will be treated as a failure and retried.


Consumer Responsibilities

  1. Verify signature on every request.

  2. Deduplicate using the event id.

  3. Migrate references from superseded[].vpin to canonical_vpin.

  4. Update caches and any identity graphs keyed by V-PIN.

  5. Optionally re-key history from actions.replay_from if your use case requires.


Change Management & Versioning

  • The envelope version and the vpin event payloads (e.g., vpin.merged, vpin.retired) may evolve. Backwards-compatible changes include new optional fields.

  • Breaking changes will increment the MAJOR version (e.g., 2026-01-01) with 90 days’ notice.


Examples

Merge of Two V-PINs

Request (from Veratad to your webhook)

POST /veratad/webhooks HTTP/1.1
Host: api.yourcompany.com
Content-Type: application/json
X-Veratad-Event-Id: evt_01J6X9VQ8E2Q3RZ2KQYH3F7W2B
X-Veratad-Timestamp: 1757517751840
X-Veratad-Signature: 9e3b0c...d2

{
  "id": "evt_01J6X9VQ8E2Q3RZ2KQYH3F7W2B",
  "type": "vpin.merged",
  "version": "2025-09-10",
  "created_at": "2025-09-10T14:22:31.840Z",
  "data": {
    "canonical_vpin": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
    "superseded": [
      { "vpin": "a1a1d7a0-1111-4d4b-b2a5-54b5a24be001" },
      { "vpin": "b2b2d7a0-2222-4d4b-b2a5-54b5a24be002" }
    ],
    "effective_at": "2025-09-10T14:22:31Z",
    "reason": {
      "code": "NEW_DATA_AVAILABLE",
      "summary": "Trusted external data indicates these V-PINs refer to the same person"
    },
    "actions": {
      "redirect_window_days": 90,
      "resolution_endpoint": "/v1/vpin/resolve/{vpin}"
    }
  },
  "trace": {
    "correlation_id": "cor_3b6f6d7e-4a6d-4f7a-b60a-2f8e3fef1c9a",
    "source": "veratad.vpin.monitoring"
  }
}

Expected Handler Response

HTTP/1.1 200 OK

Retirement of a V-PIN

Request (from Veratad to your webhook)

POST /veratad/webhooks HTTP/1.1
Host: api.yourcompany.com
Content-Type: application/json
X-Veratad-Event-Id: evt_01J6X9VQ8E2Q3RZ2KQYH3F7W2B
X-Veratad-Timestamp: 1757517751840
X-Veratad-Signature: 9e3b0c...d2

{
  "id": "evt_01J6X9VQ8E2Q3RZ2KQYH3F7W2B",
  "type": "vpin.retired",
  "version": "2025-09-10",
  "created_at": "2025-09-15T10:00:00Z",
  "data": {
    "vpin": "a1a1d7a0-1111-4d4b-b2a5-54b5a24be001",
    "retired_at": "2025-09-15T10:00:00Z",
    "reason": {
        "code": "HUMAN_REVIEW",
        "summary": "Manual review of external data flagged this V-PIN"
    }
  },
  "trace": {
    "correlation_id": "cor_3b6f6d7e-4a6d-4f7a-b60a-2f8e3fef1c9a",
    "source": "veratad.vpin.monitoring"
  }
}

Expected Handler Response

HTTP/1.1 200 OK

Test & Sandbox

Use these endpoints to validate your integration.

  • Send test event: POST /v1/vpin/webhooks/subscriptions/{id}:test with { "type": "vpin.merged" } or { "type": "vpin.retired" } to receive a mock delivery.

curl -X POST https://production.response.com/v1/vpin/webhooks/subscriptions/{id}:test \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{ "type": "vpin.merged" }'
  • Replay live event: POST /v1/vpin/webhooks/events/{event_id}:replay re-delivers a historical event to your endpoint (authorization required).

curl -X POST https://production.response.com/v1/vpin/webhooks/events/{event_id}:replay \
  -H "Authorization: Bearer {api_token}"

Changelog

  • 2025-09-10: Early access release.

Last updated