Skip to main content

Sync vs Async Flows

Zenoo supports two execution models. Choose based on whether user interaction is required and how your system handles latency.
Quick rule: server-to-server checks use sync. User-facing journeys use async.

Synchronous (Model 1)

The API blocks until all checks complete or the timeout is reached. Results are returned in the response body.

How to use

Add the X-SYNC-TIMEOUT header to any /api endpoint. The value is in milliseconds.
curl -X POST \
  "https://instance.prod.onboardapp.io/api/gateway/execute/{project_hash}/api" \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: your-api-key" \
  -H "X-SYNC-TIMEOUT: 60000" \
  -d '{
    "company_name": "Acme Holdings Ltd",
    "registration_number": "12345678",
    "country": "GB"
  }'
The API holds the connection open for up to X-SYNC-TIMEOUT milliseconds, then returns whatever results are available.

When to use

  • Server-to-server checks: AML screening, company registry verification
  • Automated pipelines: batch processing, background jobs, cron tasks
  • Simple flows: single-provider checks that complete quickly
  • Development and testing: fastest way to see results

Timeout behavior

If the response contains a tokens field, some checks are still processing. Use the pull token to retrieve the remaining results.
ScenarioResponse
All checks complete within timeout200 with full results
Timeout reached, some checks pendingPartial results with tokens for polling the rest
Provider errorError response with details and retry guidance
Check typeTimeoutNotes
AML screening only15,000 msSingle provider call, usually completes in 3-10s
Company registry lookup30,000 msDepends on jurisdiction, some registries are slower
Full Company (registry + screening)60,000 msMultiple providers in sequence
Server-side Person (database + phone)30,000 msMultiple lightweight checks in parallel
Setting timeouts below 10 seconds forces unnecessary polling. Setting them above 90 seconds ties up HTTP connections without benefit. Start with the recommended values and adjust based on your p95 latency.

Comparison table

AspectSync (Model 1)Async (Model 2)
Endpoint suffix/api/init
Response typeFull results (or partial + tokens)Tokens only
BlockingYes, up to timeoutNo
User interactionNot supportedSupported via start token
Result retrievalIn response bodyPull endpoint or webhook
Best forScreening, Company, server-side PersonPerson with document capture, biometrics
Timeout controlX-SYNC-TIMEOUT header (ms)N/A
Typical latency10-60 secondsInstant (tokens), results in 30s-5min

Processing timeline

Typical durations for each processing stage:
StageDuration
API gateway routing< 1 second
Company registry lookup5-15 seconds
PEP/sanctions screening (WorldCheck)3-10 seconds
Document verification (after user submits)10-30 seconds
Biometric liveness (after user submits)5-15 seconds
Risk model evaluation1-5 seconds
Full Company Verification (automated)15-45 seconds
Full Person Verification (user-dependent)1-30 minutes
Person Verification elapsed time is dominated by user behavior: finding documents, taking photos, retrying failed captures. Once the user submits, server-side processing completes in 30 to 60 seconds.

Mixing models

Most production integrations use both models. A typical pattern:
  1. Company Verification (sync). Submit company data, get results in the response.
  2. Person Verification (async). Initiate journeys for directors and UBOs, redirect them to verification URLs.
  3. Screening (sync). Re-screen existing customers for ongoing monitoring.
integration.js
// 1. Company Verification - sync
const companyResult = await fetch(
  `${BASE_URL}/api/gateway/execute/${PROJECT_HASH}/api`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": API_KEY,
      "X-SYNC-TIMEOUT": "60000"
    },
    body: JSON.stringify({
      company_name: "Acme Holdings Ltd",
      registration_number: "12345678",
      country: "GB"
    })
  }
);
const companyData = await companyResult.json();

// 2. Person Verification - async (for each director/UBO)
const personInit = await fetch(
  `${BASE_URL}/api/gateway/execute/${PROJECT_HASH}/kyc/init`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": API_KEY
    },
    body: JSON.stringify({
      first_name: "Jane",
      last_name: "Smith",
      country: "GB",
      external_reference: "YOUR-REF-2026-0123"
    })
  }
);
const { tokens } = await personInit.json();
const verificationUrl = `${BASE_URL}/${PROJECT_HASH}/?t=${tokens.start}`;
// Send verificationUrl to the user via email or in-app

// 3. Screening - sync (ongoing monitoring)
const screenResult = await fetch(
  `${BASE_URL}/api/gateway/execute/${PROJECT_HASH}/screening/api`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": API_KEY,
      "X-SYNC-TIMEOUT": "15000"
    },
    body: JSON.stringify({
      name: "John Smith",
      date_of_birth: "1975-03-20",
      country: "GB",
      entity_type: "person",
      categories: ["pep", "sanctions", "adverse_media", "watchlist"]
    })
  }
);

Polling example (Node.js)

A complete polling implementation with exponential backoff:
poll-results.js
async function pollForResults(pullToken, options = {}) {
  const {
    baseUrl = "https://instance.prod.onboardapp.io",
    maxAttempts = 30,
    initialDelay = 10_000, // 10 seconds
    maxDelay = 30_000, // 30 seconds
    backoffAfter = 12 // switch to maxDelay after 12 attempts (2 min)
  } = options;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(
      `${baseUrl}/api/gateway/sharable-payload/${pullToken}`
    );

    if (response.status === 200) {
      return await response.json();
    }

    if (response.status === 404) {
      throw new Error("Invalid or expired pull token");
    }

    if (response.status !== 204) {
      throw new Error(`Unexpected status: ${response.status}`);
    }

    // 204 - still processing
    const delay = attempt < backoffAfter ? initialDelay : maxDelay;
    await new Promise((resolve) => setTimeout(resolve, delay));
  }

  throw new Error(`Polling timed out after ${maxAttempts} attempts`);
}

// Usage
try {
  const results = await pollForResults("eyJhbGciOiJIUzI1NiJ9.pull-token...");
  console.log("Verification complete:", results.processing_status);
} catch (err) {
  console.error("Polling failed:", err.message);
}

Next steps