Workflows & Presets
Two reusable objects sit behind an ephemeral token:
- a workflow — an ordered list of steps the hosted verification UI renders as a wizard;
- a preset — a saved template of token settings, so you do not resend the same capability set on every mint.
Both live on the same overloaded endpoint as the token mint, and both are scoped to your tenant:
an id that belongs to another tenant answers 404, never another tenant's data.
Identiwise is in final development and the API is not open to the public yet. This page describes the behaviour of the current build, including which per-step fields are not yet acted on.
The overloaded endpoint
POST /api/v1/ephemeral-tokens dispatches on the body. The first matching field wins, in this
order:
| Body field | Action | Role required |
|---|---|---|
list_presets: true | List this tenant's presets | admin, superuser |
delete_preset_id (uuid) | Delete one preset | admin, superuser |
update_preset_id (uuid) | Update one preset | admin, superuser |
list_workflows: true | List this tenant's workflows (with_steps: true to include steps) | admin, superuser |
delete_workflow_id (uuid) | Delete one workflow | admin, superuser |
update_workflow_id (uuid) | Update one workflow, and replace its steps if steps is present | admin, superuser |
create_workflow: true and/or create_preset: true | Create either or both | admin, superuser |
subject_id (uuid) | Mint an ephemeral token — see Create Token | admin, superuser |
Every operation on this endpoint requires admin or superuser; the read-only view role is
refused 403 at the router, including the list operations.
Creation is combinable: one request may carry create_workflow, create_preset and
subject_id together, and each is created in that order from the same body. They are not
auto-linked — a preset or a token is bound to a workflow only by an explicit workflow_id, so
create the workflow first and reference the id it returns. Every id on this endpoint is a
canonical UUID string.
1. Workflows
A workflow is a name, an optional description and an ordered list of steps. The step order and
step type are what drive the hosted UI — it walks the subject through the steps in
step_order, showing one capture or instruction screen per step.
Step types the hosted UI understands
step_name | What the subject sees |
|---|---|
verify_license | A prompt to capture or upload an ID document. |
check_selfie | A prompt to capture a face photo. |
age_gender_estimation | An analysis step run against a captured face photo. |
Any other step_name renders a generic instruction page rather than failing, so an unknown or
future step type never breaks a live session.
Each step can carry a config_json string, and it is stored and returned verbatim. The keys
inside it are not enforced by the API today — the step's step_name and step_order are the
only parts of a workflow that change behaviour. Per-step configuration fields are reserved for a
later release; do not treat a value inside config_json as a control that is in force.
Create a workflow
- cURL
- PHP
- Response
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"create_workflow": true,
"workflow_name": "Standard onboarding",
"description": "Document, then selfie, then age estimation.",
"steps": [
{ "step_name": "verify_license", "step_order": 1 },
{ "step_name": "check_selfie", "step_order": 2 },
{ "step_name": "age_gender_estimation", "step_order": 3 }
]
}'
<?php
$client = new GuzzleHttp\Client();
$response = $client->post('https://acme.identiwise.com/api/v1/ephemeral-tokens', [
'headers' => [
'Authorization' => 'Bearer YOUR_SYSTEM_TOKEN',
'Content-Type' => 'application/json',
],
'json' => [
'create_workflow' => true,
'workflow_name' => 'Standard onboarding',
'description' => 'Document, then selfie, then age estimation.',
'steps' => [
['step_name' => 'verify_license', 'step_order' => 1],
['step_name' => 'check_selfie', 'step_order' => 2],
['step_name' => 'age_gender_estimation', 'step_order' => 3],
],
],
]);
$workflowId = json_decode((string) $response->getBody(), true)['workflow_id'];
// "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33"
{
"status": "workflow_created",
"workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"message": "Workflow created successfully; no ephemeral token created"
}
The workflow row and its steps are written in one transaction: if any step fails to insert, nothing is created.
List workflows
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "list_workflows": true, "with_steps": true }'
{
"status": "workflow_list",
"count": 1,
"workflows": [
{
"id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"workflow_name": "Standard onboarding",
"description": "Document, then selfie, then age estimation.",
"created_at": "2026-09-01 09:12:44",
"updated_at": "2026-09-01 09:12:44",
"steps": [
{
"id": "0191f3d0-15b9-7c02-9a44-77e1b3d5c410",
"workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"step_name": "verify_license",
"step_order": 1,
"config_json": null,
"created_at": "2026-09-01 09:12:44"
}
]
}
]
}
Update and delete
update_workflow_id updates the name and description. If the body contains steps, the
existing steps are replaced wholesale with the list you send, in one transaction — send the
complete step list, not a delta. Omit steps to leave the existing ones untouched.
Unlike a preset update, a workflow update is not partial for description: omitting it stores
an empty description. Send description on every update where you want the existing one kept.
(workflow_name does fall back to the stored value when omitted.)
# Replace the steps of an existing workflow
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"update_workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"workflow_name": "Standard onboarding (v2)",
"description": "Document, then selfie.",
"steps": [
{ "step_name": "verify_license", "step_order": 1 },
{ "step_name": "check_selfie", "step_order": 2 }
]
}'
# Delete
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "delete_workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33" }'
Both answer 200 with workflow_updated / workflow_deleted and the id, or 404 when the
workflow is not in your tenant.
2. Presets
A preset is a saved template of the token settings described in
Create Token. A token minted with preset_id inherits the values the
preset stores, including require_face_match, max_uses, the per-type attempt caps, the
capability flags, min_age and a linked workflow_id — with the two exceptions noted below.
Precedence at mint: request body → preset → built-in default. Anything you send in the mint body overrides the preset for that one token.
A preset stores a value for every flag, including the ones you leave out — and
can_return_capabilities, can_list_reupload, can_list_need_reply and
can_read_own_capabilities default to off in a preset even though minting without a preset
defaults them on. (can_edit_details and view_subject_details default on either way.)
A token minted from the preset inherits the stored value, so list every flag the preset should
grant. The one exception is require_face_match: an explicit null in the mint body means
"no opinion", so it never downgrades a strict preset.
The per-type attempt caps have the opposite gap: a preset that stores null (unlimited) for
max_license_attempts or max_selfie_attempts is not inherited. The mint reads a stored
null as "not set" and falls through to the built-in default of 3, so tokens from that preset
accept three document and three selfie submissions. To mint an uncapped token, send
"max_license_attempts": null (or "max_selfie_attempts": null) in the mint body itself.
Create a preset
- cURL
- PHP
- Response
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"create_preset": true,
"preset_name": "Standard onboarding",
"description": "Strict assurance, three document attempts.",
"workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"upload_type": "any",
"can_edit_details": true,
"view_subject_details": true,
"can_return_capabilities": true,
"can_list_reupload": true,
"can_list_need_reply": true,
"require_face_match": true,
"max_uses": 5,
"max_license_attempts": 3,
"max_selfie_attempts": 3,
"min_age": 18
}'
<?php
$client = new GuzzleHttp\Client();
$response = $client->post('https://acme.identiwise.com/api/v1/ephemeral-tokens', [
'headers' => [
'Authorization' => 'Bearer YOUR_SYSTEM_TOKEN',
'Content-Type' => 'application/json',
],
'json' => [
'create_preset' => true,
'preset_name' => 'Standard onboarding',
'description' => 'Strict assurance, three document attempts.',
'workflow_id' => '0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33',
'upload_type' => 'any',
'can_edit_details' => true,
'view_subject_details' => true,
'can_return_capabilities' => true,
'can_list_reupload' => true,
'can_list_need_reply' => true,
'require_face_match' => true,
'max_uses' => 5,
'max_license_attempts' => 3,
'max_selfie_attempts' => 3,
'min_age' => 18,
],
]);
$presetId = json_decode((string) $response->getBody(), true)['preset_id'];
// "0191f3d4-9c02-7f18-b3a1-6e5d2c7b4900"
{
"status": "preset_created",
"preset_id": "0191f3d4-9c02-7f18-b3a1-6e5d2c7b4900",
"message": "Preset created successfully; no ephemeral token was created"
}
preset_name is required. Creating a workflow and a preset in the same request answers
workflow_and_preset_created with both ids.
List, update and delete
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "list_presets": true }'
The listing returns status: "preset_list", a count and a presets array carrying each
preset's id, preset_name, description, upload_type, the capability flags, max_uses, the
per-type attempt caps, require_face_match, min_age, workflow_id and created_at. It also
returns license_threshold, selfie_threshold, photo_stream_capability,
can_read_own_capabilities and workflow_flags, which are stored but not yet enforced — see
the table in Create Token.
update_preset_id is a partial update: fields you omit keep their stored values. Sending
max_license_attempts: null or max_selfie_attempts: null means "unlimited until the token
expires"; 0 and negative values are refused with 400.
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"update_preset_id": "0191f3d4-9c02-7f18-b3a1-6e5d2c7b4900",
"max_license_attempts": 2
}'
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "delete_preset_id": "0191f3d4-9c02-7f18-b3a1-6e5d2c7b4900" }'
Both answer 200 with preset_updated / preset_deleted and the id, or 404 when the preset
is not in your tenant.
3. Using them at mint
# From a preset — the smallest possible mint
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "0191f3c2-7a41-7c3e-9b02-4f8a1d6e5b90",
"preset_id": "0191f3d4-9c02-7f18-b3a1-6e5d2c7b4900"
}'
# From a workflow alone, with settings sent inline
curl -X POST "https://acme.identiwise.com/api/v1/ephemeral-tokens" \
-H "Authorization: Bearer YOUR_SYSTEM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "0191f3c2-7a41-7c3e-9b02-4f8a1d6e5b90",
"workflow_id": "0191f3d0-15b8-7a44-8e77-2c9d0b6f1a33",
"expires_in_minutes": 30,
"max_uses": 2
}'
A preset_id or workflow_id that does not resolve inside your tenant answers 404 and mints
nothing.
4. What the subject's client sees
GET /api/v1/ephemeral/capability, authenticated with X-Api-Key: {ephemeral token}, returns
the capability set and the resolved workflow steps. It is how the hosted verification UI decides
which screens to render, and it is the same contract if you build your own client.
{
"can_edit_details": true,
"view_subject_details": true,
"upload_type": "any",
"can_list_reupload": true,
"can_list_need_reply": true,
"can_return_capabilities": true,
"require_face_match": true,
"min_age": 18,
"max_uses": 5,
"use_count": 1,
"max_license_attempts": 3,
"license_use_count": 1,
"max_selfie_attempts": 3,
"selfie_use_count": 0,
"workflow_steps": [
{
"step_name": "verify_license",
"step_order": 1,
"config": {}
}
]
}
Each step in this response carries step_name, step_order and a decoded config object — the
six-column form with ids and timestamps shown in section 1 is what
list_workflows returns, not this route.
The consumption counters are returned alongside the caps, so a client can tell how many
submissions the credential has left. The response also echoes license_threshold,
selfie_threshold, photo_stream_capability, can_read_own_capabilities, workflow_flags,
verification_type and a permanently null restrict_detail_edits — all stored, none of them
enforced today. Access to this route is gated by can_return_capabilities; when that flag is
unset the response carries only the flag itself.