V-PIN Webhook
Receive notifications when Veratad merges, splits, or retires V-PINs and learn how to verify webhook signatures.
Status
Early Access: This is an early version for select partners. Interfaces and payloads may change, and we will work with partners on updates. Contact support to request access.
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
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 includeNEW_DATA_AVAILABLE
,HUMAN_REVIEW
, andAI_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
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 includeCOLLISION_DETECTED
,HUMAN_REVIEW
, andAI_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.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
Verify signature on every request.
Deduplicate using the event id.
For
vpin.merged
events, migrate references fromsuperseded[].vpin
tocanonical_vpin
.For
vpin.split
events, evaluate thereplacements[]
payload and re-key impacted records to the correct new V-PIN; archive or retire any lingering references tosource_vpin
.Update caches and any identity graphs keyed by V-PIN.
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