English
Mailboxes
Routes are relative to <PUBLIC_BASE_URL>; request and response bodies are JSON unless stated otherwise. Authentication, scopes, pagination and the error format are described in the REST API guides.
GET /api/v1/mailboxes
List my mailboxes
The mailboxes connected by the calling member, with their status, message count, date of the last synchronisation and of the last concluded analysis.
Access — Member session or API key with scope mailboxes:read.
Responses
200 — The mailboxes.
| Field | Type | Required |
|---|---|---|
mailboxes | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"provider": { "type": "string", "enum": [ "gmail", "msgraph", "imap" ] },
"address": { "type": "string" },
"status": { "type": "string", "enum": [ "active", "disconnected", "error" ] },
"syncMode": { "type": "string", "enum": [ "full", "processing_only" ] },
"connectedAt": { "type": "string" },
"backfill": {
"anyOf": [
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [ "pending", "running", "done", "failed" ]
},
"progressPct": {
"anyOf": [
{ "type": "number", "minimum": 0, "maximum": 100 },
{ "type": "null" }
]
},
"oldestSyncedAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "status", "progressPct", "oldestSyncedAt" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"lastDeltaAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"lastSuccessfulSyncAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"lastError": {
"anyOf": [
{
"type": "object",
"properties": {
"kind": { "type": "string" },
"code": { "type": "string" },
"message": { "type": "string" },
"at": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
"operation": { "type": "string" },
"httpStatus": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [ "kind" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"messageCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"analyzedAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [
"id",
"provider",
"address",
"status",
"syncMode",
"connectedAt",
"backfill",
"lastDeltaAt",
"lastError",
"messageCount"
],
"additionalProperties": false
}
}
},
"required": [ "mailboxes" ],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
POST /api/v1/mailboxes/imap
Connect an IMAP/SMTP mailbox
The only provider without an OAuth flow. Both servers are probed before anything is stored, and the failure names which one refused. Reconnecting an address already known to the member replaces its credentials instead of creating a second mailbox (created: false). The password never appears in any response or log.
Access — Member session or API key with scope mailboxes:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
address | string | yes |
imapHost | string | yes |
imapPort | integer | no |
imapSecure | boolean | no |
smtpHost | string | yes |
smtpPort | integer | no |
smtpSecure | boolean | no |
username | string | no |
password | string | yes |
allowInvalidCertificate | boolean | no |
JSON Schema
json
{
"type": "object",
"properties": {
"address": { "type": "string" },
"imapHost": { "type": "string", "minLength": 1, "maxLength": 255 },
"imapPort": { "default": 993, "type": "integer", "minimum": 1, "maximum": 65535 },
"imapSecure": { "default": true, "type": "boolean" },
"smtpHost": { "type": "string", "minLength": 1, "maxLength": 255 },
"smtpPort": { "default": 465, "type": "integer", "minimum": 1, "maximum": 65535 },
"smtpSecure": { "default": true, "type": "boolean" },
"username": { "type": "string", "minLength": 1, "maxLength": 255 },
"password": { "type": "string", "minLength": 1, "maxLength": 1024 },
"allowInvalidCertificate": { "type": "boolean" }
},
"required": [ "address", "imapHost", "smtpHost", "password" ]
}Responses
201 — The mailbox, connected or reconnected.
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
address | string | yes |
created | boolean | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string" },
"address": { "type": "string" },
"created": { "type": "boolean" }
},
"required": [ "mailboxId", "address", "created" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — request.bad_request, credentials.encryption_disabled, imap.host_not_found, imap.unreachable, imap.tls_failed, imap.auth_failed, smtp.host_not_found, smtp.unreachable, smtp.tls_failed, smtp.auth_failed. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
Example request
json
{
"address": "alice@example.test",
"password": "••••••••",
"imapHost": "imap.example.test",
"imapPort": 993,
"imapSecure": true,
"smtpHost": "smtp.example.test",
"smtpPort": 465,
"smtpSecure": true
}Example response (201)
json
{
"mailboxId": "0192f1c2-3b4d-7e8f-9a0b-1c2d3e4f5a6b",
"address": "alice@example.test",
"created": true
}POST /api/v1/mailboxes/{id}/sync
Synchronise a mailbox now
Requests a delta from the provider. enqueued: false is not a failure: a delta was already pending for this mailbox. A mailbox that is not active is refused with 409 rather than silently queued.
Access — Member session or API key with scope mailboxes:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Responses
202 — Synchronisation requested.
| Field | Type | Required |
|---|---|---|
enqueued | boolean | yes |
JSON Schema
json
{
"type": "object",
"properties": { "enqueued": { "type": "boolean" } },
"required": [ "enqueued" ],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — mailbox.not_found, mailbox.not_active. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/mailboxes/{id}/resync
Catch up a period
Re-reads the mailbox from since to now, in slices. Idempotent (known messages are not re-downloaded) and silent: nothing is journaled, no workflow is dispatched, so three weeks can be replayed without three weeks of automatic replies. since must be in the past and within 92 days.
Access — Member session or API key with scope mailboxes:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
since | string (date-time) | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"since": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
}
},
"required": [ "since" ]
}Responses
202 — Catch-up requested.
| Field | Type | Required |
|---|---|---|
enqueued | boolean | yes |
Same schema as POST /api/v1/mailboxes/{id}/sync.
400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
This operation accepts an Idempotency-Key header: replaying the same request with the same key returns the original response instead of acting twice. See Idempotency.
Error codes — mailbox.not_found, mailbox.resync_window_invalid, mailbox.not_active. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/mailboxes/{id}/removal-impact
What removing a mailbox would take away
For the confirmation dialog: messages, running executions and published workflows that trigger on or send from this mailbox.
Access — Member session or API key with scope mailboxes:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Responses
200 — The impact.
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
address | string | yes |
messages | integer | yes |
pendingExecutions | integer | yes |
workflows | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string" },
"address": { "type": "string" },
"messages": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"pendingExecutions": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"workflows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"link": { "type": "string", "enum": [ "trigger", "sender" ] }
},
"required": [ "id", "name", "link" ],
"additionalProperties": false
}
}
},
"required": [ "mailboxId", "address", "messages", "pendingExecutions", "workflows" ],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — mailbox.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
POST /api/v1/mailboxes/{id}/disconnect
Disconnect a mailbox
Synchronisation stops and the secret is removed; the mirrored history stays readable. Reversible (reconnecting reactivates the same mailbox), hence no confirmation. Idempotent.
Access — Member session or API key with scope mailboxes:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Responses
200 — The mailbox, disconnected.
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
address | string | yes |
disarmedWorkflows | integer | yes |
credential | "deleted" | "kept" | "none" | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string" },
"address": { "type": "string" },
"disarmedWorkflows": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"credential": { "type": "string", "enum": [ "deleted", "kept", "none" ] }
},
"required": [ "mailboxId", "address", "disarmedWorkflows", "credential" ],
"additionalProperties": false
}401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — mailbox.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
DELETE /api/v1/mailboxes/{id}
Delete a mailbox and its mirror
The most destructive gesture of the instance: messages, threads, attachments, journal and executions are purged, workflows sending from the mailbox are unpublished. The body must repeat the mailbox address in confirm (case-insensitive). Rows are gone when the response leaves; blobs are purged asynchronously.
Access — Member session or API key with scope mailboxes:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
confirm | string | yes |
JSON Schema
json
{
"type": "object",
"properties": { "confirm": { "type": "string", "minLength": 1, "maxLength": 320 } },
"required": [ "confirm" ]
}Responses
200 — What was deleted.
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
address | string | yes |
deleted | object | yes |
unpublishedWorkflows | integer | yes |
credential | "deleted" | "kept" | "none" | yes |
blobsQueued | boolean | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string" },
"address": { "type": "string" },
"deleted": {
"type": "object",
"properties": {
"messages": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"threads": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"attachments": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"journalEntries": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"outboundOps": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"executions": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }
},
"required": [
"messages",
"threads",
"attachments",
"journalEntries",
"outboundOps",
"executions"
],
"additionalProperties": false
},
"unpublishedWorkflows": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"credential": { "type": "string", "enum": [ "deleted", "kept", "none" ] },
"blobsQueued": { "type": "boolean" }
},
"required": [
"mailboxId",
"address",
"deleted",
"unpublishedWorkflows",
"credential",
"blobsQueued"
],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — mailbox.not_found, mailbox.confirmation_mismatch. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/journal
List the intake journal
Why did this message trigger nothing? One entry per received message with its outcome, newest first, cursor-paginated. mailboxId is intersected with the member’s mailboxes, never substituted; q is accepted and ignored.
Access — Member session or API key with scope mailboxes:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
cursor | query | string | no |
limit | query | integer | no |
q | query | string | no |
Responses
200 — A page of entries.
| Field | Type | Required |
|---|---|---|
entries | object[] | yes |
nextCursor | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"mailboxId": { "type": "string" },
"providerMessageId": { "type": "string" },
"outcome": {
"type": "string",
"enum": [
"excluded_org",
"excluded_member",
"guardrail",
"unmatched",
"dispatched"
]
},
"ruleRef": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"from": {
"anyOf": [
{
"type": "object",
"properties": { "name": { "type": "string" }, "email": { "type": "string" } },
"required": [ "email" ],
"additionalProperties": false
},
{ "type": "null" }
]
},
"messageId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"threadId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"executions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"workflowId": { "type": "string" },
"workflowName": { "type": "string" }
},
"required": [ "id", "workflowId", "workflowName" ],
"additionalProperties": false
}
},
"createdAt": { "type": "string" }
},
"required": [
"id",
"mailboxId",
"providerMessageId",
"outcome",
"ruleRef",
"subject",
"from",
"messageId",
"threadId",
"executions",
"createdAt"
],
"additionalProperties": false
}
},
"nextCursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "entries", "nextCursor" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.bad_request. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
GET /api/v1/journal/stats
Count journal entries by outcome
Received, excluded, processed… over the member’s mailboxes, or one of them.
Access — Member session or API key with scope mailboxes:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
cursor | query | string | no |
limit | query | integer | no |
q | query | string | no |
Responses
200 — The counters.
| Field | Type | Required |
|---|---|---|
byOutcome | object | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"byOutcome": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"excluded_org",
"excluded_member",
"guardrail",
"unmatched",
"dispatched"
]
},
"additionalProperties": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"required": [
"excluded_org",
"excluded_member",
"guardrail",
"unmatched",
"dispatched"
]
}
},
"required": [ "byOutcome" ],
"additionalProperties": false
}400 — The request does not match its schema.
401 — No valid session or API key (auth.unauthenticated).
403 — Refused: insufficient role (auth.forbidden), missing scope (api_key.scope_missing, details.required names it) or a route closed to API keys (api_key.session_required).
429 — The API key exceeded its rate limit (api_key.rate_limited); Retry-After says when to retry.
Error codes — request.bad_request. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.