Check whether a user passed
Your backend can ask Zyphe, at any time, whether a user has passed a verification. You look the user up by the email you gave when you created their verification request, by the customData you attached to it, or by both, and you get back the status of every matching flow result. The answer carries no personal data collected during the verification.
When to use it
Webhooks remain the way to react to a result as it happens. The search answers a different question, "where does this user stand right now?", and fits when:
- a user signs in and you need to know whether to let them through, and you do not keep the outcome yourself;
- you are reconciling your records with Zyphe, for example after your webhook endpoint was down;
- you want to avoid sending a user through a flow they have already passed.
If you already store the outcome from webhooks, read it from your own records instead. The search is a status check, not a way to fetch results: for the data and documents a user submitted, use the Export API.
Request
POST https://api.zyphe.com/sdk/organizations/{organization_id}/flow-results/search?sandbox=false
x-api-key: zyphe_sk_...
Content-Type: application/json
| Piece | Value |
|---|---|
| Path | organization_id: your organization's ID |
| Query | sandbox=true for sandbox results, sandbox=false for production (required) |
| Header | x-api-key with a secret key (zyphe_sk_…). Publishable keys are rejected |
| Body | The search criteria below |
| Field | Type | Meaning |
|---|---|---|
email | string | The user's email, as given when the verification request was created. Matched exactly, ignoring letter case. |
customData | object | Custom data the flow result must carry. Every key you send must be present with the same value; keys you leave out are not compared. |
flowSlug | string | Limits the search to one flow. Without it, every flow of your organization is searched. |
skip | integer | How many matches to skip. Default 0. |
take | integer | How many matches to return, from 1 to 100. Default 20. |
Send at least one of email and a non-empty customData; otherwise the API answers 400. When you send several criteria, a flow result has to match all of them.
customData is matched against the flow result's custom data, the object you passed when creating the verification request (see Custom fields). Searching by your own user ID (for example { "userId": "42" }) is the most reliable lookup, because it does not depend on the user keeping the same email address.
Response
{
"totalCount": 1,
"page": [
{
"flowResultId": "936f35a8-4921-430e-b016-15be48968886",
"flowId": "2d8285d7-f4ba-42df-ab3f-681d9870d37a",
"flowSlug": "kyc-flow",
"status": "COMPLETED",
"passed": true,
"email": "jane.doe@example.com",
"customData": { "userId": "42" },
"createdAt": "2026-10-01T09:12:44.192Z",
"updatedAt": "2026-10-01T09:20:03.551Z"
}
]
}
| Field | Meaning |
|---|---|
totalCount | How many flow results match, across all pages. |
page[] | The matches on this page, most recently updated first. |
flowResultId | The flow result. Use it with the Export API when you need the details. |
flowId | The flow, when the result belongs to one. |
flowSlug | The flow's slug. |
status | Where the run stands, in the API status vocabulary (Statuses and codes). |
passed | true when the user passed the verification, that is when status is COMPLETED. |
email | The user's email, when the identity has one. |
customData | The flow result's custom data. |
createdAt | When the run started. |
updatedAt | When the run last changed. |
How to read the answer:
| You get | Meaning |
|---|---|
A match with passed: true | The user passed that flow. |
Matches, none with passed: true | The user started but has not passed. status says whether the run is in progress, in review, or failed. Treat review as pending, not as a rejection. |
An empty page | The user never started a verification that matches, in this environment. |
A user can have several flow results, for example one per flow, or a new run after a failed one. Decide on the result for the flow you care about: pass flowSlug, or pick the first match for that slug, since the page is sorted by most recent update.
Node SDK
import { searchFlowResults, type SDKOptions } from '@zyphe-sdk/node'
const opts: SDKOptions = {
apiKey: process.env.ZYPHE_SECRET_API_KEY!,
environment: 'production',
}
const { data, error } = await searchFlowResults(
{
organizationId: '<organization-id>',
isSandbox: false,
search: {
customData: { userId: '42' },
flowSlug: 'kyc-flow',
skip: 0,
take: 20,
},
},
opts,
)
if (error) {
throw new Error(`Search failed: ${JSON.stringify(error)}`)
}
const hasPassed = data.page.some((result) => result.passed)
searchFlowResults requires a secret key. See Node SDK.
curl
curl -X POST "https://api.zyphe.com/sdk/organizations/${ZYPHE_ORGANIZATION_ID}/flow-results/search?sandbox=true" \
-H "x-api-key: ${ZYPHE_SECRET_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"email": "jane.doe@example.com",
"flowSlug": "kyc-flow"
}'
Sandbox and keys
Sandbox and production results are separate. A search with sandbox=true only sees sandbox runs, and a user who passed in sandbox has an empty page in production.
API keys belong to one environment: use a secret key created in sandbox with sandbox=true, and a production secret key with sandbox=false. A key sent with the other environment's flag is rejected with invalid_api_key. See Sandbox mode.
Keep the secret key on your server. Never call this endpoint from a browser or a mobile app.
Errors
| Status | When |
|---|---|
400 | Neither email nor a non-empty customData was sent, email is not a valid address, or take is out of range |
401 | Missing or invalid API key, a publishable key, or a key from the other environment |