Questionnaire Responses

Questionnaire Responses represent filled-out DDQ questionnaires. They are the core workflow object in KYC Genie, linking a questionnaire template to a specific entity being assessed.

Response Object

The response object contains all metadata about a filled questionnaire.

Attributes

Field Type Description
iduuidUnique identifier
response_namestringDisplay name for the response
questionnaireobjectNested questionnaire template object (id, name, type)
subject_entityobjectNested entity being assessed (id, name, type)
statusstringCurrent workflow status (see Status Values below)
sent_bystringIdentifier of the user who sent the response
responded_byobjectProfile object of user who submitted the response (null until submitted)
reviewed_byobjectProfile object of analyst who reviewed the response (null until reviewed)
manager_reviewerobjectProfile object of manager who performed final review (null until manager review)
created_atdatetimeCreation timestamp
submitted_atdatetimeTimestamp of submission (null if not submitted)
reviewed_atdatetimeTimestamp of analyst review (null if not reviewed)
manager_reviewed_atdatetimeTimestamp of manager review (null if not manager reviewed)
updated_atdatetimeLast update timestamp
answersarrayArray of answer objects (only in detail view)

Status Values

Responses follow a state machine with specific allowed transitions:

StatusDescription
PREFILLYou're filling the form before sending to entity (no email sent)
DRAFTForm sent to entity for completion (email sent on creation)
SUBMITTEDEntity has submitted the completed form for your review
UNDER_REVIEWYour team is currently reviewing the submission
CHANGES_REQUESTEDYour team requested changes (returns to DRAFT status)
REVIEWEDInitial review complete, awaiting final approval
APPROVEDFinal approval granted
REJECTEDResponse rejected
RESUBMITTEDEntity resubmitted after changes were requested

Create Questionnaire Response

Creates a new DDQ response linking a questionnaire template to an entity. The response can only be created in DRAFT or PREFILL status.

Request Body

ParameterTypeRequiredDescription
questionnaire_idinteger (v1) / uuid (v2)YesID of the DDQ template. v1 accepts integer PK; v2 accepts UUID (integer still works for backwards compatibility)
entityobjectYes (v1)Entity to link or create. Pass entity_id inside to link existing, or full fields to create new. See Entities API.
entity_iduuidYes (v2)UUID of the pre-existing entity to link as the subject. Create the entity first via POST /entities/.
response_namestringNoCustom name for this response (auto-generated if not provided)
statusstringNoInitial status (default: PREFILL). Allowed values: PREFILL or DRAFT
webhook_urlstringNoOverride webhook URL for events on this response only
Status Validation

New responses can only be created with status DRAFT or PREFILL. Any other status value returns a 400 error. All other statuses are set through workflow transitions.

DRAFT Status Requires Entity Email

Creating a response with status: "DRAFT" sends an onboarding email to the entity. The linked entity must already have an email address stored - the API will return 400 if the entity has no email.

Create Response

curl -X POST https://api.kycgenie.com/api/v1/responses/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: resp-acme-ddq-20260114" \
  -d '{
    "questionnaire_id": 55,
    "entity": {
      "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426"
    },
    "status": "PREFILL"
  }'
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: resp-acme-ddq-20260114" \
  -d '{
    "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
    "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
    "status": "PREFILL"
  }'
import requests

resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Idempotency-Key": "resp-acme-ddq-20260114",
    },
    json={
        "questionnaire_id": 55,
        "entity": {"entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426"},
        "status": "PREFILL",
    },
)

data = resp.json()
print(f"Created response: {data['id']}")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

response = client.questionnaire_responses.create(
    questionnaire_id="7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
    entity_id="1b83f447-4a1e-4589-8a12-b8767e3c5426",
    status="PREFILL",
)

print(f"Created: {response.id}")        # UUID
print(f"Status:  {response.status}")    # PREFILL
const resp = await fetch('https://api.kycgenie.com/api/v1/responses/', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'resp-acme-ddq-20260114',
  },
  body: JSON.stringify({
    questionnaire_id: 55,
    entity: { entity_id: '1b83f447-4a1e-4589-8a12-b8767e3c5426' },
    status: 'PREFILL',
  }),
});

const data = await resp.json();
console.log(`Created: ${data.id}`);
const resp = await fetch('https://api.kycgenie.com/api/v2/questionnaire-responses/', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'resp-acme-ddq-20260114',
  },
  body: JSON.stringify({
    questionnaire_id: '7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a',
    entity_id: '1b83f447-4a1e-4589-8a12-b8767e3c5426',
    status: 'PREFILL',
  }),
});

const data = await resp.json();
console.log(`Created: ${data.id}`);

Response (201 Created)

{
  "id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "response_name": "Acme Corp - DDQ - Template",
  "questionnaire": {"id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a", "name": "DDQ - Template", "questionnaire_type": "ddq"},
  "subject_entity": {
    "id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
    "name": "Acme Corp",
    "entity_type": "company"
  },
  "status": "PREFILL",
  "created_at": "2026-01-14T12:41:56.456529Z",
  "submitted_at": null,
  "reviewed_at": null,
  "manager_reviewed_at": null,
  "updated_at": "2026-01-14T12:41:56.456555Z"
}

Batch Create Responses

Create multiple responses in a single request. Each item follows the same schema as the single create endpoint. Useful for bulk onboarding workflows.

Request Body

Array of response objects, each following the same schema as Create Response.

Batch Create Responses

curl -X POST https://api.kycgenie.com/api/v1/responses/batch/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: batch-onboard-20260114" \
  -d '[
    {
      "questionnaire_id": 55,
      "entity": {"entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426"},
      "status": "PREFILL"
    },
    {
      "questionnaire_id": 55,
      "entity": {"entity_id": "2c94g558-5b2f-5693-9b23-c9878f774437"},
      "status": "PREFILL"
    }
  ]'
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/batch/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: batch-onboard-20260114" \
  -d '{
    "responses": [
      {
        "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
        "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
        "status": "PREFILL"
      },
      {
        "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
        "entity_id": "2c94g558-5b2f-5693-9b23-c9878f774437",
        "status": "PREFILL"
      }
    ]
  }'
import requests

