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.

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 ddq.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

FieldTypeDescription
successbooleantrue when the workflow completed successfully.
messagestringHuman-readable success message.
workflow_typestringddq_only, verification_only, or ddq_and_verification.
workflow_session_iduuid | nullOnboardingSession ID. null on the prefill path - session is created when /send/ is called.
statusstringsent or prefill_ready.
email_sentbooleantrue if the onboarding email was dispatched in this request.
email_sent_tostring | nullRecipient email address. null on the prefill path.
entity_iduuidID of the entity.
response_iduuid | nullID of the created Response. null if no DDQ block was included.
verification_session_iduuid | nullID of the VerificationSession. null if verification was not included or on the prefill path.
has_ddqbooleantrue when a DDQ response was created.
has_verificationbooleantrue when identity verification was requested.

POST /responses/{id}/send/ response

FieldTypeDescription
session_iduuidOnboardingSession ID - always present after a successful send.
statusstringsent once the email is dispatched.
email_sentbooleantrue when the magic-link email was dispatched.
entity_iduuidID of the entity.
entity_namestringDisplay name of the entity.
response_iduuidID of the Response that was sent.
verification_session_iduuid | nullID of the VerificationSession created at send time. null if no verification config was stored on the response.
stepsstring[]Steps included in the onboarding session, e.g. ["ddq"] or ["ddq", "verification"].
expires_atdatetimeUTC 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

ParameterTypeRequiredDescription
entity_iduuidConditionalID of an existing entity. Mutually exclusive with the entity block.
entityobjectConditionalInline entity creation - entity_type, legal_name, jurisdiction, etc. Mutually exclusive with entity_id.
ddqobjectConditional*DDQ block (questionnaire_id, response_name, prefill). At least one of ddq or verification must be provided.
verificationobjectConditional*Verification block (checks: {document, identity, enhancedIdentity, proofOfAddress}). Requires entity_type: individual.
emailstringConditionalEmail address to deliver the workflow link to. Required unless ddq.prefill is true.
expiry_hoursintegerNoHours until the workflow magic link expires (1–168, default 48).
ParameterTypeRequiredDescription
entity_iduuidYesID of an existing entity. Inline entity creation is not supported in v2.
questionnaire_idinteger or UUID stringConditional*DDQ template - integer PK or UUID string. At least one of questionnaire_id or verification must be provided.
response_namestringNoOptional display name for the created response.
prefillbooleanNoWhen true, creates a PREFILL response. No email sent, no credits charged until POST /questionnaire-responses/{id}/send/.
verificationobjectConditional*Flat verification checks object (see below). Requires entity_type: individual.
emailstringConditionalEmail address to deliver the workflow link to. Required unless prefill is true.
expiry_hoursintegerNoHours until the workflow magic link expires (1–168, default 48).

DDQ Block

In v1, the DDQ config is passed as a nested object under the ddq key.

FieldTypeRequiredDescription
questionnaire_idintegerYesID of the DDQ template to use.
response_namestringNoDisplay name for the response
prefillbooleanNoWhen true, creates a PREFILL response

Verification Block

Individuals Only

The verification block requires the entity to be entity_type: individual.

Verification Checks

Pass check fields directly on the verification object.

FieldTypeDescription
documentbooleanRun a document check (passport, driving licence, etc.)
identitybooleanRun a biometric selfie / liveness check
enhanced_identitybooleanRun an enhanced video identity check
proof_of_addressbooleanRequest a proof-of-address document upload

Wrap check fields in a nested checks object, using camelCase keys.

FieldTypeDescription
documentbooleanRun a document check (passport, driving licence, etc.)
identitybooleanRun a biometric selfie / liveness check
enhancedIdentitybooleanRun an enhanced video identity check
proofOfAddressbooleanRequest 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
}
{
  "success": true,
  "message": "Workflow sent successfully.",
  "workflow_type": "verification_only",
  "workflow_session_id": "b2c3d4e5-f6a7-8901-bcde-f01234567890",
  "status": "sent",
  "email_sent": true,
  "email_sent_to": "jane@example.com",
  "entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
  "response_id": null,
  "verification_session_id": "cc123456-7890-abcd-ef01-234567890abc",
  "has_ddq": false,
  "has_verification": true
}
{
  "success": true,
  "message": "Workflow sent successfully.",
  "workflow_type": "ddq_and_verification",
  "workflow_session_id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
  "status": "sent",
  "email_sent": true,
  "email_sent_to": "jane@example.com",
  "entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
  "response_id": "770g0611-g40d-63f6-c938-668877662222",
  "verification_session_id": "cc123456-7890-abcd-ef01-234567890abc",
  "has_ddq": true,
  "has_verification": true
}

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.

  1. Create a PREFILL response - call POST /workflows/send/ with prefill: trueddq.prefill: true. If you want identity verification included, pass the verification block now - it is stored on the response and dispatched automatically at send time. You receive a response_id. No email is sent, no credits charged.
  2. (Optional) AI autofill - call POST /questionnaire-responses/{id}/autofill/POST /responses/{id}/autofill/. The AI reads entity documents and fills answers automatically. Monitor completion via the response.autofill_completed webhook.
  3. (Optional) Fill or edit answers manually - call PUT /questionnaire-responses/{id}/save/PUT /responses/{id}/save/. Can be combined with autofill.
  4. Send to the entity - call POST /questionnaire-responses/{id}/send/POST /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.
Verification is stored, not re-passed

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
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},
    "verification": {"checks": {"document": true, "identity": 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,
    "verification": {"document": true, "identity": 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,
        "verification": {"document": True, "identity": True},
    },
)
data = resp.json()
response_id = data["response_id"]  # save for later steps
# Verification config stored - fires automatically at /send/
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,
    verification={"document": True, "identity": True},
)
response_id = result.response_id  # save for later steps
# result.status     == "prefill_ready"
# result.email_sent == False
# Verification config stored - fires automatically at /send/
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 },
    verification: { checks: { document: true, identity: true } },
  }),
});
const { response_id } = await resp.json(); // Verification stored - fires at /send/
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,
    verification: { document: true, identity: true },
  }),
});
const { response_id } = await resp.json(); // Verification stored - fires at /send/
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",
    "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" \
  -d '{
    "entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
    "verification": {"document": true, "identity": true},
    "email": "jane@example.com"
  }'
import requests

resp = requests.post(
    "workflows/send/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "entity_id": "0a2a70d6-4338-413d-af74-0ffb2a81a9f9",
        "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",
    verification={"document": True, "identity": True},
    email="jane@example.com",
)
# result.workflow_type           == "verification_only"
# result.has_ddq                 == False
# result.workflow_session_id       is set immediately
# result.verification_session_id   is set immediately
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',
    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' },
  body: JSON.stringify({
    entity_id: '0a2a70d6-4338-413d-af74-0ffb2a81a9f9',
    verification: { document: true, identity: true },
    email: 'jane@example.com',
  }),
});
console.log(await resp.json());

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.

POST /api/v2/questionnaire-responses/{response_id}/send/

Path Parameters

ParameterTypeDescription
response_iduuidID of the PREFILL response to send

Request Body

ParameterTypeRequiredDescription
emailstringConditionalEmail to deliver the workflow link to. If omitted, falls back to the email address stored on the entity.
expiry_hoursintegerNoHours 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 /responses/{id}/send/ POST /questionnaire-responses/{id}/send/ Yes
Credits are charged after the email is dispatched

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.