# V-PIN Webhook

## Status

{% hint style="warning" %}
**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.
{% endhint %}

## 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.

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

<mark style="color:green;">`GET`</mark> `/v1/vpin/resolve/{vpin}`

**Response 200**

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

**Response 404**

```json
{ "error": "VPIN_NOT_FOUND" }
```

**Response 409** (V-PIN was split)

```json
{
  "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.

<mark style="color:green;">`POST`</mark> `/v1/vpin/webhooks/subscriptions`

**Headers**

* `Authorization: Bearer {api_token}`

**Request**

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

**Response 201**

```json
{
  "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

{% tabs %}
{% tab title="TypeScript" %}

```typescript
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');
  }
}
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> None:
    base = f"{timestamp}.{raw_body.decode('utf-8')}"
    expected = hmac.new(secret.encode(), base.encode(), hashlib.sha256).hexdigest()

    valid = hmac.compare_digest(signature, expected)

    age = abs(int(time.time() * 1000) - int(timestamp))
    if not valid or age > 5 * 60 * 1000:
        raise ValueError('Invalid signature')
```

{% endtab %}
{% endtabs %}

### 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)**

```http
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
HTTP/1.1 200 OK
```

### Split of a Shared V-PIN

**Request (from Veratad to your webhook)**

```http
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
HTTP/1.1 200 OK
```

### Retirement of a V-PIN

**Request (from Veratad to your webhook)**

```http
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
HTTP/1.1 200 OK
```

***

## Test & Sandbox

Use these endpoints to validate your integration.

* Send test event: <mark style="color:green;">`POST`</mark> `/v1/vpin/webhooks/subscriptions/{id}:test` with `{ "type": "vpin.merged" }`, `{ "type": "vpin.split" }`, or `{ "type": "vpin.retired" }` to receive a mock delivery.

```bash
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: <mark style="color:green;">`POST`</mark> `/v1/vpin/webhooks/events/{event_id}:replay` re-delivers a historical event to your endpoint (authorization required).

```bash
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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://api.veratad.com/v-pin/webhook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