# v1: bare list payload with nested entity object
resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/batch/",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Idempotency-Key": "batch-onboard-20260114",
    },
    json=[
        {
            "questionnaire_id": 55,
            "entity": {"entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426"},
            "status": "PREFILL",
        },
        {
            "questionnaire_id": 55,
            "entity": {"entity_id": "2c94g558-5b2f-5693-9b23-c9878f774437"},
            "status": "PREFILL",
        },
    ],
)

data = resp.json()
print(f"Created: {data['created_count']}, Failed: {data['failed_count']}")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.batch_create(
    responses=[
        {
            "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
            "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
            "status": "PREFILL",
        },
        {
            "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
            "entity_id": "2c94g558-5b2f-5693-9b23-c9878f774437",
            "status": "PREFILL",
        },
    ]
)

print(f"Created: {result.created_count}, Failed: {result.failed_count}")
const resp = await fetch('https://api.kycgenie.com/api/v1/responses/batch/', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'batch-onboard-20260114',
  },
  body: JSON.stringify([
    { questionnaire_id: 55, entity: { entity_id: '1b83f447-4a1e-4589-8a12-b8767e3c5426' }, status: 'PREFILL' },
    { questionnaire_id: 55, entity: { entity_id: '2c94g558-5b2f-5693-9b23-c9878f774437' }, status: 'PREFILL' },
  ]),
});

const data = await resp.json();
console.log(`Created: ${data.created_count}, Failed: ${data.failed_count}`);
const resp = await fetch('https://api.kycgenie.com/api/v2/questionnaire-responses/batch/', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'batch-onboard-20260114',
  },
  body: JSON.stringify({
    responses: [
      { questionnaire_id: '7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a', entity_id: '1b83f447-4a1e-4589-8a12-b8767e3c5426', status: 'PREFILL' },
      { questionnaire_id: '7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a', entity_id: '2c94g558-5b2f-5693-9b23-c9878f774437', status: 'PREFILL' },
    ],
  }),
});

const data = await resp.json();
console.log(`Created: ${data.created_count}, Failed: ${data.failed_count}`);

Response (200 OK)

{
  "created_count": 2,
  "failed_count": 0,
  "results": [
    {"success": true, "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64", "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426", "entity_name": "Acme Corp"},
    {"success": true, "response_id": "e78288c0-7982-56c0-96g3-06b399885575", "entity_id": "2c94g558-5b2f-5693-9b23-c9878f774437", "entity_name": "Globex Ltd"}
  ]
}

List Questionnaire Responses

Retrieves a paginated list of all responses in your tenant, ordered by creation date (newest first).

Query Parameters

ParameterTypeDescription
statusstringFilter by status: DRAFT, PREFILL, SUBMITTED, etc.
entity_iduuidFilter by subject entity UUID
questionnaire_idinteger / uuidFilter by questionnaire (integer PK in v1; UUID in v2)
created_afterISO 8601Return responses created after this datetime
created_beforeISO 8601Return responses created before this datetime
submitted_afterISO 8601Return responses submitted after this datetime
pageintegerPage number (default: 1)
page_sizeintegerResults per page (default: 50, max: 100)
cursorstringOpaque cursor from next_cursor in the previous response

List Responses

curl "https://api.kycgenie.com/api/v1/responses/?status=SUBMITTED" \
  -H "Authorization: Bearer YOUR_API_KEY"
curl "https://api.kycgenie.com/api/v2/questionnaire-responses/?status=SUBMITTED" \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

resp = requests.get(
    "https://api.kycgenie.com/api/v1/responses/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"status": "SUBMITTED"},
)

data = resp.json()
for r in data["results"]:
    print(f"{r['id']}  {r['status']}  {r['response_name']}")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

page = client.questionnaire_responses.list(status="SUBMITTED")
for r in page.results:
    print(f"{r.id}  {r.status}  {r.response_name}")

