---
title: "Identity Verification"
description: "Identity verification in Dub utilizes the Veriff platform to authenticate partner identities, safeguard the partner network against duplicate identity fraud, and build trust with program owners. Th..."
last_updated: "2026-10-05T05:07:35.164326+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/identity-verification"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/actions/partners/start-identity-verification.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts)
- [apps/web/lib/dub.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dub.ts)
- [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx)
- [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx)
- [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx)
- [apps/web/app/api/callback/plain/partner/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/partner/route.ts)
- [apps/web/app/ee/api/appsflyer/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/appsflyer/webhook/route.ts)
- [apps/web/app/ee/api/cron/partners/verify-country-change/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/partners/verify-country-change/route.ts)
- [apps/web/app/ee/api/track/application/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts)
- [apps/web/app/ee/api/embed/referrals/tremendous/send-otp/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/embed/referrals/tremendous/send-otp/route.ts)
- [apps/web/app/ee/api/embed/referrals/tremendous/verify-otp/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/embed/referrals/tremendous/verify-otp/route.ts)
- [apps/web/ui/partners/identity-verification/identity-verification-card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-card.tsx)
- [apps/web/lib/veriff/create-veriff-session.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/create-veriff-session.ts)
- [packages/email/src/templates/broadcasts/identity-verification-announcement.tsx](https://github.com/blade47/dub/blob/HEAD/packages/email/src/templates/broadcasts/identity-verification-announcement.tsx)
- [apps/web/lib/actions/partners/start-partner-platform-verification.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-partner-platform-verification.ts)
- [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/quickstart.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx)
- [apps/web/app/api/user/referrals-token/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/user/referrals-token/route.ts)
- [apps/web/app/api/dub/webhook/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/dub/webhook/route.ts)
- [apps/web/lib/veriff/client.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts)
- [apps/web/lib/auth/track-dub-lead.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/track-dub-lead.ts)
- [apps/web/app/api/veriff/webhook/handle-decision-event.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts)
- [apps/web/ui/modals/social-verification-by-code-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/social-verification-by-code-modal.tsx)
- [apps/web/lib/partners/sync-partner-identity.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/sync-partner-identity.ts)
- [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts)
- [apps/web/lib/auth/partner.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/partner.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/tracking/installation-section.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/tracking/installation-section.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/tracking/verify-install.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/tracking/verify-install.tsx)
- [apps/web/app/ee/partners.dub.co/onboarding/onboarding/payouts/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(onboarding)/onboarding/payouts/page.tsx)
- [apps/web/app/ee/app.dub.co/embed/referrals/token.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/token.tsx)
</details>

## Overview

Identity verification in Dub utilizes the Veriff platform to authenticate partner identities, safeguard the partner network against duplicate identity fraud, and build trust with program owners. The verification system encompasses API client integrations, automated webhook decision handlers, cron verification routines, and specialized user interface components across both partner and administrative portals.

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:1-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L1-L89), [apps/web/lib/veriff/client.ts:1-47](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L1-L47), [apps/web/app/api/veriff/webhook/handle-decision-event.ts:1-141](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L1-L141)

## Veriff Integration Client and Configuration

### Overview

The Veriff integration layer provides low-level connectivity to the Veriff Station API (`https://stationapi.veriff.com/v1`), managing authenticated HTTP requests, HMAC signature generation for decision retrieval, and session initialization. The architecture inherits from an underlying `HttpBaseClient` and enforces runtime environment assertions via `assertEnv` to guarantee required credentials are present.

Sources: [apps/web/lib/veriff/client.ts:1-47](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L1-L47)

### Client Implementation and Environment Configuration

The `VeriffClient` class configures base connectivity settings, including a vendor identifier, the API base URL, and response logging behavior. Authentication headers are dynamically constructed by asserting the presence of `VERIFF_API_KEY`.

