> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Workflow

> Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` — any other key (including `workflow_type`) is rejected with a 400. Not idempotent — each call creates a new workflow. Maximum 50 workflows per application.

export const AgentPromptAccordion = ({prompt, title = "AI Agent Integration Prompt"}) => {
  const [copied, setCopied] = React.useState(false);
  const handleCopy = e => {
    e.stopPropagation();
    if (!prompt) return;
    navigator.clipboard.writeText(prompt.trim()).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };
  const agents = ["Claude Code", "Codex", "Cursor", "Devin", "Windsurf", "GitHub Copilot"];
  return <div className="didit-agent-card">
      {}
      <div className="didit-agent-titlebar">
        <div className="didit-agent-dots" aria-hidden="true">
          <span className="didit-agent-dot didit-agent-dot-red"></span>
          <span className="didit-agent-dot didit-agent-dot-yellow"></span>
          <span className="didit-agent-dot didit-agent-dot-green"></span>
        </div>
        <span className="didit-agent-filename">{title}</span>
        <button type="button" className={`didit-agent-copy ${copied ? "didit-agent-copy-copied" : ""}`} onClick={handleCopy} title="Copy prompt to clipboard" aria-label={copied ? "Copied!" : "Copy prompt to clipboard"}>
          {copied ? <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Copied</span>
            </> : <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
              </svg>
              <span>Copy</span>
            </>}
        </button>
      </div>

      {}
      <pre className="didit-agent-body"><code>{prompt.trim()}</code></pre>

      {}
      <div className="didit-agent-footer">
        <span className="didit-agent-footer-label">Paste into</span>
        <div className="didit-agent-chips">
          {agents.map(name => <span key={name} className="didit-agent-chip">{name}</span>)}
        </div>
      </div>
    </div>;
};

<AgentPromptAccordion
  title="Create Workflow API Prompt"
  prompt={`Create a simple Didit workflow through the Management API.

Endpoint:
POST https://verification.didit.me/v3/workflows/

Authentication:
Use the x-api-key header with my Didit API key.

Requirements:
- Use the simple v3 API shape with features.
- Send features in the exact order users should complete them.
- Do not send workflow_graph, branches, actions, webhooks, or conditional logic.
- Didit will convert the features array into a linear node-based workflow and add the final status step automatically.
- Use uppercase feature values such as OCR, LIVENESS, FACE_MATCH, AML, QUESTIONNAIRE, EMAIL_VERIFICATION, PHONE_VERIFICATION, or PROOF_OF_ADDRESS.
- Configure each feature with the fields from /management-api/workflows/feature-configs.
- For an OCR feature, omit config.documents_allowed or send an empty object to allow every supported document type. Send config.documents_allowed only to restrict the allowed documents.
- Put dependency features first. For example, put OCR before FACE_MATCH, NFC, DATABASE_VALIDATION, or user AML checks that depend on document data.
- For a QUESTIONNAIRE feature, first create the questionnaire with POST /v3/questionnaires/ and use the returned questionnaire_id as config.questionnaire_uuid.
- Store the workflow uuid returned by the response. Use it as workflow_id when creating sessions and as settings_uuid when updating this workflow.

Example KYC request body:
{
"workflow_label": "Standard KYC",
"features": [
{
  "feature": "OCR",
  "config": {
    "documents_allowed": {
      "USA": {
        "DL": {
          "enabled": 1,
          "sides": 2
        }
      }
    },
    "duplicated_user_action": "REVIEW"
  }
},
{
  "feature": "LIVENESS",
  "config": {
    "face_liveness_method": "PASSIVE"
  }
},
{
  "feature": "FACE_MATCH"
}
]
}

Example questionnaire workflow body:
{
"workflow_label": "Source of Funds",
"features": [
{
  "feature": "QUESTIONNAIRE",
  "config": {
    "questionnaire_uuid": "11111111-2222-3333-4444-555555555555",
    "review_questionnaire_manually": true
  }
}
]
}