# Next page
if page.has_more:
    page2 = client.questionnaire_responses.list(
        status="SUBMITTED",
        cursor=page.next_cursor,
    )
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/?status=SUBMITTED',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const data = await resp.json();
data.results.forEach(r => console.log(`${r.id}  ${r.status}  ${r.response_name}`));
const resp = await fetch(
  'https://api.kycgenie.com/api/v2/questionnaire-responses/?status=SUBMITTED',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const data = await resp.json();
data.results.forEach(r => console.log(`${r.id}  ${r.status}  ${r.response_name}`));

Response

{
  "count": 42,
  "next": "https://api.kycgenie.com/api/v1/responses/?page=2",
  "previous": null,
  "results": [
    {
      "id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
      "response_name": "Acme Corp - DDQ",
      "questionnaire_name": "DDQ - Template",
      "entity_name": "Acme Corp",
      "status": "SUBMITTED",
      "created_at": "2026-01-21T10:30:00Z",
      "submitted_at": "2026-01-21T14:23:45Z"
    }
  ]
}

Get Response

Retrieves a specific response by ID, including full metadata. The detail shape includes nested questionnaire and subject_entity objects with IDs, unlike the compact list shape.

Path Parameters

ParameterTypeDescription
response_iduuidThe unique identifier of the response

Get Response

curl https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.get(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

data = resp.json()
print(f"Status: {data['status']}")
print(f"Questionnaire: {data['questionnaire']['id']}")
print(f"Entity: {data['subject_entity']['name']}")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

response = client.questionnaire_responses.get_details(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)

print(f"Status: {response.status}")
print(f"Entity: {response.subject_entity.name}")
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(
  `https://api.kycgenie.com/api/v1/responses/${responseId}/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const data = await resp.json();
console.log(`Status: ${data.status}`);
console.log(`Entity: ${data.subject_entity.name}`);
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const data = await resp.json();
console.log(`Status: ${data.status}`);
console.log(`Questionnaire: ${data.questionnaire.id}`);
console.log(`Entity: ${data.subject_entity.name}`);

Response Example

{
  "id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "response_name": "Acme Corp - DDQ - Template",
  "questionnaire": {"id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a", "name": "DDQ - Template", "questionnaire_type": "ddq"},
  "subject_entity": {
    "id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
    "name": "Acme Corp",
    "entity_type": "company"
  },
  "status": "SUBMITTED",
  "created_at": "2026-01-21T10:30:00Z",
  "submitted_at": "2026-01-21T14:23:45Z",
  "reviewed_at": null,
  "manager_reviewed_at": null,
  "updated_at": "2026-01-21T14:23:45Z"
}

Get Questions

Returns every question in the questionnaire merged with its current answer state, grouped by section. This is the primary endpoint for PREFILL workflows - one call gives you every question_id needed to call PUT answers/, and clearly shows which required questions still need an answer.

v2 only

This endpoint is only available on v2. On v1, retrieve questions from GET /api/v1/questionnaires/{id}/ and answers separately from the response detail.

Response Fields

FieldTypeDescription
summaryobjectSame completion counts as GET answers/
sections[].namestringSection name (e.g. "Company Information")
sections[].sub_sections[].namestring | nullSub-section name, or null if not grouped
questions[].question_idintegerID to pass to PUT answers/
questions[].question_typestringDetermines which answer field to populate - see Answer Formats
questions[].optionsarray | nullValid values for select / multi_select questions
questions[].is_answeredbooleantrue if a non-empty answer exists
questions[].answer_textstring | nullCurrent answer, or null if unanswered
file_questions[].file_question_idintegerID to pass to PUT answers/{fq_id}/file/
file_questions[].is_satisfiedbooleantrue if files_uploaded ≥ min_files

Get Questions

curl https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/questions/ \
  -H "Authorization: Bearer YOUR_API_KEY"
# v2 only - not available on v1
# Use GET /api/v1/questionnaires/{id}/ for questions,
# and GET /api/v1/responses/{id}/ for answers.
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

form = client.questionnaire_responses.get_questions(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)

print(f"Progress: {form.summary.answered}/{form.summary.total_questions}")
print(f"Required missing: {form.summary.required_unanswered} text, {form.summary.required_files_unanswered} files")

# Build answers for all unanswered required questions
answers = []
for section in form.sections:
    for sub in section.sub_sections:
        for q in sub.questions:
            if not q.is_answered and q.required:
                answers.append({
                    "question_id": q.question_id,
                    "answer_text": f"Answer to: {q.question_text}",
                })

client.questionnaire_responses.update_answers(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
    answers=answers,
)
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.get(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/questions/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

form = resp.json()
print(f"Progress: {form['summary']['answered']}/{form['summary']['total_questions']}")

for section in form["sections"]:
    for sub in section["sub_sections"]:
        for q in sub["questions"]:
            status = "✓" if q["is_answered"] else ("✗ required" if q["required"] else "–")
            print(f"  [{status}] Q{q['question_number']}: {q['question_text'][:60]}")
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/questions/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const { summary, sections } = await resp.json();
console.log(`Progress: ${summary.answered}/${summary.total_questions}`);
console.log(`Required missing: ${summary.required_unanswered} text, ${summary.required_files_unanswered} files`);

// Collect unanswered required questions
const missing = sections
  .flatMap(s => s.sub_sections)
  .flatMap(sub => sub.questions)
  .filter(q => !q.is_answered && q.required);

console.log(`Unanswered required questions:`, missing.map(q => q.question_id));
// v2 only - not available on v1

Response Example

{
  "summary": {
    "total_questions": 83,
    "answered": 45,
    "required_unanswered": 12,
    "total_file_questions": 5,
    "files_uploaded": 3,
    "required_files_unanswered": 2
  },
  "sections": [
    {
      "name": "Security",
      "sub_sections": [
        {
          "name": "Data Protection",
          "questions": [
            {
              "question_id": 42,
              "question_text": "Describe your encryption at rest.",
              "question_number": 1,
              "question_type": "text",
              "required": true,
              "options": null,
              "exclude_from_autofill": false,
              "answer_text": "AES-256 encryption.",
              "date_answer": null,
              "is_answered": true,
              "is_flagged": false
            },
            {
              "question_id": 43,
              "question_text": "Do you conduct annual penetration tests?",
              "question_number": 2,
              "question_type": "boolean",
              "required": true,
              "options": null,
              "exclude_from_autofill": false,
              "answer_text": null,
              "date_answer": null,
              "is_answered": false,
              "is_flagged": false
            }
          ],
          "file_questions": [
            {
              "file_question_id": 67,
              "description": "Upload latest penetration test report",
              "required": true,
              "min_files": 1,
              "max_files": 1,
              "accepted_extensions": ["pdf"],
              "files_uploaded": 0,
              "is_satisfied": false,
              "attachments": []
            }
          ]
        }
      ]
    }
  ]
}

Get Response Answers

Retrieve all submitted answers for a response, including a completion summary, text answers, and file answers. Text and file answers are returned in separate arrays since they have different fields and different save paths.

Answer Object Fields

FieldTypeDescription
iduuidUnique answer identifier
question_idintegerID of the question being answered
question_textstringFull text of the question
question_numberintegerQuestion number within questionnaire
question_typestringOne of: text, email, tel, url, number, percentage, currency, boolean, yes_no_na, select, multi_select, country, date, text_and_date, address, structured. See Answer Formats.
sectionstringSection name (e.g., "Company Information")
sub_sectionstringSub-section name (if applicable)
requiredbooleanWhether the question is required
answer_textstringThe text answer provided
date_answerdateDate answer for date-type questions
is_flaggedbooleanWhether this answer has been flagged for review
commentsarrayArray of comment objects on this answer
num_of_commentsintegerCount of comments

Get Response Answers

curl https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/answers/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/answers/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.get(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/answers/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

for answer in resp.json()["results"]:
    print(f"Q{answer['question_number']}: {answer['answer_text']}")
    if answer["is_flagged"]:
        print("  ^⚠ flagged")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.get_answers(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)

print(f"Answered: {result.summary.answered}/{result.summary.total_questions}")
print(f"Required unanswered: {result.summary.required_unanswered}")
print(f"Files uploaded: {result.summary.files_uploaded}/{result.summary.total_file_questions} ({result.summary.required_files_unanswered} required missing)")

for answer in result.text_answers:
    print(f"Q{answer.question_number}: {answer.answer_text}")
    if answer.is_flagged:
        print("  ^⚠ flagged")
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(
  `https://api.kycgenie.com/api/v1/responses/${responseId}/answers/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const { results } = await resp.json();
results.forEach(a => {
  console.log(`Q${a.question_number}: ${a.answer_text}`);
  if (a.is_flagged) console.log('  ^ flagged');
});
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

const { summary, text_answers, file_answers } = await resp.json();
console.log(`Answered: ${summary.answered}/${summary.total_questions}`);
console.log(`Required unanswered: ${summary.required_unanswered}`);
text_answers.forEach(a => {
  console.log(`Q${a.question_number}: ${a.answer_text}`);
  if (a.is_flagged) console.log('  ^ flagged');
});

Response Example

{
  "summary": {
    "total_questions": 83,
    "answered": 45,
    "required_unanswered": 12,
    "total_file_questions": 5,
    "files_uploaded": 3,
    "required_files_unanswered": 2
  },
  "text_answers": [
    {
      "id": "aa11bb22-cc33-44dd-88ee-ff0011223344",
      "question_id": 42,
      "question_text": "Describe your encryption at rest.",
      "question_number": 1,
      "question_type": "text",
      "section": "Security",
      "sub_section": null,
      "required": true,
      "answer_text": "AES-256 encryption at rest and TLS 1.3 in transit.",
      "date_answer": null,
      "is_flagged": false,
      "comments": [],
      "num_of_comments": 0,
      "created_at": "2026-01-21T14:10:00Z",
      "updated_at": "2026-01-21T14:10:00Z"
    }
  ],
  "file_answers": []
}

Answer Format by Question Type

Each question has a question_type that determines which field(s) to populate in an answer object and what value format is expected. The answer object is submitted inside the answers array when calling the Save Answers endpoints.

Tip

Always check the question_type on each question returned by Get Response Answers before constructing your answer payload. The options array (present on select and multi_select questions) lists the exact strings that are valid values.

Question TypeField to useExpected formatExample value
textanswer_textAny string (max 2000 chars)"Acme Corporation Ltd"
emailanswer_textValid email address"jane@example.com"
telanswer_textPhone number string"+44 7700 900123"
urlanswer_textURL string"https://example.com"
numberanswer_textNumeric string"42"
percentageanswer_textPercentage as a numeric string (without % symbol)"25.5"
currencyanswer_textPipe-separated amount and ISO 4217 currency code: "amount|CODE""1000000|USD"
booleananswer_text"yes" or "no""yes"
yes_no_naanswer_text"yes", "no", or "na""na"
selectanswer_textExactly one option string from the question's options list"Sole Trader"
multi_selectanswer_textJSON array or JSON array string of one or more option strings from the question's options list["Option A","Option B"]
countryanswer_textISO 3166-1 alpha-2 code, ISO-3 code, or full country/nationality name - normalized to ISO-2 on save"IE" or "Ireland"
datedate_answerISO 8601 date string: YYYY-MM-DD"1990-06-15"
text_and_dateanswer_text + date_answerFree-text string in answer_text; date in date_answer (YYYY-MM-DD)See example below
addressanswer_textObject (or JSON string) with line1, line2 (optional), city, state (optional), postcode, country (ISO-2 or full name)See example below
structuredstructured_dataJSON array of instance objects, one per row of the structured templateSee example below

Save Answers

Saves text answers and file uploads to a response without submitting. Use this to build up answers incrementally without triggering validation or autoflag.

PUT /api/v1/responses/{response_id}/save/

Request Body (multipart/form-data)

ParameterTypeRequiredDescription
answersstring (JSON)NoJSON-encoded array of answer objects: [{"question_id": 123, "answer_text": "..."}]
{file_question_id}fileNoFiles keyed by file question integer ID (e.g., field name 67 → file binary)

Answer Object Fields

FieldTypeDescription
question_idintegerID of the question being answered
answer_textstringThe text answer
date_answerdateOptional: date answer (YYYY-MM-DD)

v2 replaces the single /save/ endpoint with two dedicated endpoints - one for text answers and one per file question. Both return immediately without submitting; call POST submit/ when ready.

Save Text Answers

PUT /api/v2/questionnaire-responses/{response_id}/answers/

Accepts a bare JSON array of answer objects. Answers are upserted by question_id.

Request Body (application/json)

FieldTypeRequiredDescription
question_idintegerYesID of the question being answered
answer_textstringNoText answer
date_answerdateNoDate answer (YYYY-MM-DD) - for date / text_and_date questions
structured_dataobjectNoKey-value object for structured / address questions

Upload File Answer

PUT /api/v2/questionnaire-responses/{response_id}/answers/{fq_id}/file/

Uploads a file for a specific file question. fq_id is the integer ID of the FileQuestion. Document type is resolved automatically from the question config - callers cannot override it.

Request Body (multipart/form-data)

FieldTypeRequiredDescription
filefileYesThe file binary (max 10 MB; PDF, JPEG, PNG, DOCX, XLSX, CSV accepted)

Save Answers

curl -X PUT https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/save/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: save-acme-ddq-20260114" \
  -F 'answers=[{"question_id": 42, "answer_text": "AES-256 encryption at rest"},{"question_id": 43, "answer_text": "Annual penetration testing"}]' \
  -F "67=@audit_report.pdf"
# Step 1: save text answers (bare JSON array)
curl -X PUT https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/answers/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[{"question_id": 42, "answer_text": "AES-256 encryption at rest"},{"question_id": 43, "answer_text": "Annual penetration testing"}]'

# Step 2: upload file for file question 67
curl -X PUT https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/answers/67/file/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@audit_report.pdf"
import json
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

# v1: single multipart call for both text and files
resp = requests.put(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/save/",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Idempotency-Key": "save-acme-ddq-20260114",
    },
    data={"answers": json.dumps([
        {"question_id": 42, "answer_text": "AES-256 encryption at rest"},
        {"question_id": 43, "answer_text": "Annual penetration testing"},
    ])},
    files={"67": open("audit_report.pdf", "rb")},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

# Step 1: save text answers
result = client.questionnaire_responses.update_answers(
    response_id=response_id,
    answers=[
        {"question_id": 42, "answer_text": "AES-256 encryption at rest"},
        {"question_id": 43, "answer_text": "Annual penetration testing"},
    ],
)
print(f"Saved: {result.answers_saved} answers")

# Step 2: upload a file for file question 67
with open("audit_report.pdf", "rb") as f:
    file_answer = client.questionnaire_responses.upload_file_answer(
        response_id=response_id,
        fq_id=67,
        file=("audit_report.pdf", f, "application/pdf"),
    )
print(f"Attachment ID: {file_answer.attachment_id}")
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';
const formData = new FormData();
formData.append('answers', JSON.stringify([
  { question_id: 42, answer_text: 'AES-256 encryption at rest' },
  { question_id: 43, answer_text: 'Annual penetration testing' },
]));
formData.append('67', fileInput.files[0]);

const resp = await fetch(`https://api.kycgenie.com/api/v1/responses/${responseId}/save/`, {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Idempotency-Key': 'save-acme-ddq-20260114',
  },
  body: formData,
});
console.log(await resp.json());
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

// Step 1: save text answers (bare JSON array)
await fetch(`https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/`, {
  method: 'PUT',
  headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify([
    { question_id: 42, answer_text: 'AES-256 encryption at rest' },
    { question_id: 43, answer_text: 'Annual penetration testing' },
  ]),
});

// Step 2: upload file for file question 67
const formData = new FormData();
formData.append('file', fileInput.files[0]);
await fetch(`https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/67/file/`, {
  method: 'PUT',
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
  body: formData,
});

Response Example (200 OK)

{
  "success": true,
  "message": "Response progress saved",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "status": "DRAFT",
  "answers_saved": 2,
  "files_uploaded": 1,
  "uploaded_files": [
    {"file_question_id": 67, "filename": "audit_report.pdf", "document_type": "Audit Report"}
  ]
}

Send Response

Transitions a PREFILL response to DRAFT and sends an onboarding email to the subject entity. Creates an OnboardingSession with a unique time-limited access token.

Requirements

  • Response status must be PREFILL - DRAFT responses have already been sent
  • An email address must be available (entity on record, or supplied in request body)
  • Consumes send_ddq credits from your plan

Request Body (optional, application/json)

FieldTypeRequiredDescription
emailstringNoOverride email address. Falls back to the entity's email on record. Returns 400 if neither is available.
expiry_hoursintegerNoLink expiry in hours. Defaults to tenant setting (typically 48).

What Happens

  • Response transitions from PREFILLDRAFT
  • Any existing AI autofill annotations are cleared from answers
  • Onboarding email sent to entity with a unique time-limited link
  • If a pending_verification_config was set on the response, an identity verification session is also created and bundled into the onboarding flow
  • Credits charged only after email dispatches successfully - if email fails the status reverts to PREFILL
With identity verification

If a pending_verification_config was set on the response, steps will be ["ddq", "verification"] and verification_session_id will be populated with the session ID.

Send Response

# Minimal - uses entity's email on record
curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/ \
  -H "Authorization: Bearer YOUR_API_KEY"

# With email override and custom expiry
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": "compliance@acmecorp.com", "expiry_hours": 72}'
# Minimal - uses entity's email on record
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/ \
  -H "Authorization: Bearer YOUR_API_KEY"

# With email override and custom expiry
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "compliance@acmecorp.com", "expiry_hours": 72}'
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"},
    # Optional - omit to use entity's email on record
    json={"email": "compliance@acmecorp.com", "expiry_hours": 72},
)

