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.
| Scenario | Response |
|---|
| All checks complete within timeout | 200 with full results |
| Timeout reached, some checks pending | Partial results with tokens for polling the rest |
| Provider error | Error response with details and retry guidance |
Recommended timeouts
| Check type | Timeout | Notes |
|---|
| AML screening only | 15,000 ms | Single provider call, usually completes in 3-10s |
| Company registry lookup | 30,000 ms | Depends on jurisdiction, some registries are slower |
| Full Company (registry + screening) | 60,000 ms | Multiple providers in sequence |
| Server-side Person (database + phone) | 30,000 ms | Multiple 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.
Asynchronous (Model 2)
The API returns immediately with tokens. Processing happens in the background.How to use
Call the /init endpoint without the X-SYNC-TIMEOUT header.curl -X POST \
"https://instance.prod.onboardapp.io/api/gateway/execute/{project_hash}/kyc/init" \
-H "Content-Type: application/json" \
-H "X-API-KEY: your-api-key" \
-d '{
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1990-06-15",
"country": "GB",
"external_reference": "YOUR-REF-2026-0123"
}'
Response:{
"tokens": {
"pull": "eyJhbGciOiJIUzI1NiJ9.pull-token...",
"start": "eyJhbGciOiJIUzI1NiJ9.start-token..."
}
}
When to use
- User-facing journeys: Person Verification document capture, biometric liveness
- Long-running flows: multi-provider checks, manual review steps
- User interaction required: the user must upload a document or complete liveness
- High-volume processing: avoid holding HTTP connections open
Token pair
The response contains two tokens:| Token | Purpose | Lifetime |
|---|
pull | Retrieve results via GET /api/gateway/sharable-payload/{pull} | Until results expire |
start | Construct the verification URL for user-facing flows | 24 hours |
Store both tokens in your database immediately. The pull token is the only way to retrieve results. If you lose it, you must reinitiate the verification.Verification URL
For Person Verification flows, construct the verification URL from the start token:https://instance.prod.onboardapp.io/{project_hash}/?t={start_token}
Redirect the user to this URL to begin document capture and biometric verification. The URL expires after 24 hours.Result retrieval
Polling. Call the pull endpoint and check the HTTP status code.curl -X GET \
"https://instance.prod.onboardapp.io/api/gateway/sharable-payload/{pull_token}"
| Status | Meaning |
|---|
200 with JSON body | Results are ready |
204 No Content | Still processing, retry after a delay |
404 | Invalid or expired token |
Webhook. Configure an endpoint to receive verification.completed events. See Webhooks Guide.Both (recommended). Use webhooks for real-time notification, with polling as a fallback for missed webhooks.
Comparison table
| Aspect | Sync (Model 1) | Async (Model 2) |
|---|
| Endpoint suffix | /api | /init |
| Response type | Full results (or partial + tokens) | Tokens only |
| Blocking | Yes, up to timeout | No |
| User interaction | Not supported | Supported via start token |
| Result retrieval | In response body | Pull endpoint or webhook |
| Best for | Screening, Company, server-side Person | Person with document capture, biometrics |
| Timeout control | X-SYNC-TIMEOUT header (ms) | N/A |
| Typical latency | 10-60 seconds | Instant (tokens), results in 30s-5min |
Processing timeline
Typical durations for each processing stage:
| Stage | Duration |
|---|
| API gateway routing | < 1 second |
| Company registry lookup | 5-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 evaluation | 1-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:
- Company Verification (sync). Submit company data, get results in the response.
- Person Verification (async). Initiate journeys for directors and UBOs, redirect them to verification URLs.
- Screening (sync). Re-screen existing customers for ongoing monitoring.
// 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:
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