```typescript
class VeriffClient extends HttpBaseClient {
  protected readonly vendor = "Veriff";
  protected readonly baseUrl = "https://stationapi.veriff.com/v1";
  protected readonly logResponseBodies = false;

  protected buildAuthHeaders() {
    return {
      "X-AUTH-CLIENT": assertEnv("VERIFF_API_KEY"),
    };
  }
}
```

Sources: [apps/web/lib/veriff/client.ts:11-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L11-L20)

> [!IMPORTANT]
> Both `VERIFF_API_KEY` and `VERIFF_SHARED_SECRET` are asserted at runtime via `assertEnv()`. Omitting these environment variables will cause immediate failure during header generation or cryptographic signing.

Sources: [apps/web/lib/veriff/client.ts:1-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L1-L44)

### Session Initialization and Decision Fetching

The client exposes methods for creating verification sessions and retrieving session decisions. The `fetchSessionDecision` method computes an `X-HMAC-SIGNATURE` header using SHA-256 over the target `sessionId` with the `VERIFF_SHARED_SECRET`.

```typescript
  async createSession(input: z.input<typeof veriffCreateSessionInputSchema>) {
    return await this.post("/sessions", {
      input,
      inputSchema: veriffCreateSessionInputSchema,
      outputSchema: veriffCreateSessionOutputSchema,
    });
  }

  async fetchSessionDecision(sessionId: string) {
    const hmacSignature = crypto
      .createHmac("sha256", assertEnv("VERIFF_SHARED_SECRET"))
      .update(sessionId)
      .digest("hex");

    return await this.get(`/sessions/${sessionId}/decision`, {
      headers: {
        "X-HMAC-SIGNATURE": hmacSignature,
      },
      outputSchema: veriffDecisionEventSchema,
    });
  }
```

Sources: [apps/web/lib/veriff/client.ts:23-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L23-L44)

The high-level wrapper function `createVeriffSession` processes partner records by splitting full names into given and family name parts, handling edge cases where only a single name token exists, and invoking `veriffClient.createSession`.

```typescript
export async function createVeriffSession({
  partner,
}: {
  partner: Pick<Partner, "id" | "email" | "name">;
}) {
  const nameParts = partner.name.split(" ");
  const firstName = nameParts[0] || partner.name;
  const lastName = nameParts.slice(1).join(" ") || partner.name;

  try {
    return await veriffClient.createSession({
      verification: {
        vendorData: partner.id,
        person: {
          firstName,
          lastName,
        },
      },
    });
  } catch (error) {
    throw new Error(
      "Failed to create Veriff session. Please try again later or contact support.",
    );
  }
}
```