data = resp.json()
print(f"Session: {data['session_id']}")
print(f"Expires: {data['expires_at']}")
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.send(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
    # Optional - omit to use entity's email on record
    email="compliance@acmecorp.com",
    expiry_hours=72,
)

print(f"Session:  {result.session_id}")
print(f"Steps:    {result.steps}")        # e.g. ['ddq'] or ['ddq', 'verification']
print(f"Expires:  {result.expires_at}")
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
    // Optional body - omit to use entity's email on record
    body: JSON.stringify({ email: 'compliance@acmecorp.com', expiry_hours: 72 }),
  }
);
console.log(await resp.json());
const resp = await fetch(
  'https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/send/',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: 'compliance@acmecorp.com', expiry_hours: 72 }),
  }
);
console.log(await resp.json());

Response Example (200 OK)

{
  "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "sent",
  "email_sent": true,
  "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
  "entity_name": "Acme Corp",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "verification_session_id": null,
  "steps": ["ddq"],
  "expires_at": "2026-01-16T12:41:56.456529Z"
}

Submit Response

Submits a completed response for review. Transitions from DRAFT, PREFILL, CHANGES_REQUESTED, or RESUBMITTED to SUBMITTED status. All answers must be saved before calling this endpoint.

Validation Required

All required questions must have answers before submission. The API returns 400 with details about missing answers if validation fails.