Build the API call, handle validation errors, and return the workflow uuid from the response.`}
/>

Use this endpoint to create workflows through the API without building workflow graph nodes yourself. Send a `features` array in the order users should complete the checks. Didit converts it into a node-based workflow internally and adds the final status step automatically.

This `POST /v3/workflows/` endpoint creates **simple, linear** workflows only — a flat `features` list. For **branching, conditional routing, Document AI, and decline/decision nodes**, use the node **[graph format](#branching--node-based-workflows-graph)** documented below (it powers the visual builder in the console and the [Didit MCP](/integration/mcp/overview) workflow tools).

See [Workflow Feature Configs](/management-api/workflows/feature-configs) for every `config` field accepted by each feature.

## Basic KYC workflow

```json theme={null}
{
  "workflow_label": "Standard KYC",
  "features": [
    {
      "feature": "OCR",
      "config": {
        "documents_allowed": {
          "USA": {
            "DL": {
              "enabled": 1,
              "sides": 2
            }
          }
        },
        "duplicated_user_action": "REVIEW"
      }
    },
    {
      "feature": "LIVENESS",
      "config": {
        "face_liveness_method": "PASSIVE"
      }
    },
    {
      "feature": "FACE_MATCH"
    }
  ]
}
```

## Questionnaire workflow

First create the questionnaire with `POST /v3/questionnaires/`. Then use the returned `questionnaire_id` as `questionnaire_uuid`.

```json theme={null}
{
  "workflow_label": "Source of Funds",
  "features": [
    {
      "feature": "QUESTIONNAIRE",
      "config": {
        "questionnaire_uuid": "11111111-2222-3333-4444-555555555555",
        "review_questionnaire_manually": true
      }
    }
  ]
}
```

Feature order matters. For example, put `OCR` before `FACE_MATCH`, `NFC`, `DATABASE_VALIDATION`, or user AML checks that depend on document data.

## Feature config reference

Each `features[]` item has this shape:

```json theme={null}
{
  "feature": "OCR",
  "label": "ID Verification",
  "config": {}
}
```

The exact `config` object depends on the feature. Review the full field list, defaults, required settings, value ranges, and examples in [Workflow Feature Configs](/management-api/workflows/feature-configs).

For `OCR`, omit `config.documents_allowed`, send `null`, or send `{}` to allow every supported document type. Include it only when you want to restrict the workflow to specific countries or document types. For `QUESTIONNAIRE`, `config.questionnaire_uuid` is required.

## Optional workflow settings

You can include any of these top-level fields when creating a workflow to control session behavior. They are all optional and fall back to the workflow defaults when omitted.

| Field                     | Type                      | Description                                                                                                             |
| ------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `is_white_label_enabled`  | boolean                   | Enable white-label customization for sessions created with this workflow. Default `false`.                              |
| `is_desktop_allowed`      | boolean                   | Allow the verification flow to run on desktop browsers. Default `false`.                                                |
| `max_retry_attempts`      | integer (0–10)            | Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline. Default `3`. |
| `retry_window_days`       | integer (1–365) or `null` | Rolling window in days used to count retries (`null` = all-time limit). Default `7`.                                    |
| `session_expiration_time` | integer (3600–2419200)    | Seconds before an unfinished session expires. Minimum 1 hour, maximum 4 weeks. Default `604800` (7 days).               |

```json theme={null}
{
  "workflow_label": "Standard KYC",
  "is_white_label_enabled": true,
  "is_desktop_allowed": true,
  "max_retry_attempts": 5,
  "retry_window_days": 14,
  "session_expiration_time": 86400,
  "features": [
    { "feature": "OCR" },
    { "feature": "LIVENESS" },
    { "feature": "FACE_MATCH" }
  ]
}
```

## Branching & node-based workflows (graph)

A linear `features` list can't express "if X then Y." For **conditional branching** (decline on a status, route on an extracted field, gate a step on a match), a **Document AI** request step, or terminal **decline/decision** nodes, Didit workflows use a **node graph** — the same structure the console's visual builder produces.

<Info>
  The simplest way to build a graph workflow is the **[Didit MCP](/integration/mcp/overview)** — ask an agent in plain language ("after ID verification, if the profession is ≈ 'Software Engineer', add a proof-of-funds step") and it constructs, validates, and saves the graph for you. The structure below is what those tools (and the console) produce.
</Info>

### Graph shape

A graph is `{ start_node, nodes }`. `nodes` is a map of node id → node. Edges are implicit: each node points forward via `next` (unconditional) and/or `branches[].goto` (conditional).

| Node type           | Shape                                                                                                                                   |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `feature`           | `{ "node_type": "feature", "feature": "OCR", "config": {…}, "next": "<id>", "branches": [] }`                                           |
| `branch`            | `{ "node_type": "branch", "branches": [ { "id", "logic": "and"\|"or", "rules": [<rule>], "goto": "<id>" } ], "next": "<fallback id>" }` |
| `status` (terminal) | `{ "node_type": "status", "session_status": "Approved"\|"Declined"\|"In Review"\|"Determine" }`                                         |
| `action`            | `{ "node_type": "action", "actions": [ { "type": "add_tag"\|"add_review"\|… } ], "next": "<id>" }`                                      |
| `webhook`           | `{ "node_type": "webhook", "config": { "url", "method" }, "branches": [], "next": "<id>" }`                                             |

`start_node` must be a feature node. Branches are evaluated in order, **first match wins**; an empty `rules: []` is the catch-all "else" (keep it last). `session_status: "Determine"` auto-decides from the combined feature outcomes.

<Warning>
  **Every branch node must have an explicit else branch** — a branch with `rules: []` that routes sessions matching none of the conditions. Don't rely on the branch node's `next` as the fallback: a bare `next` renders as an ambiguous edge in the builder and is easy to omit. When you send a branch node with a `next` and no else branch, the API **converts that `next` into an explicit else branch** for you (so `{ …, "branches": [<conditions>], "next": "liveness" }` is saved as `branches: [<conditions>, { "id": "else", "rules": [], "goto": "liveness" }]`). Build the else yourself to be explicit; the conversion is a safety net, not a substitute.
</Warning>

### Branch rules & operators

Each rule compares a **field** to a **value** with an **operator**:

```json theme={null}
{ "field": "kyc.extra_fields.profession", "operator": "fuzzy_match", "value": "Software Engineer", "score": 80 }
```

* **Fields** name a feature outcome (`kyc.status`, `aml.status`, `document_ai.status`, `face_match.score`, …) or an extracted value (`kyc.full_name`, `kyc.age`, `kyc.nationality`, `kyc.extra_fields.profession`, a Document-AI field `document_ai.<key>`, `expected_details.*`). A branch may only read a field whose feature has run on every path reaching it.
* **Operators:** `equals`, `not_equals`, `greater_than(_or_equals)`, `less_than(_or_equals)`, `contains`, `not_contains`, `in`, `not_in`, `is_empty`, `is_not_empty`, `regex`, and **`fuzzy_match`** (string fields only; requires a `score` 0–100 — a similarity threshold, e.g. profession ≥ 80% similar to "Software Engineer").

### Document AI (e.g. proof of funds)

A `DOCUMENT_AI` feature node requests one or more documents and extracts named fields:

```json theme={null}
{
  "node_type": "feature",
  "feature": "DOCUMENT_AI",
  "label": "Proof of Funds",
  "config": {
    "document_ai_documents": [
      {
        "document_key": "proof_of_funds",
        "title": "Proof of Funds",
        "description": "Upload a recent bank statement.",
        "fields": [
          {
            "key": "account_holder",
            "name": "Account holder",
            "instruction": "The full name of the account holder as printed on the statement.",
            "type": "text",
            "required": true,
            "is_full_name": true
          },
          {
            "key": "closing_balance",
            "name": "Closing balance",
            "instruction": "The closing balance amount.",
            "type": "number",
            "required": true
          }
        ]
      }
    ],
    "document_ai_missing_required_fields_action": "REVIEW",
    "document_ai_name_mismatch_action": "REVIEW"
  },
  "next": "final_decision"
}
```

Each configured field becomes a branchable value `document_ai.<key>`, and the step's outcome is `document_ai.status`.

Set `"is_full_name": true` on the field that holds the document holder's full name. Its extracted value is compared against the verified identity's full name (the OCR/ID-verification name, or the session's expected details), and `document_ai_name_mismatch_action` decides what happens on a mismatch below `document_ai_name_match_score_threshold`. The flag is **optional** — a document may mark no field — and **at most one field per document** may set it. See [Workflow feature configs → Document AI](/management-api/workflows/feature-configs#document-ai) for every Document AI config field.

### Endpoints & draft lifecycle

Graph editing is per-application and uses the console/management endpoints (user-Bearer + `read:workflows`/`write:workflows`). A graph can only be edited on a **draft** version, so the lifecycle is **create-draft → set graph → publish**:

| Method  | Path                                                                              | Purpose                                 |
| ------- | --------------------------------------------------------------------------------- | --------------------------------------- |
| `GET`   | `…/verification-settings/{settings_uuid}/workflow-graph/`                         | Current graph + `status`/`is_editable`  |
| `POST`  | `…/application/{app}/workflow-graph/validate/` (body `{ "graph": … }`)            | Dry-run validation                      |
| `GET`   | `…/application/{app}/workflow-graph/field-definitions/`                           | Branchable fields + valid operators     |
| `POST`  | `…/verification-settings/{settings_uuid}/create-draft/`                           | Editable draft from a published version |
| `PUT`   | `…/verification-settings/{settings_uuid}/workflow-graph/` (body `{ "graph": … }`) | Save the graph                          |
| `PATCH` | `…/verification-settings/{settings_uuid}/` (body `{ "status": "published" }`)     | Publish the draft                       |

### Worked example: route Software Engineers to proof of funds

> After ID verification: if it was **declined**, decline the session; if the holder's **profession ≈ "Software Engineer"** (≥80% similar), request a **proof-of-funds** document; then make a final decision.

```json theme={null}
{
  "start_node": "ocr_1",
  "nodes": {
    "ocr_1": { "node_type": "feature", "feature": "OCR", "label": "ID Verification", "next": "after_ocr" },
    "after_ocr": {
      "node_type": "branch",
      "label": "Route after ID verification",
      "branches": [
        { "id": "declined", "logic": "or", "rules": [ { "field": "kyc.status", "operator": "equals", "value": "Declined" } ], "goto": "decline" },
        { "id": "software_engineer", "logic": "and", "rules": [ { "field": "kyc.extra_fields.profession", "operator": "fuzzy_match", "value": "Software Engineer", "score": 80 } ], "goto": "proof_of_funds" },
        { "id": "else", "logic": "and", "rules": [], "goto": "final_decision" }
      ]
    },
    "proof_of_funds": {
      "node_type": "feature",
      "feature": "DOCUMENT_AI",
      "label": "Proof of Funds",
      "config": {
        "document_ai_documents": [
          { "document_key": "proof_of_funds", "title": "Proof of Funds", "description": "Upload a recent bank statement.",
            "fields": [
              { "key": "account_holder", "name": "Account holder", "type": "text", "required": true },
              { "key": "closing_balance", "name": "Closing balance", "type": "number", "required": true }
            ] }
        ],
        "document_ai_missing_required_fields_action": "REVIEW"
      },
      "next": "final_decision"
    },
    "final_decision": { "node_type": "status", "session_status": "Determine", "label": "Final decision" },
    "decline": { "node_type": "status", "session_status": "Declined", "label": "Declined" }
  }
}
```

The final node uses `session_status: "Determine"`, which **auto-decides** (approve / decline / in-review) from whatever features actually ran on the session — so both the proof-of-funds path and the else path resolve correctly without any branch reading `document_ai.status` (which doesn't exist on the path that skips Document AI). Remember: a branch may only read a field whose feature ran on *every* path reaching it.

Send it as `{ "graph": <above> }` to the `…/workflow-graph/` endpoint (validate first). To build a **new** workflow from scratch use the MCP's `didit_workflow_set_graph`; to **modify an existing** workflow use `didit_workflow_edit_graph` (small ops, preserves the existing nodes and allow-lists). See the [Didit MCP](/integration/mcp/tools).


## OpenAPI

````yaml POST /v3/workflows/
openapi: 3.0.0
info:
  version: 3.0.0
  title: Didit Verification API
  description: Identity verification API. Authenticate with x-api-key header.
servers:
  - url: https://verification.didit.me
security: []
tags: []
paths:
  /v3/workflows/:
    post:
      tags:
        - Workflows
      summary: Create workflow
      description: >-
        Create a simple workflow. Send `features` in execution order (ordering
        rules apply); defaults to `published` v1. The body accepts ONLY the
        fields listed in `SimpleWorkflowRequest` — any other key (including
        `workflow_type`) is rejected with a 400. Not idempotent — each call
        creates a new workflow. Maximum 50 workflows per application.
      operationId: create_workflow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimpleWorkflowRequest'
            examples:
              Simple KYC:
                summary: Linear ID, liveness, and face match workflow
                value:
                  workflow_label: Standard KYC
                  features:
                    - feature: OCR
                      config:
                        duplicated_user_action: review
                    - feature: LIVENESS
                      config:
                        face_liveness_method: passive
                    - feature: FACE_MATCH
                      config:
                        face_match_score_decline_threshold: 40
                        face_match_score_review_threshold: 60
              KYC + AML:
                summary: Run AML after ID verification
                value:
                  workflow_label: KYC with AML
                  features:
                    - feature: OCR
                    - feature: AML
                      config:
                        aml_score_review_threshold: 70
                        aml_score_approve_threshold: 30
              Questionnaire workflow:
                summary: Run a questionnaire created with POST /v3/questionnaires/
                value:
                  workflow_label: Source of Funds
                  features:
                    - feature: QUESTIONNAIRE
                      config:
                        questionnaire_uuid: 11111111-2222-3333-4444-555555555555
                        review_questionnaire_manually: true
      responses:
        '201':
          description: >-
            Workflow created. The response body is the FULL workflow
            configuration — the same raw `VerificationSettings` serialization
            returned by `GET /v3/workflows/{settings_uuid}/` (~190 fields: every
            feature toggle, threshold, action and the generated
            `workflow_graph`), not the compact list item. Store the `uuid` (also
            exposed as `workflow_id`) so you can reference it from later
            session-creation calls. Note: `features` is a `" + "`-joined string
            and prices are JSON numbers.
          content:
            application/json:
              schema:
                type: object
                description: >-
                  Full workflow configuration (abbreviated here — see `GET
                  /v3/workflows/{settings_uuid}/`).
                additionalProperties: true
              examples:
                Created (abbreviated):
                  summary: Key fields of the full configuration object
                  value:
                    uuid: c3d4e5f6-7890-12cd-ef34-333333333333
                    workflow_id: c3d4e5f6-7890-12cd-ef34-333333333333
                    workflow_label: Standard KYC
                    workflow_type: null
                    is_default: false
                    is_archived: false
                    total_price: 0.15
                    min_price: 0.15
                    max_price: 0.15
                    features: OCR + LIVENESS + FACE_MATCH
                    is_simple_workflow: false
                    is_editable: false
                    workflow_url: https://verify.didit.me/u/w9TkPOx8RWG3UAz5ZRQGVQ
                    workflow_graph:
                      start_node: ocr
                      nodes: ... one node per feature plus a final status node ...
                    max_retry_attempts: 3
                    retry_window_days: 7
                    session_expiration_time: 604800
                    version: 1
                    status: published
                    published_at: '2026-05-17T10:30:00Z'
                    created_at: '2026-05-17T10:30:00Z'
                    updated_at: '2026-05-17T10:30:00Z'
        '400':
          description: >-
            Validation error. Common causes: a top-level key outside the
            accepted whitelist (e.g. `workflow_type`), an unknown `feature`
            value, a feature placed before its dependency (e.g., `FACE_MATCH`
            before `OCR`), a passive feature (`AML`, `IP_ANALYSIS`,
            `DATABASE_VALIDATION`, `KYB_KEY_PEOPLE`) used as the first step, a
            `QUESTIONNAIRE` feature whose `config.questionnaire_uuid` does not
            exist for this application, or an out-of-range threshold. Body is a
            DRF validation error object — keys are field names, values are
            arrays of human-readable messages.
          content:
            application/json:
              examples:
                Missing feature:
                  value:
                    features:
                      - At least one feature is required.
                Unknown top-level field:
                  value:
                    workflow_type:
                      - >-
                        The v3 workflow API only accepts `workflow_label`,
                        `is_default`, `status`, `features`,
                        `is_white_label_enabled`, `is_desktop_allowed`,
                        `max_retry_attempts`, `retry_window_days`,
                        `session_expiration_time`, `face_liveness_max_attempts`,
                        `face_match_max_attempts`.
                Unknown feature:
                  value:
                    features:
                      - >-
                        Invalid feature 'FOO'. Allowed values: AGE_ESTIMATION,
                        AML, DATABASE_VALIDATION, EMAIL_VERIFICATION,
                        FACE_MATCH, IP_ANALYSIS, KYB_DOCUMENTS, KYB_KEY_PEOPLE,
                        KYB_REGISTRY, LIVENESS, NFC, OCR, PHONE_VERIFICATION,
                        PROOF_OF_ADDRESS, QUESTIONNAIRE.
                Ordering:
                  value:
                    features:
                      - 'Feature ''FACE_MATCH'' must appear after one of: OCR.'
                Questionnaire not found:
                  value:
                    features: >-
                      Questionnaire '11111111-2222-3333-4444-555555555555' was
                      not found for this application. Create the questionnaire
                      first, then use its questionnaire_id.
        '403':
          description: >-
            The API key is missing, malformed, expired, or scoped to a different
            application. Refresh via the [Auth API](/auth-api/get-credentials)
            and retry.
          content:
            application/json:
              examples:
                Invalid token:
                  value:
                    detail: You do not have permission to perform this action.
        '415':
          description: >-
            Unsupported media type. Set `Content-Type: application/json` and
            send a JSON body.
        '429':
          description: Rate limit exceeded. Retry with exponential backoff.
        '500':
          description: Unexpected server error while creating the workflow. Safe to retry.
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -X POST 'https://verification.didit.me/v3/workflows/' \
              -H 'x-api-key: YOUR_API_KEY' \
              -H 'Content-Type: application/json' \
              -d '{
                "workflow_label": "Standard KYC",
                "features": [
                  {"feature": "OCR"},
                  {"feature": "LIVENESS", "config": {"face_liveness_method": "passive"}},
                  {"feature": "FACE_MATCH", "config": {"face_match_score_decline_threshold": 40, "face_match_score_review_threshold": 60}}
                ]
              }'
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            payload = {
                "workflow_label": "Standard KYC",
                "features": [
                    {"feature": "OCR"},
                    {"feature": "LIVENESS", "config": {"face_liveness_method": "passive"}},
                    {"feature": "FACE_MATCH"},
                ],
            }
            resp = requests.post(
                "https://verification.didit.me/v3/workflows/",
                headers={"x-api-key": "YOUR_API_KEY"},
                json=payload,
                timeout=10,
            )
            resp.raise_for_status()
            workflow = resp.json()
            print("Workflow id:", workflow["uuid"])
        - lang: JavaScript
          label: Node.js (fetch)
          source: >-
            const res = await
            fetch("https://verification.didit.me/v3/workflows/", {
              method: "POST",
              headers: {
                'x-api-key': 'YOUR_API_KEY',
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                workflow_label: "Standard KYC",
                features: [
                  { feature: "OCR" },
                  { feature: "LIVENESS", config: { face_liveness_method: "passive" } },
                  { feature: "FACE_MATCH" },
                ],
              }),
            });

            if (!res.ok) throw new Error(`Didit ${res.status}: ${await
            res.text()}`);

            const workflow = await res.json();

            console.log("Workflow id:", workflow.uuid);
components:
  schemas:
    SimpleWorkflowRequest:
      type: object
      description: >-
        Create a simple linear workflow. The API converts this feature list into
        a node-based workflow graph internally. STRICT field whitelist: any key
        not listed here (including `workflow_type`) is rejected with a 400.
      required:
        - features
      properties:
        workflow_label:
          type: string
          maxLength: 50
          default: ID Verification
          description: >-
            Display name for the workflow (max 50 characters). Defaults to `"ID
            Verification"` when omitted — always set it so workflows stay
            distinguishable.
          example: Standard KYC
        is_default:
          type: boolean
          description: Set this workflow as the default for new sessions.
        status:
          type: string
          enum:
            - draft
            - published
          description: >-
            Omit this field to create a published workflow ready for sessions.
            Use `draft` only when you want to save without publishing.
        features:
          type: array
          minItems: 1
          description: >-
            Verification features in execution order. The API links each item to
            the next one and adds one final status node automatically.
          items:
            $ref: '#/components/schemas/SimpleWorkflowFeature'
        is_white_label_enabled:
          type: boolean
          description: >-
            Enable white-label customization for sessions created with this
            workflow.
          default: false
        is_desktop_allowed:
          type: boolean
          description: Allow the verification flow to run on desktop browsers.
          default: false
        max_retry_attempts:
          type: integer
          minimum: 0
          maximum: 10
          default: 3
          description: >-
            Maximum retry attempts allowed after a declined verification. `0`
            blocks the user after the first decline.
        retry_window_days:
          type: integer
          minimum: 1
          maximum: 365
          nullable: true
          default: 7
          description: >-
            Rolling window in days used to count retries. `null` enforces an
            all-time limit.
        face_liveness_max_attempts:
          type: integer
          minimum: 1
          maximum: 3
          default: 3
          description: >-
            Maximum liveness submissions per session. Default 3: one initial
            attempt plus two retries. This value is also copied into generated
            liveness and age-estimation workflow nodes unless the feature-level
            config overrides it.
        face_match_max_attempts:
          type: integer
          minimum: 1
          maximum: 3
          default: 3
          description: >-
            Maximum face-match submissions per session. Default 3: one initial
            attempt plus two retries. This value is also copied into generated
            face-match workflow nodes unless the feature-level config overrides
            it.
        session_expiration_time:
          type: integer
          minimum: 3600
          maximum: 2419200
          default: 604800
          description: >-
            Seconds before an unfinished session expires. Minimum 1 hour (3600),
            maximum 4 weeks (2419200).
    SimpleWorkflowFeature:
      type: object
      required:
        - feature
      properties:
        feature:
          type: string
          description: Feature to run. Use the uppercase values shown here.
          enum:
            - OCR
            - NFC
            - LIVENESS
            - FACE_MATCH
            - PROOF_OF_ADDRESS
            - QUESTIONNAIRE
            - DOCUMENT_AI
            - PHONE_VERIFICATION
            - EMAIL_VERIFICATION
            - DATABASE_VALIDATION
            - AML
            - IP_ANALYSIS
            - AGE_ESTIMATION
            - KYB_REGISTRY
            - KYB_DOCUMENTS
            - KYB_KEY_PEOPLE
        config:
          type: object
          description: >-
            Feature-specific settings. For `QUESTIONNAIRE`, include
            `questionnaire_uuid` with the `questionnaire_id` returned by the
            questionnaire create endpoint.
          additionalProperties: true
        label:
          type: string
          description: Optional internal label for this workflow node.
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````