Preventing Duplicates

Duplicate verification sessions waste money, confuse users, and create data quality issues. This guide covers strategies for preventing duplicates at every layer.

Use idempotency keys

The most reliable way to prevent duplicate session creation is the Idempotency-Key header. If a network error occurs and you retry the request, Verifa returns the original response instead of creating a second session.

$curl -X POST https://api.withverifa.com/api/v1/sessions \
> -H "X-API-Key: vk_live_your_key_here" \
> -H "Idempotency-Key: signup_user_abc123" \
> -H "Content-Type: application/json" \
> -d '{
> "external_ref": "user_abc123",
> "country": "US"
> }'

Key design tips:

  • Use a value that uniquely identifies the intent (e.g., signup_\{user_id\} or reverify_\{user_id\}_\{date\})
  • Don’t use random values generated on each attempt — that defeats the purpose
  • Keys are cached for 24 hours

See Idempotency for the full specification.

Use external references

Always set external_ref to your internal user ID when creating sessions. This:

  • Links the session to a persistent identity record
  • Lets you query existing sessions before creating new ones
  • Enables duplicate detection across sessions

Check before creating

Before creating a new session, check if the user already has an active one:

$# Check for pending/processing sessions for this user
$curl "https://api.withverifa.com/api/v1/sessions?external_ref=user_abc123&status=pending" \
> -H "X-API-Key: vk_live_your_key_here"

If an active session exists, resume it instead of creating a new one:

$curl -X POST https://api.withverifa.com/api/v1/sessions/session_existing123/resume \
> -H "X-API-Key: vk_live_your_key_here"

Handle expired sessions

When a user’s session expires (they took too long), resume it rather than creating a new session:

$curl -X POST https://api.withverifa.com/api/v1/sessions/session_abc123/resume \
> -H "X-API-Key: vk_live_your_key_here"

This re-issues a capture token and preserves any documents already uploaded.

Duplicate detection during verification

Verifa automatically detects when the same person verifies under different external_ref values. The duplicate_detection check searches across all previous sessions using:

  • Device fingerprint — Same browser or device
  • Email address — Same email submitted during capture
  • Phone number — Same phone submitted during capture
  • Document number — Same ID document number extracted via OCR
  • Face embedding — Same face via cosine similarity matching
  • Name + DOB — Same name and date of birth combination
  • IP address — Same IP address (weaker signal)

When duplicates are detected, the signals appear in the risk assessment and can route the session to manual review.

Client-side best practices

Disable the submit button

Prevent the user from clicking “Start Verification” multiple times:

1async function startVerification() {
2 const button = document.getElementById("verify-btn");
3 button.disabled = true;
4 button.textContent = "Starting...";
5
6 try {
7 const session = await createSession();
8 window.location.href = session.capture_url;
9 } catch (error) {
10 button.disabled = false;
11 button.textContent = "Start Verification";
12 showError(error);
13 }
14}

Cache the session

Store the session ID locally so refreshing the page doesn’t trigger a new session:

1async function getOrCreateSession(userId) {
2 const cached = localStorage.getItem(`verifa_session_${userId}`);
3 if (cached) {
4 const session = JSON.parse(cached);
5 // Check if still active
6 const current = await fetchSession(session.id);
7 if (["pending", "capturing"].includes(current.status)) {
8 return current;
9 }
10 }
11
12 const session = await createSession(userId);
13 localStorage.setItem(`verifa_session_${userId}`, JSON.stringify(session));
14 return session;
15}

Webhook deduplication

In rare cases, you may receive the same webhook event more than once. Use the session_id to deduplicate:

1async def handle_webhook(event):
2 session_id = event["data"]["session_id"]
3 event_type = event["event"]
4
5 # Check if already processed
6 if await is_processed(session_id, event_type):
7 return {"status": "already_processed"}
8
9 # Process the event
10 await process_event(event)
11
12 # Mark as processed
13 await mark_processed(session_id, event_type)