Autoflag webhooks

response.autoflag_started and response.autoflag_completed are fired for all submissions - both API-driven and guest portal submissions.

In test mode, the autoflag pipeline is simulated - response.autoflag_completed webhooks are delivered with "simulated": true and no AI credits are consumed.

Submit Response

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/submit/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/submit/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/submit/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)

print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.submit(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)

print(f"Status: {result.status}")  # SUBMITTED
print(f"Task:   {result.autoflag_task_id}")
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/submit/',
  { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);

console.log(await resp.json());

Response Example

{
  "success": true,
  "message": "Response submitted successfully",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "status": "SUBMITTED",
  "submitted_at": "2026-01-21T14:23:45Z",
  "autoflag_task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Approve Response

Approve a submitted response, moving it to APPROVED status. This is the correct way to drive approval via API - not via the response notes endpoint.

Requirements

  • Response must be in SUBMITTED, UNDER_REVIEW, REVIEWED, or RESUBMITTED status
  • API key must belong to your reviewer tenant, not the subject entity

What Happens

  • response.status changes to APPROVED
  • Action is logged in the audit trail
  • Webhook response.status_changed is fired (if configured)
  • If notes is provided, a ResponseNote is automatically created and tagged with status_transition: "APPROVED"

Request Body

FieldTypeRequiredDescription
notesstringNoOptional approval notes. If provided, a response note is automatically created and tagged with APPROVED.

Error Responses

StatusDescription
400Response is not in an approvable status
404Response not found

Approve Response

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/approve/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes": "All checks passed. No adverse screening results."}'
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/approve/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes": "All checks passed. No adverse screening results."}'
import requests

resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/approve/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"notes": "All checks passed. No adverse screening results."},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.approve(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
    notes="All checks passed. No adverse screening results.",
)
print(f"Status: {result.new_status}")  # APPROVED
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/approve/',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({ notes: 'All checks passed. No adverse screening results.' }),
  }
);
console.log(await resp.json());

