V-PIN Webhook

Receive notifications when Veratad merges, splits, 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, when an issued V-PIN must be split because it was assigned to multiple distinct people, 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 we later discover that a single V-PIN was used for more than one individual, Veratad will split it into distinct V-PINs and emit a vpin.split event that lists the replacements. 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.split — Sent when a V-PIN is split into multiple replacement V-PINs.

  • 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.split and 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.split — Payload Contract

{
  "source_vpin": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
  "replacements": [
    {
      "vpin": "c3c3d7a0-3333-4d4b-b2a5-54b5a24be003",
      "first_seen_at": "2025-09-12T18:00:00Z",
      "links": [
        { "name": "first_name", "value": "Jo***" },
        { "name": "last_name", "value": "Sm***" },
        { "name": "date_of_birth", "value": "1988-04-**" },
        { "name": "verification_confirmation_number", "value": "VRFD-20240915-****" }
      ]
    },
    {
      "vpin": "d4d4d7a0-4444-4d4b-b2a5-54b5a24be004",
      "first_seen_at": "2025-09-12T18:05:32Z",
      "links": [
        { "name": "first_name", "value": "Ma***" },
        { "name": "last_name", "value": "Le***" },
        { "name": "date_of_birth", "value": "1991-11-**" },
        { "name": "verification_confirmation_number", "value": "VPIN-20240801-****" }
      ]
    }
  ],
  "effective_at": "2025-09-12T18:10:00Z",
  "reason": {
    "code": "COLLISION_DETECTED",
    "summary": "Identity collision review determined the original V-PIN represented multiple people",
    "signals": [
      { "name": "human_review", "value": true }
    ]
  },
  "actions": {
    "resolution_endpoint": "/v1/vpin/resolve/{vpin}",
    "superseded_vpin_status": "retired"
  }
}
  • source_vpin — The V-PIN that previously represented more than one person.

  • replacements — Array of new V-PINs to use going forward. Metadata is masked to protect PII and help route each record to the correct subject.

  • effective_at — Timestamp from which the split mapping is authoritative.

  • reason — Machine and/or human justification for the split. Common codes include COLLISION_DETECTED, HUMAN_REVIEW, and AI_AGENT_REVIEW.

  • actions.resolution_endpoint — Use to resolve existing references to the correct replacement V-PIN.

  • actions.superseded_vpin_status — Expected disposition of the original V-PIN (for example, retired).


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 or understand how to handle a split.

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" }

Response 409 (V-PIN was split)

{
  "input": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
  "split": true,
  "replacements": [
    {
      "vpin": "c3c3d7a0-3333-4d4b-b2a5-54b5a24be003",
      "effective_at": "2025-09-12T18:10:00Z"
    },
    {
      "vpin": "d4d4d7a0-4444-4d4b-b2a5-54b5a24be004",
      "effective_at": "2025-09-12T18:10:00Z"
    }
  ]
}

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. For vpin.merged events, migrate references from superseded[].vpin to canonical_vpin.

  4. For vpin.split events, evaluate the replacements[] payload and re-key impacted records to the correct new V-PIN; archive or retire any lingering references to source_vpin.

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

  6. 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.split, 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

Split of a Shared 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_01J6Y3M4N5P6Q7R8S9T0U1V2W3
X-Veratad-Timestamp: 1757691000000
X-Veratad-Signature: 8b7c1d...aa

{
  "id": "evt_01J6Y3M4N5P6Q7R8S9T0U1V2W3",
  "type": "vpin.split",
  "version": "2025-09-10",
  "created_at": "2025-09-12T18:10:00.000Z",
  "data": {
    "source_vpin": "15ebd7a0-2b4e-4d4b-b2a5-54b5a24becce",
    "replacements": [
      { "vpin": "c3c3d7a0-3333-4d4b-b2a5-54b5a24be003" },
      { "vpin": "d4d4d7a0-4444-4d4b-b2a5-54b5a24be004" }
    ],
    "effective_at": "2025-09-12T18:10:00Z",
    "reason": {
      "code": "COLLISION_DETECTED",
      "summary": "Shared V-PIN was reassigned to two unique identities"
    }
  },
  "trace": {
    "correlation_id": "cor_5c7a9b1d-2e3f-4a5b-8c9d-0e1f2a3b4c5d",
    "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" }, { "type": "vpin.split" }, 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.split" }'
  • 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