Sources: [apps/web/lib/veriff/create-veriff-session.ts:4-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/create-veriff-session.ts#L4-L28)

### Call-Chain Execution Walkthrough

When initiating a partner verification session, execution flows sequentially through client parsing, HTTP transport, and remote API ingestion:

1. `createVeriffSession()` receives the partner object, splits `partner.name` by whitespace to isolate `firstName` and `lastName`, and constructs the payload.
2. `veriffClient.createSession()` accepts the Zod-validated input and delegates to `this.post("/sessions")` on the `HttpBaseClient`.
3. `buildAuthHeaders()` injects the `X-AUTH-CLIENT` header by evaluating `assertEnv("VERIFF_API_KEY")`.
4. The Station API returns the session response conforming to `veriffCreateSessionOutputSchema`.

Sources: [apps/web/lib/veriff/create-veriff-session.ts:4-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/create-veriff-session.ts#L4-L22), [apps/web/lib/veriff/client.ts:16-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L16-L29)

### Veriff Client Configuration Reference

| Property / Method | Target / Type | Purpose | Sources |
| :--- | :--- | :--- | :--- |
| `vendor` | `"Veriff"` | Identifies the client vendor in base logs and errors. | [apps/web/lib/veriff/client.ts:12-12](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L12-L12) |
| `baseUrl` | `"https://stationapi.veriff.com/v1"` | Root endpoint for all Veriff Station API requests. | [apps/web/lib/veriff/client.ts:13-13](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L13-L13) |
| `logResponseBodies` | `boolean` (`false`) | Controls whether raw HTTP response bodies are dumped to logs. | [apps/web/lib/veriff/client.ts:14-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L14-L14) |
| `buildAuthHeaders()` | Method | Asserts and returns the `X-AUTH-CLIENT` header mapping to `VERIFF_API_KEY`. | [apps/web/lib/veriff/client.ts:16-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L16-L20) |
| `createSession()` | Method | Dispatches a POST request to `/sessions` with input/output validation schemas. | [apps/web/lib/veriff/client.ts:23-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L23-L29) |
| `fetchSessionDecision()` | Method | Computes SHA-256 HMAC signature and fetches session decisions via GET `/sessions/{sessionId}/decision`. | [apps/web/lib/veriff/client.ts:32-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L32-L44) |

Sources: [apps/web/lib/veriff/client.ts:11-45](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/veriff/client.ts#L11-L45)

## Initiating Identity Verification Flows

### Overview

The platform provides two distinct mechanisms for initiating Veriff identity verification sessions: a user-facing Server Action (`startIdentityVerificationAction`) and an administrative API route (`POST /api/admin/partners/[partnerId]/generate-veriff-session`). Both entry points enforce state checks, validate existing session expiration, and interact with the database using Prisma.

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:16-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L16-L89), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:12-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L12-L89)

### Server Action and Admin Endpoint Implementation

The user-facing server action utilizes `authPartnerActionClient.action` and requires the executing partner user to possess the `partner_profile.update` permission via `throwIfNoPermission`. Conversely, the administrative route uses the `withAdmin` middleware, restricting execution exclusively to users with the `owner` role.

```typescript
export const startIdentityVerificationAction = authPartnerActionClient.action(
  async ({ ctx }) => {
    const { partner, partnerUser } = ctx;

    throwIfNoPermission({
      role: partnerUser.role,
      permission: "partner_profile.update",
    });

    if (partner.identityVerificationStatus) {
      switch (partner.identityVerificationStatus) {
        case "approved":
          throw new Error(
            "Your identity has already been verified. No further action is required.",
          );
        case "submitted":
        case "review":
          throw new Error(
            "A verification attempt is already in progress. Please wait for it to complete or resubmit.",
          );
      }
    }
    // ...
  },
);
```

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:16-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L16-L37), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:12-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L12-L48)

> [!WARNING]
> Attempting to generate a session when `identityVerificationStatus` is `approved`, `submitted`, or `review` will immediately abort execution, throwing a validation error or returning a `400` HTTP response depending on whether the server action or admin route was invoked.

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:25-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L25-L37), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:34-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L34-L48)

### Session Reuse and Rate Limiting

To prevent duplicate active sessions and manage external API consumption, both flows check existing metadata before provisioning a new Veriff session. If an unexpired session is found, its existing URL is returned directly.

```typescript
    const veriffMetadata = parseVeriffMetadata(partner.veriffMetadata);

    if (
      veriffMetadata.attemptCount >= MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS
    ) {
      throw new Error(
        "You've reached the maximum number of identity verification attempts. Please contact support: https://dub.co/support",
      );
    }

    if (
      partner.veriffSessionId &&
      veriffMetadata.sessionUrl &&
      veriffMetadata.sessionExpiresAt &&
      veriffMetadata.sessionExpiresAt > new Date()
    ) {
      return {
        sessionUrl: veriffMetadata.sessionUrl,
      };
    }

    await assertRateLimit({
      policy: RATELIMIT_POLICIES.identityVerificationStart,
      identifier: partner.id,
    });
```

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:39-65](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L39-L65), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:50-64](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L50-L64)

> [!NOTE]
> The server action enforces Upstash rate limiting using `RATELIMIT_POLICIES.identityVerificationStart` keyed on the partner ID, whereas the admin endpoint relies strictly on the `owner` role authorization wrapper.

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:62-65](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L62-L65), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:86-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L86-L89)

### Call-Chain Execution Walkthrough

When a session cannot be reused and passes rate limits, execution proceeds through creation and database persistence:

1. `startIdentityVerificationAction()` or `POST` route verifies authorization and parses partner metadata via `parseVeriffMetadata()`.
2. `assertRateLimit()` ensures the partner has not exceeded initiation quotas.
3. `createVeriffSession()` is called with the partner record to obtain a new verification object containing `verification.id` and `verification.url`.
4. `prisma.partner.update()` persists the new session ID and merges metadata containing a 7-day expiration calculated via `addDays(new Date(), 7)`.
5. The session URL is returned to the client.

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:39-87](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L39-L87), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:50-84](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L50-L84)

### Verification Flow Parameters and Methods

| Flow / Handler | Authorization Requirement | Rate Limit Policy | Success Return Value | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `startIdentityVerificationAction` | `partner_profile.update` permission | `RATELIMIT_POLICIES.identityVerificationStart` | `{ sessionUrl: string }` | [apps/web/lib/actions/partners/start-identity-verification.ts:16-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L16-L88) |
| `POST /api/admin/partners/[partnerId]/generate-veriff-session` | Admin `owner` role via `withAdmin` | None (Admin-bypassed) | `NextResponse.json({ sessionUrl: string })` | [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:12-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L12-L89) |

Sources: [apps/web/lib/actions/partners/start-identity-verification.ts:16-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/actions/partners/start-identity-verification.ts#L16-L88), [apps/web/app/ee/api/admin/partners/partnerId/generate-veriff-session/route.ts:12-89](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/partners/%5BpartnerId%5D/generate-veriff-session/route.ts#L12-L89)

## Veriff Webhook and Decision Processing

### Overview

The verification subsystem processes incoming Veriff decision webhooks and handles asynchronous background cron verifications for partner country changes. When Veriff posts a decision event or a cron job executes, the system resolves partner sessions, maps raw verification statuses to Prisma enums, checks for duplicate identity fraud and country mismatches, updates metadata, and dispatches transactional emails with idempotency keys.

Sources: [apps/web/app/ee/api/cron/partners/verify-country-change/route.ts:21-140](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/partners/verify-country-change/route.ts#L21-L140), [apps/web/app/api/veriff/webhook/handle-decision-event.ts:32-141](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L32-L141)

### Veriff Decision Status Mapping

Veriff webhook decision events report raw verification statuses that are translated directly into Prisma `IdentityVerificationStatus` enum values.

| Veriff Raw Status | Prisma `IdentityVerificationStatus` Mapping | Sources |
| :--- | :--- | :--- |
| `approved` | `approved` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |
| `declined` | `declined` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |
| `expired` | `expired` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |
| `abandoned` | `abandoned` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |
| `review` | `review` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |
| `resubmission_requested` | `resubmissionRequested` | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30) |

Sources: [apps/web/app/api/veriff/webhook/handle-decision-event.ts:20-30](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L20-L30)

### Webhook Event Handling Call-Chain

When `handleDecisionEvent` receives a Veriff webhook payload, it executes a strict sequence of validation, fraud evaluation, and database synchronization steps:

1. `handleSessionDecision()` extracts verification details including `id`, `status`, `decisionTime`, `reason`, `attemptId`, and `riskLabels`.
2. `prisma.partner.findUnique()` searches for a partner matching `veriffSessionId: id`.
3. If `effectiveStatus === "approved"`, `checkCountryMismatch()` and duplicate risk label checks are evaluated; if either triggers, `effectiveStatus` is overridden to `"declined"`.
4. `parseVeriffMetadata()` and `mergeVeriffMetadata()` update the attempt count, decline reason, and clear active session URLs unless `resubmission_requested` is set.
5. `prisma.partner.update()` persists the new verification status, `identityVerifiedAt` timestamp, and merged metadata.
6. `sendEmailNotification()` dispatches the appropriate transactional email with an idempotency header derived from `attemptId`.

Sources: [apps/web/app/api/veriff/webhook/handle-decision-event.ts:32-141](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L32-L141)

> [!WARNING]
> If a partner's verified document country does not match their account country during webhook processing, `effectiveStatus` is forcefully overridden from `approved` to `declined`, even if Veriff's automated risk engine initially approved the session.

Sources: [apps/web/app/api/veriff/webhook/handle-decision-event.ts:95-98](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L95-L98)

### Country Change Verification Cron Route

The cron route at `POST /api/cron/partners/verify-country-change` periodically validates existing partner country settings against their verified Veriff session data:

```typescript
export const POST = withCron(async ({ rawBody }) => {
  const { partnerId } = schema.parse(JSON.parse(rawBody));

  const partner = await prisma.partner.findUnique({
    where: { id: partnerId },
    select: {
      id: true,
      name: true,
      email: true,
      country: true,
      identityVerificationStatus: true,
      identityVerifiedAt: true,
      veriffSessionId: true,
      veriffMetadata: true,
    },
  });

  if (!partner || !partner.veriffSessionId || !partner.country) {
    return logAndRespond("Partner, session ID, or country missing. Skipping.");
  }

  const { verification } = await veriffClient.fetchSessionDecision(
    partner.veriffSessionId,
  );

  const documentCountry =
    (verification.document?.country || verification.person?.nationality) ?? null;

  if (partner.country.toLowerCase() === documentCountry?.toLowerCase()) {
    if (!partner.identityVerifiedAt) {
      await prisma.partner.update({
        where: { id: partner.id },
        data: { identityVerifiedAt: new Date() },
      });
      await sendEmail({
        to: partner.email!,
        subject: "Your identity has been verified",
        react: PartnerIdentityVerified({
          partner: { name: partner.name, email: partner.email! },
        }),
      });
    }
  } else {
    const declineReason =
      "Your account country no longer matches your verified identity document country. Please re-verify.";
    const { attemptCount } = parseVeriffMetadata(partner.veriffMetadata);

    await prisma.partner.update({
      where: { id: partner.id },
      data: {
        identityVerificationStatus: null,
        identityVerifiedAt: null,
        veriffSessionId: null,
        veriffMetadata: mergeVeriffMetadata(partner.veriffMetadata, {
          sessionUrl: null,
          sessionExpiresAt: null,
          declineReason,
          attemptCount,
        }),
      },
    });

    await sendEmail({
      to: partner.email!,
      subject: "Identity re-verification required",
      react: PartnerIdentityVerificationFailed({
        failureType: "countryChange",
        failureReasonText: declineReason,
        partner: { name: partner.name, email: partner.email! },
      }),
    });
  }

  return logAndRespond("Country verification check completed.");
});
```

Sources: [apps/web/app/ee/api/cron/partners/verify-country-change/route.ts:21-140](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/partners/verify-country-change/route.ts#L21-L140)

> [!NOTE]
> When a country mismatch is detected by the cron route, the partner's verification status, verified timestamp, and active session ID are reset to `null`, while the cumulative `attemptCount` is preserved through metadata merging.

Sources: [apps/web/app/ee/api/cron/partners/verify-country-change/route.ts:98-119](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/partners/verify-country-change/route.ts#L98-L119)

## Duplicate Identity Fraud Detection

### Overview

When Veriff flags a decision event with risk labels indicating duplicate identities, the webhook handler overrides the verification status to declined and asynchronously dispatches `detectDuplicateIdentityFraud`. This routine evaluates program enrollments against configured fraud rules and executes automated financial remediation by holding pending and processed commissions across affected accounts.

Sources: [apps/web/app/api/veriff/webhook/handle-decision-event.ts:85-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L85-L94), [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:17-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L17-L23)

### Fraud Detection Call-Chain Execution

The execution flow for identifying and acting on duplicate identity fraud proceeds through a series of filtering, grouping, and persistence steps:

`detectDuplicateIdentityFraud()` → filters `riskLabels` against `veriffRiskLabels` and extracts associated `sessionIds` → queries `prisma.programEnrollment.findMany()` for matching partners → filters enrollments via `isFraudRuleEnabled()` for `FraudRuleType.partnerDuplicateAccount` → groups enrollments by `programId` via `partnersByProgram` reducer → filters out programs with fewer than two partners → builds `CreateFraudEventInput` records for active enrollments → persists fraud events via `createFraudEvents()` → settles commission freezes via `holdPendingCommissions()` and `holdProcessedCommissions()`.

Sources: [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:17-144](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L17-L144)

> [!TIP]
> `detectDuplicateIdentityFraud` automatically merges the current verification session ID into the extracted risk label session IDs and de-duplicates the resulting array before querying program enrollments.

Sources: [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:30-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L30-L39)

### Fraud Detection Parameters and Criteria

| Parameter / Entity | Source Definition | Purpose / Condition | Sources |
| :--- | :--- | :--- | :--- |
| `veriffSessionId` | `VeriffDecisionEvent["verification"]["id"]` | Current session identifier injected into the target session pool. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:17-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L17-L23) |
| `riskLabels` | `VeriffDecisionEvent["verification"]["riskLabels"]` | Array of risk labels returned by Veriff containing related session IDs. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:17-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L17-L23) |
| `FraudRuleType.partnerDuplicateAccount` | `@prisma/client` | Fraud rule type evaluated to determine if duplicate account checks are active for a program. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:1-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L1-L15) |
| `INACTIVE_ENROLLMENT_STATUSES` | `@/lib/zod/schemas/partners` | Status list used to skip inactive program enrollments during fraud event generation. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:1-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L1-L15) |