Response Example

{
  "status": "success",
  "message": "Response approved successfully",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "old_status": "SUBMITTED",
  "new_status": "APPROVED",
  "notes": "All checks passed. No adverse screening results."
}

Reject Response

Reject a submitted response, moving it to REJECTED status.

Requirements

  • Response must be in SUBMITTED, UNDER_REVIEW, REVIEWED, or RESUBMITTED status

What Happens

  • response.status changes to REJECTED
  • Action is logged in the audit trail
  • Webhook response.status_changed is fired (if configured)
  • If reason is provided, a ResponseNote is automatically created and tagged with status_transition: "REJECTED"

Request Body

FieldTypeRequiredDescription
reasonstringNoReason for rejection. Recommended for audit trail. If provided, a response note is automatically created.

Error Responses

StatusDescription
400Response is not in a rejectable status
404Response not found

Reject Response

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/reject/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Missing UBO documentation. Beneficial ownership chain unverifiable."}'
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/reject/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Missing UBO documentation. Beneficial ownership chain unverifiable."}'
import requests

resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/reject/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"reason": "Missing UBO documentation. Beneficial ownership chain unverifiable."},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.reject(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
    reason="Missing UBO documentation. Beneficial ownership chain unverifiable.",
)
print(f"Status: {result.new_status}")  # REJECTED
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/reject/',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({ reason: 'Missing UBO documentation. Beneficial ownership chain unverifiable.' }),
  }
);
console.log(await resp.json());

Response Example

{
  "status": "success",
  "message": "Response rejected successfully",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "old_status": "SUBMITTED",
  "new_status": "REJECTED",
  "reason": "Missing UBO documentation. Beneficial ownership chain unverifiable."
}

Request Changes

Send a response back to the submitter requesting corrections or additional information. Moves the response to CHANGES_REQUESTED status - the submitter can then update answers and resubmit, which moves the response to RESUBMITTED.

Requirements

  • Response must be in SUBMITTED, UNDER_REVIEW, REVIEWED, or RESUBMITTED status

What Happens

  • response.status changes to CHANGES_REQUESTED
  • Submitter receives an email notification (if email notifications are configured)
  • Submitter can update answers and resubmit - response then moves to RESUBMITTED
  • Action is logged in the audit trail
  • If changes_needed is provided, a ResponseNote is automatically created and tagged with status_transition: "CHANGES_REQUESTED"

Request Body

FieldTypeRequiredDescription
changes_neededstringNoDescription of the required changes. Strongly recommended - this is included in the notification email to the submitter and logged as a note.

Error Responses

StatusDescription
400Response is not in a valid status for this action
404Response not found

Request Changes

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/request-changes/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"changes_needed": "Please provide the latest audited financial statements and shareholder register."}'
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/request-changes/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"changes_needed": "Please provide the latest audited financial statements and shareholder register."}'
import requests

resp = requests.post(
    "https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/request-changes/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"changes_needed": "Please provide the latest audited financial statements and shareholder register."},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.request_changes(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
    changes_needed="Please provide the latest audited financial statements and shareholder register.",
)
print(f"Status: {result.new_status}")  # CHANGES_REQUESTED
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/request-changes/',
  {
    method: 'POST',
    headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({ changes_needed: 'Please provide the latest audited financial statements and shareholder register.' }),
  }
);
console.log(await resp.json());

Response Example

{
  "status": "success",
  "message": "Changes requested successfully",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
  "old_status": "SUBMITTED",
  "new_status": "CHANGES_REQUESTED",
  "changes_needed": "Please provide the latest audited financial statements and shareholder register."
}

Delete Response

Permanently deletes a response. This action cannot be undone.

Permanent Action

Deleting a response removes all associated answers and file uploads. Consider archiving instead if you need to maintain records.

API Response

Returns 204 No Content on success. Some API clients may receive 200 OK - treat both as success.

Delete Response

curl -X DELETE https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl -X DELETE https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.delete(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.status_code)  # 204
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

