English
Examples
This page drives the API with curl, from a fresh key to the first execution. Every request and response uses the real field names; long responses are abbreviated with …. Replace <PUBLIC_BASE_URL> with the address of your instance.
The walkthrough needs a key with workflows:write (create, save, publish), executions:write (test and read executions) and messages:read (pick a message to test on).
1. Create an API key
The simplest way is the interface: Settings › API keys › New key, tick the three domains, copy the secret. Over the API, keys are created by a session only, so sign in first and keep the cookie:
sh
curl -s -c cookies.txt -X POST <PUBLIC_BASE_URL>/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{ "email": "alice@example.test", "password": "…" }'
curl -s -b cookies.txt -X POST <PUBLIC_BASE_URL>/api/v1/api-keys \
-H 'Content-Type: application/json' \
-d '{
"name": "Walkthrough",
"scopes": ["workflows:write", "executions:write", "messages:read"],
"expiresInDays": 30
}'json
{
"key": {
"id": "0192f1c2-3b4d-7e8f-9a0b-1c2d3e4f5a6b",
"name": "Walkthrough",
"prefix": "Qm9uam91",
"scopes": ["executions:write", "messages:read", "workflows:write"],
"memberId": "0192f1c2-0000-7000-8000-000000000001",
"memberEmail": "alice@example.test",
"createdAt": "2026-10-04T09:00:00.000Z",
"expiresAt": "2026-11-03T09:00:00.000Z",
"lastUsedAt": null,
"revokedAt": null
},
"token": "mk_Qm9uam91cl9jZXN0X3VuX2V4ZW1wbGVfZGVfY2xl"
}token is shown here and never again. Keep it in a variable for the rest of the page:
sh
export MK_URL='<PUBLIC_BASE_URL>'
export MK_KEY='mk_Qm9uam91cl9jZXN0X3VuX2V4ZW1wbGVfZGVfY2xl'2. Create a workflow
POST /api/v1/workflows creates a draft owned by the member. The body needs only a name; without a graph, the server creates an empty draft with one email trigger. The route creates something, so send an Idempotency-Key: if the connection drops, replay the same command and you get the same workflow back.
sh
curl -s -X POST $MK_URL/api/v1/workflows \
-H "Authorization: Bearer $MK_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f1d4c2e-6b3a-4a0e-9d2b-7c5e1f0a8b43' \
-d '{ "name": "Mark invoices as read" }'json
{
"id": "0192f1c2-aaaa-7000-8000-000000000001",
"name": "Mark invoices as read",
"published": false,
"draftVersion": {
"id": "0192f1c2-aaaa-7000-8000-00000000000a",
"number": 1,
"publishedAt": null,
"createdAt": "2026-10-04T09:01:00.000Z",
"updatedAt": "2026-10-04T09:01:00.000Z"
},
"publishedVersion": null,
"archivedAt": null,
"dispatchPriority": null,
"pausedAt": null,
"errorWorkflowId": null,
"graph": {
"id": "0192f1c2-aaaa-7000-8000-00000000000a",
"workflowId": "0192f1c2-aaaa-7000-8000-000000000001",
"nodes": [
{ "id": "__trigger__", "type": "trigger.email", "version": 1, "name": "Email received", "params": {}, "onError": "fail" }
],
"connections": []
},
"validation": { "ok": false, "errors": [ { "code": "empty_workflow", "message": "…" } ], "warnings": [] },
"createdAt": "2026-10-04T09:01:00.000Z",
"updatedAt": "2026-10-04T09:01:00.000Z"
}The response is the full workflow (GET /api/v1/workflows/{id} returns the same shape): its summary, the draft graph, and the validation diagnostic of that graph. An empty draft is not publishable yet, which the diagnostic says.
sh
export WF='0192f1c2-aaaa-7000-8000-000000000001'3. Save the draft
PUT /api/v1/workflows/{id}/draft replaces the draft graph. The graph is the document described in Workflows: nodes and connections, plus the id and workflowId of the version (take them from the previous response; the server re-anchors them to the real draft anyway). Here the trigger feeds a mail.flag step that marks the message as read.
sh
curl -s -X PUT $MK_URL/api/v1/workflows/$WF/draft \
-H "Authorization: Bearer $MK_KEY" \
-H 'Content-Type: application/json' \
-d '{
"graph": {
"id": "0192f1c2-aaaa-7000-8000-00000000000a",
"workflowId": "0192f1c2-aaaa-7000-8000-000000000001",
"nodes": [
{ "id": "__trigger__", "type": "trigger.email", "version": 1, "name": "Email received", "params": {} },
{ "id": "flag", "type": "mail.flag", "version": 1, "name": "Mark as read", "params": { "seen": true } }
],
"connections": [
{ "from": "__trigger__", "output": "main", "to": "flag" }
]
},
"expectedDraft": { "id": "0192f1c2-aaaa-7000-8000-00000000000a", "updatedAt": "2026-10-04T09:01:00.000Z" }
}'json
{
"version": {
"id": "0192f1c2-aaaa-7000-8000-00000000000a",
"number": 1,
"publishedAt": null,
"createdAt": "2026-10-04T09:01:00.000Z",
"updatedAt": "2026-10-04T09:03:12.000Z"
},
"validation": { "ok": true, "errors": [], "warnings": [] }
}Saving is warning-only: an inconsistent graph is saved and its problems come back in validation; only a malformed document is refused (400 workflow.invalid_graph). expectedDraft is optional: with it, the save is refused with 409 workflow.draft_conflict if someone (another tab, a colleague) wrote the draft since you read it. Without it, you overwrite.
4. Test the draft on a real message
A test runs the draft, not the published version, on a real message of your mailbox, in simulation: nothing is sent, nothing is written outside Mankomail (see Test mode). Pick a message first:
sh
curl -s "$MK_URL/api/v1/messages?limit=1" -H "Authorization: Bearer $MK_KEY"Then plan the test with its messageId. The response is 202: the execution is planned, not finished.
sh
curl -s -X POST $MK_URL/api/v1/workflows/$WF/test \
-H "Authorization: Bearer $MK_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 0d2a7e4b-3c5f-4e6a-8b9c-1d2e3f4a5b6c' \
-d '{ "messageId": "0192f1c2-eeee-7000-8000-000000000042" }'json
{
"execution": {
"id": "0192f1c2-bbbb-7000-8000-000000000001",
"workflowId": "0192f1c2-aaaa-7000-8000-000000000001",
"workflowVersionId": "0192f1c2-aaaa-7000-8000-00000000000a",
"mailboxId": "0192f1c2-cccc-7000-8000-000000000001",
"messageId": "0192f1c2-eeee-7000-8000-000000000042",
"status": "queued",
"simulated": true,
"triggerNodeId": "__trigger__",
"triggerType": "trigger.email",
"error": null,
"startedAt": null,
"finishedAt": null,
"createdAt": "2026-10-04T09:05:00.000Z"
},
"plannedNodes": ["__trigger__", "flag"]
}triggerNodeId is only needed when the draft has triggers of different natures (an email trigger and a webhook, say); targetNodeId stops the run after a given node. Read the result with step 7 below: a simulated execution has the same detail as a real one, with simulated: true.
5. Publish
POST /api/v1/workflows/{id}/publish freezes the draft as the published version and arms its triggers. It is blocking: validation errors refuse with 409 workflow.not_publishable and the diagnostic in details.validation.
sh
curl -s -X POST $MK_URL/api/v1/workflows/$WF/publish \
-H "Authorization: Bearer $MK_KEY" \
-H 'Idempotency-Key: 9b8c7d6e-5f4a-4b3c-9d2e-1f0a9b8c7d6e'json
{
"ok": true,
"validation": { "ok": true, "errors": [], "warnings": [] },
"publishedVersion": {
"id": "0192f1c2-aaaa-7000-8000-00000000000a",
"number": 1,
"publishedAt": "2026-10-04T09:06:00.000Z",
"createdAt": "2026-10-04T09:01:00.000Z"
},
"armedMailboxes": 1,
"webhook": null
}From now on, every email arriving in the member's mailboxes starts a real execution. armedMailboxes counts the mailboxes the trigger listens to. For a workflow whose trigger is a webhook, webhook carries the URL and its token, in clear, this once. The next PUT …/draft creates version 2 as the new draft; the published version keeps running until the next publish.
6. List the executions
GET /api/v1/executions is paginated by cursor (Pagination) and filtered by workflowId, mailboxId, status (queued, running, waiting, succeeded, failed, cancelled) and simulated.
sh
curl -s "$MK_URL/api/v1/executions?workflowId=$WF&simulated=false&limit=20" \
-H "Authorization: Bearer $MK_KEY"json
{
"executions": [
{
"id": "0192f1c2-bbbb-7000-8000-000000000007",
"workflowId": "0192f1c2-aaaa-7000-8000-000000000001",
"workflowVersionId": "0192f1c2-aaaa-7000-8000-00000000000a",
"mailboxId": "0192f1c2-cccc-7000-8000-000000000001",
"messageId": "0192f1c2-eeee-7000-8000-000000000051",
"status": "succeeded",
"simulated": false,
"triggerNodeId": "__trigger__",
"triggerType": "trigger.email",
"messageHeadline": { "subject": "Invoice 2026-0142", "fromName": "Acme", "fromEmail": "billing@acme.example" },
"error": null,
"startedAt": "2026-10-04T09:12:44.100Z",
"finishedAt": "2026-10-04T09:12:44.620Z",
"createdAt": "2026-10-04T09:12:44.000Z"
}
],
"nextCursor": null
}Newest first. When nextCursor is not null, send it back as cursor with the same filters to get the next page.
7. Read one execution
GET /api/v1/executions/{id} returns the execution step by step: each step with its status, attempt number, duration, the data it published and the effects it described.
sh
curl -s $MK_URL/api/v1/executions/0192f1c2-bbbb-7000-8000-000000000007 \
-H "Authorization: Bearer $MK_KEY"json
{
"id": "0192f1c2-bbbb-7000-8000-000000000007",
"workflowId": "0192f1c2-aaaa-7000-8000-000000000001",
"workflowName": "Mark invoices as read",
"status": "succeeded",
"simulated": false,
"…": "…",
"steps": [
{
"id": "0192f1c2-ffff-7000-8000-000000000001",
"nodeId": "flag",
"status": "succeeded",
"attempt": 1,
"output": "main",
"data": {},
"dataTruncated": false,
"durationMs": 310,
"…": "…"
}
],
"attachments": []
}An execution of another member, or an unknown id, is 404 execution.not_found. A failed execution can be replayed with POST /api/v1/executions/{id}/retry (executions:write), from the start or from the failed step ({ "from": "failed_step" }); a running one can be stopped with POST /api/v1/executions/{id}/cancel.
Where to go next
- The exact fields of every request and response: the reference, in particular Workflows and Executions.
- Waking a workflow from another system without a mailbox: a webhook trigger (
POST /hooks/wf/{token}) or a signal (POST /api/v1/signals, scopesignals:write). - Letting an AI assistant do the same through its own tools: MCP.