English
Address book
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/contacts
List the address book
The merged view of the member: shared layer and personal layer side by side, each entry resolved into the profile a composed message would use. Filter by kind (address or domain), layer, q; resolve from the point of view of a mailboxId. Cursor-paginated.
Access — Member session or API key with scope contacts:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
kind | query | "contact" | "domain" | no |
q | query | string | no |
layer | query | "all" | "shared" | "member" | no |
mailboxId | query | string | no |
cursor | query | string | no |
limit | query | integer | no |
Responses
200 — A page of entries.
| Field | Type | Required |
|---|---|---|
items | object[] | yes |
nextCursor | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": { "type": "string", "enum": [ "contact", "domain" ] },
"value": { "type": "string", "minLength": 1, "maxLength": 320 },
"shared": {
"anyOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"status": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"orgNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" }
},
"required": [
"id",
"displayName",
"language",
"status",
"orgNotes",
"updatedAt"
],
"additionalProperties": false
},
{ "type": "null" }
]
},
"member": {
"anyOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"personalNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" }
},
"required": [
"id",
"formality",
"tone",
"language",
"signatureId",
"personalNotes",
"updatedAt"
],
"additionalProperties": false
},
{ "type": "null" }
]
},
"memberByMailbox": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"personalNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" },
"mailboxId": { "type": "string" }
},
"required": [
"id",
"formality",
"tone",
"language",
"signatureId",
"personalNotes",
"updatedAt",
"mailboxId"
],
"additionalProperties": false
}
},
"resolved": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureText": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"status": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"notes": { "type": "array", "items": { "type": "string" } }
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"signatureId",
"status",
"notes"
],
"additionalProperties": false
},
"sources": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"language": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"displayName": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"signatureText": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"status": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"notes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
}
}
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"status",
"notes"
],
"additionalProperties": false
}
},
"required": [
"kind",
"value",
"shared",
"member",
"memberByMailbox",
"resolved",
"sources"
],
"additionalProperties": false
}
},
"nextCursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "items", "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, contacts.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/contacts
Remove a card
Removes the card of kind/value in the given layer (default member). For the member layer, mailboxId targets the card of that mailbox; without it, only the global card goes and per-mailbox cards survive. The shared layer requires the administrator role (403).
Access — Member session or API key with scope contacts:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
kind | query | "contact" | "domain" | yes |
value | query | string | yes |
layer | query | "member" | "shared" | no |
mailboxId | query | string | no |
Responses
204 — Removed.
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, auth.forbidden, contacts.invalid_value, contacts.mailbox_not_found, contacts.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
PUT /api/v1/contacts/member
Save my card for a contact
Upserts the member’s relational card (formality, tone, language, signature, notes) for an address or a domain. With mailboxId, the card is specific to that mailbox and takes precedence over the global one. Only the fields present are written; null clears one. Returns the full merged entry.
Access — Member session or API key with scope contacts:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
kind | "contact" | "domain" | yes |
value | string | yes |
mailboxId | string | no |
formality | "tu" | "vous" | null | no |
tone | "formal" | "neutral" | "casual" | null | no |
language | string | null | no |
signatureId | string | null | no |
personalNotes | string | null | no |
JSON Schema
json
{
"type": "object",
"properties": {
"kind": { "type": "string", "enum": [ "contact", "domain" ] },
"value": { "type": "string", "minLength": 1, "maxLength": 320 },
"mailboxId": { "type": "string" },
"formality": {
"anyOf": [ { "type": "string", "enum": [ "tu", "vous" ] }, { "type": "null" } ]
},
"tone": {
"anyOf": [
{ "type": "string", "enum": [ "formal", "neutral", "casual" ] },
{ "type": "null" }
]
},
"language": {
"anyOf": [
{ "type": "string", "pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$" },
{ "type": "null" }
]
},
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"personalNotes": { "anyOf": [ { "type": "string", "maxLength": 4000 }, { "type": "null" } ] }
},
"required": [ "kind", "value" ]
}Responses
200 — The merged entry.
| Field | Type | Required |
|---|---|---|
entry | object | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"entry": {
"type": "object",
"properties": {
"kind": { "type": "string", "enum": [ "contact", "domain" ] },
"value": { "type": "string", "minLength": 1, "maxLength": 320 },
"shared": {
"anyOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"status": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"orgNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" }
},
"required": [
"id",
"displayName",
"language",
"status",
"orgNotes",
"updatedAt"
],
"additionalProperties": false
},
{ "type": "null" }
]
},
"member": {
"anyOf": [
{
"type": "object",
"properties": {
"id": { "type": "string" },
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"personalNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" }
},
"required": [
"id",
"formality",
"tone",
"language",
"signatureId",
"personalNotes",
"updatedAt"
],
"additionalProperties": false
},
{ "type": "null" }
]
},
"memberByMailbox": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"personalNotes": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"updatedAt": { "type": "string" },
"mailboxId": { "type": "string" }
},
"required": [
"id",
"formality",
"tone",
"language",
"signatureId",
"personalNotes",
"updatedAt",
"mailboxId"
],
"additionalProperties": false
}
},
"resolved": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{ "type": "string", "enum": [ "formal", "neutral", "casual" ] },
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureText": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"status": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"notes": { "type": "array", "items": { "type": "string" } }
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"signatureId",
"status",
"notes"
],
"additionalProperties": false
},
"sources": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"language": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"displayName": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"signatureText": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"status": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"notes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
}
}
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"status",
"notes"
],
"additionalProperties": false
}
},
"required": [
"kind",
"value",
"shared",
"member",
"memberByMailbox",
"resolved",
"sources"
],
"additionalProperties": false
}
},
"required": [ "entry" ],
"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, contacts.invalid_value, contacts.mailbox_not_found, contacts.unknown_signature. 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
{
"kind": "contact",
"value": "bob@example.test",
"formality": "tu",
"tone": "casual",
"language": "fr"
}DELETE /api/v1/contacts/member
Remove my card for a contact
The historical alias of DELETE /api/v1/contacts with layer forced to member. Same handler, same rules.
Access — Member session or API key with scope contacts:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
kind | query | "contact" | "domain" | yes |
value | query | string | yes |
layer | query | "member" | "shared" | no |
mailboxId | query | string | no |
Responses
204 — Removed.
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, contacts.invalid_value, contacts.mailbox_not_found, contacts.not_found. The common codes (request.bad_request, auth.unauthenticated, api_key.scope_missing, api_key.rate_limited…) apply to every route; see Errors.
PUT /api/v1/contacts/shared
Save the shared card of a contact
Upserts the organisation’s card (display name, language, status, notes) for an address or a domain. Administrators only. Returns the entry as seen by the calling administrator.
Access — Member session or API key with scope admin:write (administrator role required).
Request body (application/json)
| Field | Type | Required |
|---|---|---|
kind | "contact" | "domain" | yes |
value | string | yes |
displayName | string | null | no |
language | string | null | no |
status | string | null | no |
orgNotes | string | null | no |
JSON Schema
json
{
"type": "object",
"properties": {
"kind": { "type": "string", "enum": [ "contact", "domain" ] },
"value": { "type": "string", "minLength": 1, "maxLength": 320 },
"displayName": { "anyOf": [ { "type": "string", "maxLength": 200 }, { "type": "null" } ] },
"language": {
"anyOf": [
{ "type": "string", "pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$" },
{ "type": "null" }
]
},
"status": { "anyOf": [ { "type": "string", "maxLength": 40 }, { "type": "null" } ] },
"orgNotes": { "anyOf": [ { "type": "string", "maxLength": 4000 }, { "type": "null" } ] }
},
"required": [ "kind", "value" ]
}Responses
200 — The merged entry.
| Field | Type | Required |
|---|---|---|
entry | object | yes |
Same schema as PUT /api/v1/contacts/member.
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, contacts.invalid_value. 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/contacts/shared
Remove the shared card of a contact
Administrators only. Personal cards of the members are untouched.
Access — Member session or API key with scope admin:write (administrator role required).
Parameters
| Name | In | Type | Required |
|---|---|---|---|
kind | query | "contact" | "domain" | yes |
value | query | string | yes |
layer | query | "member" | "shared" | no |
mailboxId | query | string | no |
Responses
204 — Removed.
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, contacts.invalid_value, contacts.not_found. 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/contacts/defaults
Read my profile defaults
What a message uses when no card says otherwise: the member’s defaults, or those of one of their mailboxes with mailboxId. The organisation’s defaults are read elsewhere by administrators.
Access — Member session or API key with scope contacts:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
Responses
200 — The defaults.
| Field | Type | Required |
|---|---|---|
defaults | object | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"defaults": {
"type": "object",
"properties": {
"formality": {
"anyOf": [ { "type": "string", "enum": [ "tu", "vous" ] }, { "type": "null" } ]
},
"tone": {
"anyOf": [
{ "type": "string", "enum": [ "formal", "neutral", "casual" ] },
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "formality", "tone", "language", "signatureId" ],
"additionalProperties": false
}
},
"required": [ "defaults" ],
"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, contacts.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.
PUT /api/v1/contacts/defaults
Update my profile defaults
Partial: only the fields present are written, null clears one. With mailboxId, the defaults of that mailbox.
Access — Member session or API key with scope contacts:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
formality | "tu" | "vous" | null | no |
tone | "formal" | "neutral" | "casual" | null | no |
language | string | null | no |
signatureId | string | null | no |
JSON Schema
json
{
"type": "object",
"properties": {
"formality": {
"anyOf": [ { "type": "string", "enum": [ "tu", "vous" ] }, { "type": "null" } ]
},
"tone": {
"anyOf": [
{ "type": "string", "enum": [ "formal", "neutral", "casual" ] },
{ "type": "null" }
]
},
"language": {
"anyOf": [
{ "type": "string", "pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$" },
{ "type": "null" }
]
},
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
}
}Responses
200 — The defaults, updated.
| Field | Type | Required |
|---|---|---|
defaults | object | yes |
Same schema as GET /api/v1/contacts/defaults.
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, contacts.mailbox_not_found, contacts.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/contacts/resolve
Resolve recipient profiles
Exactly what a composed message would see for each address: the resolved profile and where each field came from. A POST because a batch of addresses does not belong in a URL. mailboxId is the sending mailbox; without it, the member’s global defaults apply.
Access — Member session or API key with scope contacts:read.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
emails | string[] | yes |
mailboxId | string | no |
JSON Schema
json
{
"type": "object",
"properties": {
"emails": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 320 }
},
"mailboxId": { "type": "string" }
},
"required": [ "emails" ]
}Responses
200 — The profiles and their sources, by address.
| Field | Type | Required |
|---|---|---|
profiles | object | yes |
sources | object | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"profiles": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{ "type": "string", "enum": [ "tu", "vous" ] },
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{ "type": "string", "enum": [ "formal", "neutral", "casual" ] },
{ "type": "null" }
]
},
"language": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureText": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"signatureId": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"status": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"notes": { "type": "array", "items": { "type": "string" } }
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"signatureId",
"status",
"notes"
],
"additionalProperties": false
}
},
"sources": {
"type": "object",
"propertyNames": { "type": "string" },
"additionalProperties": {
"type": "object",
"properties": {
"formality": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"tone": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"language": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"displayName": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"signatureText": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"status": {
"anyOf": [
{
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
},
{ "type": "null" }
]
},
"notes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"org_default",
"shared_domain",
"shared_contact",
"member_default",
"member_mailbox_default",
"member_domain",
"member_contact",
"member_mailbox_domain",
"member_mailbox_contact"
]
}
}
},
"required": [
"formality",
"tone",
"language",
"displayName",
"signatureText",
"status",
"notes"
],
"additionalProperties": false
}
}
},
"required": [ "profiles", "sources" ],
"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, contacts.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/contacts/import/preview
Preview a CSV import
Reads the CSV, detects the delimiter, proposes a column mapping and returns sample rows. Changes nothing. A POST because the file is in the body.
Access — Member session or API key with scope contacts:read.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
csv | string | yes |
delimiter | "," | ";" | "\t" | "auto" | no |
JSON Schema
json
{
"type": "object",
"properties": {
"csv": { "type": "string", "minLength": 1, "maxLength": 2097152 },
"delimiter": { "default": "auto", "type": "string", "enum": [ ",", ";", "\t", "auto" ] }
},
"required": [ "csv" ]
}Responses
200 — The proposed mapping and samples.
| Field | Type | Required |
|---|---|---|
columns | string[] | yes |
sample | string[][] | yes |
suggestedMapping | object | yes |
rowCount | integer | yes |
delimiter | "," | ";" | "\t" | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"columns": { "type": "array", "items": { "type": "string" } },
"sample": {
"type": "array",
"items": { "type": "array", "items": { "type": "string" } }
},
"suggestedMapping": {
"type": "object",
"properties": {
"email": { "type": "string", "minLength": 1 },
"displayName": { "type": "string", "minLength": 1 },
"formality": { "type": "string", "minLength": 1 },
"tone": { "type": "string", "minLength": 1 },
"language": { "type": "string", "minLength": 1 },
"notes": { "type": "string", "minLength": 1 }
},
"additionalProperties": false
},
"rowCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"delimiter": { "type": "string", "enum": [ ",", ";", "\t" ] }
},
"required": [ "columns", "sample", "suggestedMapping", "rowCount", "delimiter" ],
"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, contacts.invalid_csv. 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/contacts/import
Import contacts from a CSV
Writes the rows into the member layer (optionally for one mailbox) or, for administrators, into the shared layer. Existing cards are updated or skipped according to onConflict; the response counts what was created, updated and skipped, with the reason per skipped row.
Access — Member session or API key with scope contacts:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
csv | string | yes |
delimiter | "," | ";" | "\t" | "auto" | no |
mapping | object | yes |
layer | "member" | "shared" | yes |
mailboxId | string | no |
onConflict | "skip" | "update" | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"csv": { "type": "string", "minLength": 1, "maxLength": 2097152 },
"delimiter": { "default": "auto", "type": "string", "enum": [ ",", ";", "\t", "auto" ] },
"mapping": {
"type": "object",
"properties": {
"email": { "type": "string", "minLength": 1 },
"displayName": { "type": "string", "minLength": 1 },
"formality": { "type": "string", "minLength": 1 },
"tone": { "type": "string", "minLength": 1 },
"language": { "type": "string", "minLength": 1 },
"notes": { "type": "string", "minLength": 1 }
},
"required": [ "email" ]
},
"layer": { "type": "string", "enum": [ "member", "shared" ] },
"mailboxId": { "type": "string" },
"onConflict": { "type": "string", "enum": [ "skip", "update" ] }
},
"required": [ "csv", "mapping", "layer", "onConflict" ]
}Responses
200 — The import report.
| Field | Type | Required |
|---|---|---|
created | integer | yes |
updated | integer | yes |
skipped | object[] | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"created": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"updated": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"row": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"reason": {
"type": "string",
"enum": [ "missing_email", "invalid_email", "duplicate" ]
}
},
"required": [ "row", "reason" ],
"additionalProperties": false
}
}
},
"required": [ "created", "updated", "skipped" ],
"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, auth.forbidden, contacts.mailbox_not_found, contacts.invalid_csv, contacts.unknown_column. 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/contacts/enrichment/runs
Start an enrichment run
Reads the sent mail of one mailbox over lookbackDays and asks a model to propose cards for the most frequent correspondents. Queued, not done: 202 with the run id; poll its state, then read the suggestions.
Access — Member session or API key with scope contacts:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
lookbackDays | integer | no |
maxContacts | integer | no |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string", "minLength": 1 },
"lookbackDays": { "default": 365, "type": "integer", "minimum": 1, "maximum": 730 },
"maxContacts": { "default": 50, "type": "integer", "minimum": 1, "maximum": 200 }
},
"required": [ "mailboxId" ]
}Responses
202 — The run, queued.
| Field | Type | Required |
|---|---|---|
runId | string | yes |
JSON Schema
json
{
"type": "object",
"properties": { "runId": { "type": "string" } },
"required": [ "runId" ],
"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, contacts.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.
GET /api/v1/contacts/enrichment/runs/{id}
Read an enrichment run
Its status and counters. A run of another member is a 404.
Access — Member session or API key with scope contacts:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Responses
200 — The run.
| Field | Type | Required |
|---|---|---|
id | string | yes |
mailboxId | string | yes |
status | "pending" | "running" | "done" | "failed" | yes |
startedAt | string | null | yes |
finishedAt | string | null | yes |
summary | object | yes |
error | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"mailboxId": { "type": "string" },
"status": { "type": "string", "enum": [ "pending", "running", "done", "failed" ] },
"startedAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"finishedAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"summary": {
"type": "object",
"properties": {
"contacts": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"withProposal": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"llmSkipped": { "type": "boolean" }
},
"required": [ "contacts", "withProposal", "llmSkipped" ],
"additionalProperties": false
},
"error": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "id", "mailboxId", "status", "startedAt", "finishedAt", "summary", "error" ],
"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 — contacts.enrichment_run_not_found. 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/contacts/suggestions
List contact suggestions
What enrichment proposed, most frequent correspondents first, cursor-paginated. status defaults to pending. Each item says whether the address already has a card, so the screen does not propose a duplicate.
Access — Member session or API key with scope contacts:read.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
mailboxId | query | string | no |
status | query | "pending" | "accepted" | "rejected" | no |
cursor | query | string | no |
limit | query | integer | no |
Responses
200 — A page of suggestions.
| Field | Type | Required |
|---|---|---|
items | object[] | yes |
nextCursor | string | null | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"mailboxId": { "type": "string" },
"email": { "type": "string" },
"displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ] },
"evidence": {
"type": "object",
"properties": {
"sentCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"receivedCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
"lastAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "sentCount", "receivedCount", "lastAt" ],
"additionalProperties": false
},
"proposal": {
"anyOf": [
{
"type": "object",
"properties": {
"formality": { "type": "string", "enum": [ "tu", "vous" ] },
"tone": {
"type": "string",
"enum": [ "formal", "neutral", "casual" ]
},
"language": { "type": "string" },
"notes": { "type": "string" }
},
"additionalProperties": false
},
{ "type": "null" }
]
},
"confidence": {
"anyOf": [
{ "type": "number", "minimum": 0, "maximum": 1 },
{ "type": "null" }
]
},
"existing": {
"type": "object",
"properties": {
"layer": {
"anyOf": [
{ "type": "string", "enum": [ "member", "shared" ] },
{ "type": "null" }
]
}
},
"required": [ "layer" ],
"additionalProperties": false
},
"status": { "type": "string", "enum": [ "pending", "accepted", "rejected" ] },
"createdAt": { "type": "string" }
},
"required": [
"id",
"mailboxId",
"email",
"displayName",
"evidence",
"proposal",
"confidence",
"existing",
"status",
"createdAt"
],
"additionalProperties": false
}
},
"nextCursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }
},
"required": [ "items", "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, contacts.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/contacts/suggestions/accept-all
Accept every pending suggestion
Every pending suggestion of the mailbox becomes a card, as proposed, in the mailbox or member scope. Suggestions decided meanwhile are skipped; accepted counts those that actually switched.
Access — Member session or API key with scope contacts:write.
Request body (application/json)
| Field | Type | Required |
|---|---|---|
mailboxId | string | yes |
scope | "mailbox" | "member" | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"mailboxId": { "type": "string", "minLength": 1 },
"scope": { "type": "string", "enum": [ "mailbox", "member" ] }
},
"required": [ "mailboxId", "scope" ]
}Responses
200 — How many were accepted.
| Field | Type | Required |
|---|---|---|
accepted | integer | yes |
JSON Schema
json
{
"type": "object",
"properties": {
"accepted": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }
},
"required": [ "accepted" ],
"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, contacts.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/contacts/suggestions/{id}/accept
Accept a suggestion
The suggestion becomes a card of the member, with the proposed values or the edits given, in the mailbox or member scope. Decided once: a suggestion already accepted or rejected is a 404.
Access — Member session or API key with scope contacts:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required |
|---|---|---|
scope | "mailbox" | "member" | yes |
edits | object | no |
JSON Schema
json
{
"type": "object",
"properties": {
"scope": { "type": "string", "enum": [ "mailbox", "member" ] },
"edits": {
"type": "object",
"properties": {
"displayName": { "type": "string", "maxLength": 200 },
"formality": { "type": "string", "enum": [ "tu", "vous" ] },
"tone": { "type": "string", "enum": [ "formal", "neutral", "casual" ] },
"language": { "type": "string", "pattern": "^[a-z]{2}(-[A-Za-z0-9]{2,8})?$" },
"notes": { "type": "string", "maxLength": 4000 }
}
}
},
"required": [ "scope" ]
}Responses
200 — The merged entry.
| Field | Type | Required |
|---|---|---|
entry | object | yes |
Same schema as PUT /api/v1/contacts/member.
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, contacts.suggestion_not_found, contacts.mailbox_not_found, contacts.invalid_value, contacts.unknown_signature. 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/contacts/suggestions/{id}/reject
Reject a suggestion
The address will not be proposed again by later runs.
Access — Member session or API key with scope contacts:write.
Parameters
| Name | In | Type | Required |
|---|---|---|---|
id | path | string | yes |
Responses
204 — Rejected.
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 — contacts.suggestion_not_found. 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/admin/contacts/defaults
Read the organisation writing defaults
Formality, tone, language and signature applied when neither the contact nor the member says otherwise.
Access — Member session or API key with scope admin:read (administrator role required).
Responses
200 — The defaults.
| Field | Type | Required |
|---|---|---|
defaults | object | yes |
Same schema as GET /api/v1/contacts/defaults.
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.
PUT /api/v1/admin/contacts/defaults
Set the organisation writing defaults
Three states per field: absent (unchanged), null (cleared), a value.
Access — Member session or API key with scope admin:write (administrator role required).
Request body (application/json)
| Field | Type | Required |
|---|---|---|
formality | "tu" | "vous" | null | no |
tone | "formal" | "neutral" | "casual" | null | no |
language | string | null | no |
signatureId | string | null | no |
Same schema as PUT /api/v1/contacts/defaults.
Responses
200 — The defaults, after write.
| Field | Type | Required |
|---|---|---|
defaults | object | yes |
Same schema as GET /api/v1/contacts/defaults.
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.