Sources: [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:1-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L1-L15)

### Design Trade-Offs in Fraud Evaluation

| Design Choice | Benefit | Cost | Sources |
| :--- | :--- | :--- | :--- |
| Asynchronous execution via `waitUntil` | Prevents webhook timeout by deferring heavy database queries and commission updates. | Errors during background processing must be caught via promise settling rather than returning HTTP failure codes. | [apps/web/app/api/veriff/webhook/handle-decision-event.ts:89-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L89-L94), [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:137-144](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L137-L144) |
| Program-scoped grouping | Isolates duplicate detection to specific referral programs where rules are enabled. | Requires querying and filtering enrollments across all associated sessions before grouping. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:83-102](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L83-L102) |
| Parallel commission holding via `Promise.allSettled` | Executes pending and processed commission holds concurrently without cascading failures. | Requires manual iteration over rejected settlement results to log errors. | [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:137-144](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L137-L144) |

Sources: [apps/web/app/api/veriff/webhook/handle-decision-event.ts:89-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/veriff/webhook/handle-decision-event.ts#L89-L94), [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:83-102](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L83-L102), [apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts:137-144](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts#L137-L144)

## Partner and Admin UI Surfaces

### Overview

The partner portal exposes verification components across several interactive elements, including profile sections, promotional banners, and floating reminder cards. The `IdentityVerificationSection` component renders within the partner profile view, handling state transitions for verification status, attempt counts, and error messaging.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:21-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L21-L28), [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:60-91](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L60-L91)

