Workflows API
The Workflows API lets you send DDQ questionnaires, identity verification requests, or both to an entity in a single API call, mirroring the full flow available in the KYC Genie web interface.
Under the hood, every send creates an OnboardingSession - a secure magic-link session that lets the entity access their DDQ, complete identity verification, or both, without needing a KYC Genie account.
The existing POST /responses/ endpoint creates a
response object but uses an older email delivery path. The Workflows API uses
the OnboardingSession system - the same infrastructure the web UI uses - giving
you magic-link delivery, combined DDQ + verification flows, and a richer response
payload including the session ID and expiry timestamp.
Two Delivery Paths
Choose which path fits your integration:
| Path | How to trigger | What happens |
|---|---|---|
| Direct send | prefill: false (default) - or omit |
Entity + Response (DRAFT) + optional Verification Session created,
workflow generated, magic-link email dispatched immediately,
send_ddq credit charged.
|
| Prefill → Send |
prefill: true
|
Response created in PREFILL status only.
No email, no credit charge. You fill the form via
PUT /responses/{id}/save/, then call
POST /responses/{id}/send/ when ready.
All verification config is stored on the response and dispatched automatically
at send time.
|
Response Objects
The two endpoints return different shapes.
POST /workflows/send/ response
| Field | Type | Description |
|---|---|---|
success | boolean | true when the workflow completed successfully. |
message | string | Human-readable success message. |
workflow_type | string | ddq_only, verification_only, or ddq_and_verification. |
workflow_session_id | uuid | null | OnboardingSession ID. null on the prefill path - session is created when /send/ is called. |
status | string | sent or prefill_ready. |
email_sent | boolean | true if the onboarding email was dispatched in this request. |
email_sent_to | string | null | Recipient email address. null on the prefill path. |
entity_id | uuid | ID of the entity. |
response_id | uuid | null | ID of the created Response. null if no DDQ block was included. |
verification_session_id | uuid | null | ID of the VerificationSession. null if verification was not included or on the prefill path. |
has_ddq | boolean | true when a DDQ response was created. |
has_verification | boolean | true when identity verification was requested. |
POST /responses/{id}/send/ response
| Field | Type | Description |
|---|---|---|
session_id | uuid | OnboardingSession ID - always present after a successful send. |
status | string | sent once the email is dispatched. |
email_sent | boolean | true when the magic-link email was dispatched. |
entity_id | uuid | ID of the entity. |
entity_name | string | Display name of the entity. |
response_id | uuid | ID of the Response that was sent. |
verification_session_id | uuid | null | ID of the VerificationSession created at send time. null if no verification config was stored on the response. |
steps | string[] | Steps included in the onboarding session, e.g. ["ddq"] or ["ddq", "verification"]. |
expires_at | datetime | UTC timestamp when the magic link expires. |
Send Workflow
Creates an entity or uses an existing one, optionally creates a DDQ response and/or identity verification session, and dispatches a magic-link workflow email.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
entity_id | uuid | Conditional | ID of an existing entity. Mutually exclusive with the entity block. |
entity | object | Conditional | Inline entity creation - entity_type, legal_name, jurisdiction, etc. Mutually exclusive with entity_id. |
ddq | object | Conditional* | DDQ block (questionnaire_id, response_name, prefill). At least one of ddq or verification must be provided. |
verification | object | Conditional* | Verification block (checks: {document, identity, enhancedIdentity, proofOfAddress}). Requires entity_type: individual. |
email | string | Conditional | Email address to deliver the workflow link to. Required unless ddq.prefill is true. |
expiry_hours | integer | No | Hours until the workflow magic link expires (1–168, default 48). |
| Parameter | Type | Required | Description |
|---|---|---|---|
entity_id | uuid | Yes | ID of an existing entity. Inline entity creation is not supported in v2. |
questionnaire_id | integer or UUID string | Conditional* | DDQ template - integer PK or UUID string. At least one of questionnaire_id or verification must be provided. |
response_name | string | No | Optional display name for the created response. |
prefill | boolean | No | When true, creates a PREFILL response. No email sent, no credits charged until POST /questionnaire-responses/{id}/send/. |
verification | object | Conditional* | Flat verification checks object (see below). Requires entity_type: individual. |
email | string | Conditional | Email address to deliver the workflow link to. Required unless prefill is true. |
expiry_hours | integer | No | Hours until the workflow magic link expires (1–168, default 48). |
Verification Block
The verification block requires the entity to be entity_type: individual.
Verification Checks
Pass check fields directly on the verification object.
| Field | Type | Description |
|---|---|---|
document | boolean | Run a document check (passport, driving licence, etc.) |
identity | boolean | Run a biometric selfie / liveness check |
enhanced_identity | boolean | Run an enhanced video identity check |
proof_of_address | boolean | Request a proof-of-address document upload |
API Response
Returns a Send Workflow response with status 201 Created.
Send DDQ
curl -X POST https://api.kycgenie.com/api/v1/workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"entity": {"entity_type": "company", "legal_name": "Acme Corp", "jurisdiction": "GB"},
"ddq": {"questionnaire_id": 55},
"email": "contact@acme.com"
}'
# v2: flat body - entity_id required, no entity/ddq wrappers
curl -X POST workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
"questionnaire_id": 55,
"email": "contact@acme.com"
}'
import requests
import uuid
resp = requests.post(
"https://api.kycgenie.com/api/v1/workflows/send/",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"entity": {"entity_type": "company", "legal_name": "Acme Corp", "jurisdiction": "GB"},
"ddq": {"questionnaire_id": 55},
"email": "contact@acme.com",
},
)
print(resp.json())
from kycgenie import KYCGenie
client = KYCGenie(api_key="YOUR_API_KEY")
result = client.workflows.send(
entity_id="1b83f447-4a1e-4589-8a12-b8767e3c5426",
questionnaire_id=55,
email="contact@acme.com",
)
print(f"Status: {result.status}")
print(f"Response ID: {result.response_id}")
print(f"Session ID: {result.workflow_session_id}")
const resp = await fetch('https://api.kycgenie.com/api/v1/workflows/send/', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
entity: { entity_type: 'company', legal_name: 'Acme Corp', jurisdiction: 'GB' },
ddq: { questionnaire_id: 55 },
email: 'contact@acme.com',
}),
});
console.log(await resp.json());
// v2: flat body - entity_id required, no entity/ddq wrappers
const resp = await fetch('workflows/send/', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
entity_id: '1b83f447-4a1e-4589-8a12-b8767e3c5426',
questionnaire_id: 55,
email: 'contact@acme.com',
}),
});
console.log(await resp.json());
Send DDQ + Verification (individuals only)
curl -X POST https://api.kycgenie.com/api/v1/workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"ddq": {"questionnaire_id": 55},
"verification": {"checks": {"document": true, "identity": true}},
"email": "jane@example.com"
}'
curl -X POST workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"questionnaire_id": 55,
"verification": {"document": true, "identity": true},
"email": "jane@example.com"
}'
import requests, uuid
resp = requests.post(
"workflows/send/",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"questionnaire_id": 55,
"verification": {"document": True, "identity": True},
"email": "jane@example.com",
},
)
print(resp.json())
from kycgenie import KYCGenie
client = KYCGenie(api_key="YOUR_API_KEY")
result = client.workflows.send(
entity_id="0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
questionnaire_id=55,
email="jane@example.com",
verification={
"document": True,
"identity": True,
},
)
print(f"Workflow type: {result.workflow_type}")
print(f"Response ID: {result.response_id}")
print(f"Verification session: {result.verification_session_id}")
const resp = await fetch('https://api.kycgenie.com/api/v1/workflows/send/', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
entity_id: '0a2a70d6-4338-413d-af74-0ffb2a81a9f9',
ddq: { questionnaire_id: 55 },
verification: { checks: { document: true, identity: true } },
email: 'jane@example.com',
}),
});
console.log(await resp.json());
const resp = await fetch('workflows/send/', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
entity_id: '0a2a70d6-4338-413d-af74-0ffb2a81a9f9',
questionnaire_id: 55,
verification: { document: true, identity: true },
email: 'jane@example.com',
}),
});
console.log(await resp.json());
Response (201 Created)
{
"success": true,
"message": "Workflow sent successfully.",
"workflow_type": "ddq_only",
"workflow_session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "sent",
"email_sent": true,
"email_sent_to": "contact@acme.com",
"entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
"response_id": "770g0611-g40d-63f6-c938-668877662222",
"verification_session_id": null,
"has_ddq": true,
"has_verification": false
}
Prefill Workflow
Populate the questionnaire on behalf of the entity before they see it. Useful when you have existing data or documents to pre-load.
-
Create a PREFILL response - call
POST /workflows/send/withprefill: true. If you want identity verification included, pass theverificationblock now - it is stored on the response and dispatched automatically at send time. You receive aresponse_id. No email is sent, no credits charged. -
(Optional) AI autofill - call
POST /questionnaire-responses/{id}/autofill/. The AI reads entity documents and fills answers automatically. Monitor completion via theresponse.autofill_completedwebhook. -
(Optional) Fill or edit answers manually - call
PUT /questionnaire-responses/{id}/save/. Can be combined with autofill. -
Send to the entity - call
POST /questionnaire-responses/{id}/send/. The response transitions to DRAFT, a workflow session and any pending verification session are created together, the magic-link email is dispatched, and credits are charged.
Pass the verification block once when
creating the PREFILL response. You do not need to pass it again
when calling /send/ - it fires automatically
and the entity sees the DDQ and identity checks in a single onboarding session.
Create PREFILL response
curl -X POST https://api.kycgenie.com/api/v1/workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"ddq": {"questionnaire_id": 55, "prefill": true}
}'
curl -X POST workflows/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"questionnaire_id": 55,
"prefill": true
}'
import requests
resp = requests.post(
"workflows/send/",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"questionnaire_id": 55,
"prefill": True,
},
)
data = resp.json()
response_id = data["response_id"] # save for later steps
from kycgenie import KYCGenie
client = KYCGenie(api_key="YOUR_API_KEY")
result = client.workflows.send(
entity_id="0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
questionnaire_id=55,
prefill=True,
)
response_id = result.response_id # save for later steps
# result.status == "prefill_ready"
# result.email_sent == False
const resp = await fetch('https://api.kycgenie.com/api/v1/workflows/send/', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
entity_id: '0a2a70d6-4338-413d-af74-0ffb2a81a9f9',
ddq: { questionnaire_id: 55, prefill: true },
}),
});
const { response_id } = await resp.json(); // save for later steps
const resp = await fetch('workflows/send/', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
entity_id: '0a2a70d6-4338-413d-af74-0ffb2a81a9f9',
questionnaire_id: 55,
prefill: true,
}),
});
const { response_id } = await resp.json(); // save for later steps
Response (201 Created)
{
"success": true,
"message": "Prefill response created successfully.",
"workflow_type": "ddq_only",
"workflow_session_id": null,
"status": "prefill_ready",
"email_sent": false,
"email_sent_to": null,
"entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
"response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
"verification_session_id": null,
"has_ddq": true,
"has_verification": false
}
Send a Prefill Response
Transitions a PREFILL response to DRAFT, creates a verification session if pending_verification_config was set, and dispatches the workflow email.
/api/v2/questionnaire-responses/{response_id}/send/
Path Parameters
| Parameter | Type | Description |
|---|---|---|
response_id | uuid | ID of the PREFILL response to send |
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Conditional | Email to deliver the workflow link to. If omitted, falls back to the email address stored on the entity. |
expiry_hours | integer | No | Hours until the magic link expires (1-168). |
API Response
Returns a Send Prefill response with status 200 OK.
Send an existing PREFILL response
curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "contact@acme.com", "expiry_hours": 96}'
curl -X POST questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "contact@acme.com", "expiry_hours": 96}'
import requests
response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"
resp = requests.post(
f"https://api.kycgenie.com/api/v1/responses/{response_id}/send/",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"email": "contact@acme.com", "expiry_hours": 96},
)
print(resp.json())
from kycgenie import KYCGenie
client = KYCGenie(api_key="YOUR_API_KEY")
result = client.questionnaire_responses.send(
response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
email="contact@acme.com",
expiry_hours=96,
)
print(f"Session ID: {result.session_id}")
print(f"Entity: {result.entity_name}")
print(f"Steps: {result.steps}")
print(f"Verification session: {result.verification_session_id}")
print(f"Expires: {result.expires_at}")
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';
const resp = await fetch(`https://api.kycgenie.com/api/v1/responses/${responseId}/send/`, {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'contact@acme.com', expiry_hours: 96 }),
});
console.log(await resp.json());
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';
const resp = await fetch(`questionnaire-responses/${responseId}/send/`, {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'contact@acme.com', expiry_hours: 96 }),
});
console.log(await resp.json());
Response (200 OK)
{
"session_id": "9fc3392f-bbc3-4c3e-9b67-e697410e2acd",
"status": "sent",
"email_sent": true,
"entity_id": "040f7df7-0321-43d7-ba71-4f8bdf7bbf0e",
"entity_name": "Grace Younglund",
"response_id": "f93dc316-c783-48f9-b2e8-1ac82b7cfcc4",
"verification_session_id": null,
"steps": ["ddq"],
"expires_at": "2026-06-16T19:36:32Z"
}
Credits
| Action | Charged? |
|---|---|
Direct send (POST /workflows/send/ - non-prefill) |
Yes |
| Creating a PREFILL response | No |
POST /questionnaire-responses/{id}/send/
|
Yes |
If the email delivery fails, the response object is rolled back and no credits are charged. You will receive
a 500 error and can safely retry.