client.questionnaire_responses.delete(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)
# Returns None on success (204 No Content)
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/',
  { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(resp.status);  // 204

Get Response Entities

Returns all entities associated with a response's subject entity, traversing the relationship tree and grouping by type. Use this to gather all entity IDs before running batch screening.

Get Response Entities

curl https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/entities/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/entities/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.get(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/entities/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

entities = client.questionnaire_responses.get_entities(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)
print(f"Total entities: {entities.total_count}")
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/entities/',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Response Example

{
  "subject_entity_id": "550e8400-e29b-41d4-a716-446655440000",
  "subject_entity_name": "Acme Corporation",
  "subject_entity_type": "company",
  "companies": [{"id": "660f9511-f30c-23e5-b827-537766551111", "name": "Acme Subsidiary Ltd", "type": "company"}],
  "individuals": [{"id": "770e0622-e41d-34f6-c938-648877662222", "name": "John Smith", "type": "individual"}],
  "unlinked_entities": [],
  "total_count": 2
}

Regenerate Submit Token

Generates a fresh guest-access token for the response's onboarding session, invalidating the previous token. Requires the response to have been sent first via POST /responses/{id}/send/ - returns 404 if no active session exists.

onboarding_url

The SDK model (SubmitToken) exposes onboarding_url as the canonical field. The raw API returns both onboarding_url and submit_url (identical values) for backward compatibility, but all new code should use onboarding_url.

Error Responses

  • 404: No active onboarding session - call POST /responses/{id}/send/ first

Regenerate Submit Token

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/token/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/token/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

# v1
resp = requests.post(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/token/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = resp.json()
print(data["onboarding_url"])
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

token_data = client.questionnaire_responses.generate_token(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)
print(f"Onboarding URL: {token_data.onboarding_url}")
print(f"Expires at:     {token_data.expires_at}")
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/token/',
  { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const data = await resp.json();
console.log(data.onboarding_url);
const resp = await fetch(
  'https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/token/',
  { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const data = await resp.json();
console.log(data.onboarding_url);

Response Example

{
  "token": "AbC123XyZ789...",
  "expires_at": "2026-03-14T12:00:00+00:00",
  "onboarding_url": "https://kycgenie.com/onboard/f47ac10b-58cc-4372-a567-0e02b2c3d479?token=AbC123XyZ789...",
  "submit_url": "https://kycgenie.com/onboard/f47ac10b-58cc-4372-a567-0e02b2c3d479?token=AbC123XyZ789...",
  "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
{
  "token": "AbC123XyZ789...",
  "expires_at": "2026-03-14T12:00:00+00:00",
  "onboarding_url": "https://kycgenie.com/onboard/f47ac10b-58cc-4372-a567-0e02b2c3d479?token=AbC123XyZ789...",
  "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

Autofill Response

Trigger AI-powered autofill for an entire response using documents linked to the subject entity. This is an asynchronous operation that returns immediately with a task ID for tracking.

Requirements

  • Response status must be PREFILL or DRAFT
  • Subject entity must have at least one document uploaded
  • Consumes AI autofill credits from your plan

Behavior

  • Asynchronous: Returns task_id immediately, processing happens in background
  • Overwrites: Replaces existing answers (same as web UI behavior)
  • Webhook: Sends response.autofill_completed event when finished

Autofill Response

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/autofill/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/autofill/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.post(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/autofill/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.autofill(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)
print(f"Task ID: {result.task_id}")
print(f"Status:  {result.status}")  # PREFILL or DRAFT
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/autofill/',
  { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Response (202 Accepted)

{
  "task_id": "abc123-def456-789ghi",
  "response_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "DRAFT",
  "message": "Autofill task started successfully"
}

Webhook Payload

{
  "event": "response.autofill_completed",
  "timestamp": "2026-01-28T13:21:55.853932+00:00",
  "data": {
    "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64",
    "response_name": "Acme Corp - DDQ - Template",
    "questionnaire_id": "7f1a3c4e-5b9d-4e82-a1f3-8d2c5e7b9f0a",
    "questionnaire_name": "DDQ - Template",
    "entity_id": "1b83f447-4a1e-4589-8a12-b8767e3c5426",
    "entity_name": "Acme Corp",
    "status": "PREFILL",
    "created_at": "2026-01-28T12:41:56.456529+00:00",
    "submitted_at": null,
    "answers_saved": 3,
    "total_questions": 83,
    "answers": [
      {"question_id": "45", "question_text": "Legal name of the entity", "answer": "Acme Corporation Ltd"},
      {"question_id": "46", "question_text": "Number of employees", "answer": "250"},
      {"question_id": "47", "question_text": "List your directors", "answer": "2 instance(s)"}
    ]
  }
}

Generate Assessment Report

Generate a comprehensive AI-powered risk assessment report in DOCX format. The report includes entity details, screening results, answer analysis, and risk scoring based on all available data.

Requirements

  • Response must be SUBMITTED, UNDER_REVIEW, REVIEWED, APPROVED, or RESUBMITTED
  • Subject entity must exist
  • Consumes AI assessment report credits from your plan

Behavior

  • Asynchronous: Returns task_id immediately, report generation happens in background
  • Storage: DOCX file linked to entity
  • AI Analysis: Comprehensive risk assessment using all response data
  • Includes: Entity details, screening results, flagged answers, risk sections, compliance summary

Error Responses

  • 400: Response not submitted yet
  • 404: Response or subject entity not found
  • 500: Failed to queue assessment task

Generate Assessment Report

curl -X POST https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/assessment/generate/ \
  -H "Authorization: Bearer YOUR_API_KEY"
curl -X POST https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/assessment/generate/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.post(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/assessment/generate/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.generate_assessment_report(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)
print(f"Task ID: {result.task_id}")
print(f"Status:  {result.status}")  # pending
const resp = await fetch(
  'https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/assessment/generate/',
  { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Response (202 Accepted)

{
  "status": "pending",
  "message": "Assessment report generation initiated",
  "task_id": "abc123-def456-789ghi",
  "response_id": "d67177bf-6871-45bf-85f2-95a1e52ccc64"
}

Download Assessment Report

Download the most recently generated assessment report for a response as a DOCX file.

Requirements

  • Assessment report must have been generated first using the generate endpoint
  • Report must exist in storage

Error Responses

  • 404: No assessment report found - generate one first using POST endpoint
  • 500: Failed to download report from storage

Download Assessment Report

curl -L https://api.kycgenie.com/api/v1/responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/assessment/download/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -o assessment_report.docx
curl -L https://api.kycgenie.com/api/v2/questionnaire-responses/d67177bf-6871-45bf-85f2-95a1e52ccc64/assessment/download/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -o assessment_report.docx
import requests

response_id = "d67177bf-6871-45bf-85f2-95a1e52ccc64"

resp = requests.get(
    f"https://api.kycgenie.com/api/v1/responses/{response_id}/assessment/download/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    stream=True,
)

with open("assessment_report.docx", "wb") as fh:
    for chunk in resp.iter_content(chunk_size=8192):
        fh.write(chunk)
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

content = client.questionnaire_responses.download_assessment(
    response_id="d67177bf-6871-45bf-85f2-95a1e52ccc64",
)

with open("assessment_report.docx", "wb") as fh:
    fh.write(content)
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(`https://api.kycgenie.com/api/v1/responses/${responseId}/assessment/download/`, {
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});

const blob = await resp.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'assessment_report.docx';
link.click();
const responseId = 'd67177bf-6871-45bf-85f2-95a1e52ccc64';

const resp = await fetch(`https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/assessment/download/`, {
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
});

const blob = await resp.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'assessment_report.docx';
link.click();

Response Headers

  • Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
  • Content-Disposition: attachment; filename="assessment_report_{response_id}.docx"

File Answers

List, retrieve, download, and delete file attachments on a response. To upload a file to a slot-based file question, use PUT /answers/{fq_id}/file/ documented in the Save Answers section.

File Answer Flow

Use list to inspect all uploaded file answers, get to fetch a single attachment's metadata, download to get a time-limited URL, and delete to remove an attachment no longer needed.

Supported Actions

  • List all file answers on a response
  • Get a single attachment's metadata
  • Generate a time-limited download URL
  • Delete an attachment when it is no longer needed

List File Answers

Retrieve all file attachments on a response, including document metadata and file question linkage.

GET/api/v2/questionnaire-responses/{response_id}/answers/files/

List File Answers

curl https://api.kycgenie.com/api/v2/questionnaire-responses/770g0611-g40d-63f6-c938-668877662222/answers/files/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "770g0611-g40d-63f6-c938-668877662222"

resp = requests.get(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/answers/files/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

files = client.questionnaire_responses.list_file_answers(
    response_id="770g0611-g40d-63f6-c938-668877662222",
)
for f in files.data:
    print(f"{f.document.name} - {f.file_question_type}")
const responseId = '770g0611-g40d-63f6-c938-668877662222';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/files/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Response Example

{
  "count": 1,
  "data": [
    {
      "attachment_id": "abb6336c-7edc-4814-92eb-4a80dc5b17fd",
      "response_id": "770g0611-g40d-63f6-c938-668877662222",
      "document": {
        "id": "880g1733-h50e-74g7-d049-779988773333",
        "name": "bank_statement_jan_2026.pdf",
        "size": 524288,
        "content_type": "application/pdf",
        "document_type_name": "Bank Statement",
        "classification": "entity_private",
        "uploaded_by_email": "api.user@example.com",
        "created_at": "2026-01-30T15:10:22Z",
        "expiry_date": null
      },
      "file_question_id": 67,
      "file_question_type": "Bank Statement",
      "file_question_description": "Upload your most recent bank statement (last 3 months)",
      "file_question_required": true,
      "file_question_multiple": false,
      "is_flagged": false,
      "is_submitted": false,
      "created_at": "2026-01-30T15:10:22Z",
      "updated_at": "2026-01-30T15:10:22Z"
    }
  ]
}

Get File Answer

Retrieve metadata for a single file attachment.

GET/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/

Path Parameters

ParameterTypeDescription
response_idUUIDThe response's unique identifier
attachment_idUUIDThe attachment_id from the list endpoint

Get File Answer

curl https://api.kycgenie.com/api/v2/questionnaire-responses/770g0611-g40d-63f6-c938-668877662222/answers/files/abb6336c-7edc-4814-92eb-4a80dc5b17fd/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "770g0611-g40d-63f6-c938-668877662222"
attachment_id = "abb6336c-7edc-4814-92eb-4a80dc5b17fd"

resp = requests.get(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

fa = client.questionnaire_responses.get_file_answer(
    response_id="770g0611-g40d-63f6-c938-668877662222",
    attachment_id="abb6336c-7edc-4814-92eb-4a80dc5b17fd",
)
print(f"{fa.document.name} - question: {fa.file_question_type}")
const resp = await fetch(
  'https://api.kycgenie.com/api/v2/questionnaire-responses/770g0611-g40d-63f6-c938-668877662222/answers/files/abb6336c-7edc-4814-92eb-4a80dc5b17fd/',
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Download File Answer

Generate a short-lived pre-signed URL to download a file attachment.

GET/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/download/
Download URL

Returns a time-limited URL valid for 15 minutes

Path Parameters

ParameterTypeDescription
response_idUUIDThe response's unique identifier
attachment_idUUIDThe attachment_id from the list endpoint (not document.id)

Download File Answer

curl https://api.kycgenie.com/api/v2/questionnaire-responses/770g0611-g40d-63f6-c938-668877662222/answers/files/abb6336c-7edc-4814-92eb-4a80dc5b17fd/download/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "770g0611-g40d-63f6-c938-668877662222"
attachment_id = "abb6336c-7edc-4814-92eb-4a80dc5b17fd"

resp = requests.get(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/download/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.json())
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

result = client.questionnaire_responses.download_file(
    response_id="770g0611-g40d-63f6-c938-668877662222",
    attachment_id="abb6336c-7edc-4814-92eb-4a80dc5b17fd",
)
print(f"Download URL: {result.download_url}")
print(f"Expires at:   {result.expires_at}")
const responseId = '770g0611-g40d-63f6-c938-668877662222';
const attachmentId = 'abb6336c-7edc-4814-92eb-4a80dc5b17fd';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/files/${attachmentId}/download/`,
  { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(await resp.json());

Response Example

{
  "download_url": "https://app.kycgenie.com/api/v1/files/dl_C2e4f6a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4/",
  "expires_at": "2026-02-20T15:30:00+00:00",
  "attachment_id": "abb6336c-7edc-4814-92eb-4a80dc5b17fd",
  "filename": "bank_statement_jan_2026.pdf",
  "content_type": "application/pdf",
  "size": 524288
}

Delete File Answer

DELETE/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/

Response (204 No Content)

No response body on success.

Delete File Answer

curl -X DELETE https://api.kycgenie.com/api/v2/questionnaire-responses/770g0611-g40d-63f6-c938-668877662222/answers/files/abb6336c-7edc-4814-92eb-4a80dc5b17fd/ \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response_id = "770g0611-g40d-63f6-c938-668877662222"
attachment_id = "abb6336c-7edc-4814-92eb-4a80dc5b17fd"

resp = requests.delete(
    f"https://api.kycgenie.com/api/v2/questionnaire-responses/{response_id}/answers/files/{attachment_id}/",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
print(resp.status_code)  # 204
from kycgenie import KYCGenie

client = KYCGenie(api_key="YOUR_API_KEY")

client.questionnaire_responses.delete_file_answer(
    response_id="770g0611-g40d-63f6-c938-668877662222",
    attachment_id="abb6336c-7edc-4814-92eb-4a80dc5b17fd",
)
# Returns None on success (204 No Content)
const responseId = '770g0611-g40d-63f6-c938-668877662222';
const attachmentId = 'abb6336c-7edc-4814-92eb-4a80dc5b17fd';

const resp = await fetch(
  `https://api.kycgenie.com/api/v2/questionnaire-responses/${responseId}/answers/files/${attachmentId}/`,
  { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
console.log(resp.status);  // 204