When an action is initiated via `startIdentityVerificationAction`, the client executes the mutation and dynamically loads `@veriff/incontext-sdk` to instantiate a Veriff frame with the returned `sessionUrl`.

```typescript
const { executeAsync, isPending } = useAction(
  startIdentityVerificationAction,
  {
    onError: ({ error }) => {
      toast.error(
        parseActionError(error, "Failed to start identity verification."),
      );
    },
    onSuccess: async ({ data }) => {
      const { createVeriffFrame, MESSAGES } = await import(
        "@veriff/incontext-sdk"
      );

      createVeriffFrame({
        url: data.sessionUrl,
        onEvent: (msg) => {
          if (msg === MESSAGES.FINISHED) {
            toast.success(
              "Verification submitted. We'll update your status shortly.",
            );
            mutate();
          }
        },
      });

      mutate();
    },
  },
);
```

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:30-58](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L30-L58)

> [!NOTE]
> Maximum verification attempts are tracked via `identityVerificationAttemptCount` against the `MAX_PARTNER_IDENTITY_VERIFICATION_ATTEMPTS` threshold. Reaching the limit disables the start button unless the account is approved or currently pending review.

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:79-84](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L79-L84), [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:211-218](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L211-L218)

### Promotional Banners and Cards

Partners receive prominent prompts to verify their identity via `IdentityVerificationBanner` and `IdentityVerificationCard`. The banner displays a radial-gradient background with floating shield assets, linking directly to `/profile#identity-verification` or allowing dismissal to the card layout by updating the promo state to `card`.

