Skip to content

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, scope signals:write).
  • Letting an AI assistant do the same through its own tools: MCP.