Sources: [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:13-38](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L13-L38), [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:82-98](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L82-L98)

| Component Name | File Path | Visibility Condition | Action / Target | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `IdentityVerificationBanner` | `apps/web/ui/partners/identity-verification/identity-verification-banner.tsx` | `partner` exists and banner status is active | Links to `/profile#identity-verification` or dismisses to card | [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:13-19](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L13-L19) |
| `IdentityVerificationCard` | `apps/web/ui/partners/identity-verification/identity-verification-card.tsx` | `partner` exists, status is `card`, and pathname is not `/profile` | Links to `/profile#identity-verification` | [apps/web/ui/partners/identity-verification/identity-verification-card.tsx:13-21](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-card.tsx#L13-L21) |

Sources: [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:13-19](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L13-L19), [apps/web/ui/partners/identity-verification/identity-verification-card.tsx:13-21](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-card.tsx#L13-L21)

### Administrative Review Surfaces

Administrators manage network partner verification via `NetworkIdentityVerification`, which exposes tools to generate Veriff session URLs or manually verify partners with a US LLC.

```typescript
export function NetworkIdentityVerification({
  partner,
}: {
  partner: Pick<AdminNetworkPartner, "id" | "identityVerifiedAt">;
}) {
  const [sessionUrl, setSessionUrl] = useState<string | null>(null);
  const [isGeneratingSession, setIsGeneratingSession] = useState(false);
  const [isVerifyingIdentity, setIsVerifyingIdentity] = useState(false);
  const partnerIdRef = useRef(partner.id);
...
```

Sources: [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:10-19](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L10-L19)

> [!WARNING]
> Manual verification via `handleVerifyIdentity` posts to `/api/admin/partners/${requestPartnerId}/verify-identity` and is restricted if `identityVerifiedAt` is already set.

Sources: [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:72-83](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L72-L83), [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:159-163](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L159-L163)

### UI Component Design Trade-Offs

| Design Choice | Benefit | Cost | Sources |
| :--- | :--- | :--- | :--- |
| Dynamic import of Veriff SDK (`@veriff/incontext-sdk`) | Reduces initial bundle size by loading the Veriff client-side framework only when verification starts. | Introduces a network load step upon click before the iframe can be initialized. | [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:39-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L39-L43) |
| Component-level ref tracking (`partnerIdRef`) in admin view | Prevents stale async state updates if a different partner row is selected while a request is pending. | Requires redundant checks against `partnerIdRef.current` across fetch catch and finally blocks. | [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:18-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L18-L25), [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:49-68](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L49-L68) |
| Conditional banner/card state persistence | Allows users to minimize intrusive banners into compact cards without losing prompt visibility. | Requires maintaining local promo display state across client navigation. | [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:15-16](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L15-L16) |

Sources: [apps/web/app/ee/partners.dub.co/dashboard/profile/identity-verification-section.tsx:39-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/identity-verification-section.tsx#L39-L43), [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:18-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L18-L25), [apps/web/app/ee/admin.dub.co/dashboard/partners/network/identity-verification.tsx:49-68](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/partners/network/identity-verification.tsx#L49-L68), [apps/web/ui/partners/identity-verification/identity-verification-banner.tsx:15-16](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/identity-verification/identity-verification-banner.tsx#L15-L16)

## Related

- [Fraud Detection and Hold Rules](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/fraud-detection-and-hold-rules)
- [Payout Processing](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/payout-processing)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
