Nolmo API 1.0.0
Another system works with a Nolmo workspace the way a person does, within what an admin lets its key do. Make a key in Settings, API keys, and send it as Authorization: Bearer <key>. Start with the guides.
Authentication. Make a key in Settings, API keys, and send it as Authorization: Bearer <key>. A key has scopes (cases:read, cases:write, cases:reply, contacts:read, contacts:write, webhooks:manage, knowledge:read, knowledge:write) and sees only the groups it was given (articles belong to the workspace, not to a group). Keep the key on a server; anyone who has it can do what it may.
Errors. Every error is JSON: {"error": {"code": "...", "message": "...", "details": null}}. Branch on code, show message to a developer, never to a customer.
Limits. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Past the limit the answer is 429 with Retry-After. Send an Idempotency-Key on every POST so a retry writes once.
Versions. Within /v1 fields and events are only ever added, so ignore the fields and events you do not know. A removal, a rename or a change of meaning is /v2, announced six months before and served beside /v1 for twelve. The machine-readable document is /v1/openapi.json.
Guides
Account
GET /v1/me
Who is calling
The key's name, workspace, scopes and groups. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
200 Successful Response
Body application/json: Me
{
"all_groups": false,
"groups": [
{
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f8",
"name": "Support"
}
],
"key": {
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "Contact form",
"prefix": "nolmo_live_k3J9fQ2a"
},
"scopes": [
"cases:read",
"cases:write"
],
"tenant": {
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f7",
"name": "Furu & Lin",
"slug": "furu-lin"
}
}| Field | Type | About | |
|---|---|---|---|
key | MeKey | required | |
tenant | MeTenant | required | |
scopes | list of cases:read | cases:write | cases:reply | contacts:read | contacts:write | webhooks:manage | knowledge:read | knowledge:write | required | What the key may do |
all_groups | boolean | required | The key sees every group's cases |
groups | list of MeGroup | required | The groups it sees; empty with |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Cases
GET /v1/cases/search
Search cases
Find cases by what was said in them, from the cases the key sees. A case in a group the key does not see is never found, with or without a filter.
This is the app's own search, so the inbox and the API find the same cases. Searching the team's internal notes too is opt-in with include_notes. Page with after, as with GET /v1/cases.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
q | query | string | required | What to look for: words from the subject or a message, or a case number e.g. cracked tiles |
status | query | list of new | open | pending | hold | solved | closed or null | Only cases in these statuses | |
priority | query | list of low | normal | high | urgent or null | Only cases with these priorities | |
type | query | list of string or null | Only cases of these types, by key ( e.g. ['return'] | |
group_id | query | list of string or null | Only cases in these groups ( e.g. ['0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6'] | |
assignee_id | query | list of string or null | Only cases assigned to these people ( e.g. ['0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6'] | |
tag | query | list of string or null | Only cases with every one of these tags e.g. ['delivery'] | |
channel | query | list of string or null | Only cases that arrived on these channels e.g. ['email', 'api'] | |
created_from | query | string (date-time) or null | Created at or after this time e.g. 2026-11-01T00:00:00Z | |
created_to | query | string (date-time) or null | Created before this time e.g. 2026-12-01T00:00:00Z | |
updated_from | query | string (date-time) or null | Updated at or after this time e.g. 2026-11-01T00:00:00Z | |
updated_to | query | string (date-time) or null | Updated before this time e.g. 2026-12-01T00:00:00Z | |
include_notes | query | boolean | Also count the team's internal notes, which are left out unless you ask for them | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CaseList
{
"items": [
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Case | required | |
next_cursor | string or null | required | Send it as |
400 A filter is not an id, or the cursor is not one this API issued
Body application/json: ErrorResponse
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/cases/{number}/attachments
List a case's files
The files on the case's messages, oldest first, a page at a time. Files on the team's internal notes are left out unless include_notes is true. Download one with GET /v1/attachments/{id}.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
include_notes | query | boolean | Also count the team's internal notes, which are left out unless you ask for them | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | About how many files to return; a message's files stay together |
200 Successful Response
Body application/json: CaseFileList
{
"items": [
{
"content_type": "image/jpeg",
"created_at": "2026-11-09T10:30:00Z",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"message_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "damaged-tiles.jpg",
"on": "message",
"size": 183204
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of CaseFile | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
404 No such case or file, or one in a group the key does not see
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/attachments/{attachment_id}
Download a file
The file, or a redirect (307) to a signed address that lives for a few minutes and carries no key; follow it without sending your Authorization header on. A file on a case the key does not see, a file on an internal note (unless include_notes is true) and an id that does not exist are the same 404.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
attachment_id | path | string | required | The file's e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
include_notes | query | boolean | Also count the team's internal notes, which are left out unless you ask for them |
200 The file itself
Body application/octet-stream: string (binary)
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
404 No such case or file, or one in a group the key does not see
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
307 A signed URL that works for a few minutes; follow it
GET /v1/cases/{number}/events
List a case's events
What happened to the case, oldest first, a page at a time: when it was made, each reply, and every change of status, assignee, priority or type, with who did it. A person is a first name, a key is its name.
The kinds are created, customer_replied, team_replied, status_changed, reopened, assigned, priority_changed and type_changed. Other kinds may be added later; ignore the ones you do not know. Nothing about the workspace's clocks, the AI's work or the team's notes is part of it.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CaseEventList
{
"items": [
{
"actor": {
"name": "Astrid",
"type": "team"
},
"at": "2026-11-09T11:02:00Z",
"from": "open",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "status_changed",
"to": "solved"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of CaseEvent | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
404 No such case or file, or one in a group the key does not see
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/cases
List or find cases
The cases the key sees, newest first. A case in a group the key does not see is not listed, and not found by its external_ref either.
To find the case you made, send its external_ref (and the source you made it under, if you gave one).
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
external_ref | query | string or null | Your own id for the case: finds the case you made with it e.g. order-12345 | |
source | query | string or null | Only cases made under this e.g. Returns page | |
status | query | list of new | open | pending | hold | solved | closed or null | Only cases in these statuses | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CaseList
{
"items": [
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Case | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/cases
Create a case
Create a case for a person, with what they wrote as its first message.
The person is found by email, then phone, then an id in external_ids, and made when none is known. The case is routed by the workspace's rules, gets its clocks and is assessed, exactly as a mail would. It arrives on the channel api, and its timeline says which key made it.
Send an Idempotency-Key so that a retry makes no second case. An external_ref finds the case again (GET /v1/cases?external_ref=); a second create with the same external_ref and source is 409 external_ref_taken and names the case that has it.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: CaseIn
{
"contact": {
"email": "kari@example.no",
"name": "Kari Kunde"
},
"external_ref": "order-12345",
"message": {
"body_text": "Three of the tiles in the pack were cracked. Order 12345."
},
"priority": "normal",
"source": "Returns page",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
]
}| Field | Type | About | |
|---|---|---|---|
contact | ContactIn | required | |
subject | string | required | The case's title |
message | FirstMessageIn | required | |
priority | low | normal | high | urgent |
| |
type | string or null | One of the workspace's case types, by its key | |
tags | list of string | Labels to put on the case | |
brand_id | string (uuid) or null | The brand or store the case belongs to | |
group_id | string (uuid) or null | The group the case goes to. It must be one of the key's groups ( | |
custom_fields | object | Values for the workspace's case fields, by the field's key | |
external_ref | string or null | Your own id for the case. A second create with the same id from the same | |
source | string or null | What you call your system, such as |
201 Successful Response
Body application/json: Case
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}| Field | Type | About | |
|---|---|---|---|
number | integer | required | The case's number in the workspace; the id in the paths |
subject | string | required | |
status | new | open | pending | hold | solved | closed | required |
|
priority | low | normal | high | urgent | required | |
type | string or null | required | The case type's key |
tags | list of string | required | |
custom_fields | object | required | |
external_ref | string or null | required | Your own id for the case, if it has one |
source | string or null | required | The |
channel | string | required | How the case arrived: |
group_id | string (uuid) or null | required | |
brand_id | string (uuid) or null | required | |
assignee_first_name | string or null | required | The first name of the person who has the case |
contact | Contact | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
solved_at | string (date-time) or null | required | |
last_message_at | string (date-time) or null | required | |
version | integer | required | Send it back in |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
409 The case has changed, is closed, or the `external_ref` is taken
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/cases/{number}
Get a case
One case by its number. A case in a group the key does not see is a 404.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
200 Successful Response
Body application/json: Case
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}| Field | Type | About | |
|---|---|---|---|
number | integer | required | The case's number in the workspace; the id in the paths |
subject | string | required | |
status | new | open | pending | hold | solved | closed | required |
|
priority | low | normal | high | urgent | required | |
type | string or null | required | The case type's key |
tags | list of string | required | |
custom_fields | object | required | |
external_ref | string or null | required | Your own id for the case, if it has one |
source | string or null | required | The |
channel | string | required | How the case arrived: |
group_id | string (uuid) or null | required | |
brand_id | string (uuid) or null | required | |
assignee_first_name | string or null | required | The first name of the person who has the case |
contact | Contact | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
solved_at | string (date-time) or null | required | |
last_message_at | string (date-time) or null | required | |
version | integer | required | Send it back in |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
404 No such case, or one in a group the key does not see
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
PATCH /v1/cases/{number}
Change a case
Change a case's tags, priority, custom fields or external_ref. Status and assignment are the team's, and sending them is a 422. Send the version you read: if the case has moved on the answer is 409 case_changed, and nothing is written.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
Request body application/json: CasePatchIn
{
"priority": "high",
"tags": [
"delivery",
"refund"
],
"version": 3
}| Field | Type | About | |
|---|---|---|---|
tags | list of string or null | Replaces the case's tags | |
priority | low | normal | high | urgent or null |
| |
custom_fields | object or null | Values for the case fields, merged key by key | |
external_ref | string or null | Your own id for the case; | |
version | integer or null | The |
200 Successful Response
Body application/json: Case
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}| Field | Type | About | |
|---|---|---|---|
number | integer | required | The case's number in the workspace; the id in the paths |
subject | string | required | |
status | new | open | pending | hold | solved | closed | required |
|
priority | low | normal | high | urgent | required | |
type | string or null | required | The case type's key |
tags | list of string | required | |
custom_fields | object | required | |
external_ref | string or null | required | Your own id for the case, if it has one |
source | string or null | required | The |
channel | string | required | How the case arrived: |
group_id | string (uuid) or null | required | |
brand_id | string (uuid) or null | required | |
assignee_first_name | string or null | required | The first name of the person who has the case |
contact | Contact | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
solved_at | string (date-time) or null | required | |
last_message_at | string (date-time) or null | required | |
version | integer | required | Send it back in |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
404 No such case, or one in a group the key does not see
Body application/json: ErrorResponse
409 The case has changed, is closed, or the `external_ref` is taken
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/cases/{number}/messages
List a case's messages
The case's public thread, oldest first, a page at a time. The team's internal notes are not part of it unless include_notes is true; a side conversation never is.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
include_notes | query | boolean | Also the team's internal notes, which are left out unless you ask | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: MessageList
{
"items": [
{
"attachments": [],
"author": {
"name": "Astrid",
"type": "team"
},
"body_text": "Hi Kari, we are sending new tiles today.",
"created_at": "2026-11-09T10:30:00Z",
"delivery": "sent",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "reply"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Message | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
404 No such case, or one in a group the key does not see
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/cases/{number}/messages
Add a message to a case
Add to a case. kind says what it is:
customer: a message from the person the case is about. It reopens a solved case, as a mail would, and is refused (409case_closed) on a closed one.note: internal, for the team, authored by the key. It is never part of the public thread.reply: a public answer to the customer, sent through the case's channel as a team reply is (by email for an email case), authored by the key. It needs the scopecases:reply; without it the answer is 403 and nothing is written. The key does not become the case's assignee. If nothing can carry the answer (no address, no channel) it is 409no_way_to_answerand nothing is written.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
number | path | integer | required | The case's number in the workspace e.g. 1042 |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: MessageIn
{
"body_text": "Any news on this? I still have not heard anything.",
"kind": "customer"
}| Field | Type | About | |
|---|---|---|---|
kind | customer | note | reply | required |
|
body_text | string | required | |
body_html | string or null | Cleaned before it is stored, as in a new case | |
attachments | list of string | Ids from |
201 Successful Response
Body application/json: Message
{
"attachments": [],
"author": {
"name": "Astrid",
"type": "team"
},
"body_text": "Hi Kari, we are sending new tiles today.",
"created_at": "2026-11-09T10:30:00Z",
"delivery": "sent",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "reply"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
kind | customer | note | reply | required |
|
author | Author | required | |
body_text | string | required | |
body_html | string or null | required | |
attachments | list of Attachment | required | |
delivery | queued | sent | failed or null | required | For a reply that leaves by email: where it is. Null for everything else |
created_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
404 No such case, or one in a group the key does not see
Body application/json: ErrorResponse
409 The case has changed, is closed, or the `external_ref` is taken
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/uploads
Upload a file
Upload a file as multipart/form-data (the part is called file), then name its id in attachments of a new case or of a message. The same kinds of file and the same size limit as in the app apply (pictures, documents, archives, mail, text; not pages or scripts). A file is yours until a message uses it, and only you can use it; one that is never used is deleted after a day.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body multipart/form-data: Body_upload_file_v1_uploads_post
| Field | Type | About | |
|---|---|---|---|
file | string | required |
201 Successful Response
Body application/json: Upload
{
"content_type": "image/jpeg",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "damaged-tiles.jpg",
"size": 183204
}| Field | Type | About | |
|---|---|---|---|
id | string | required | Name it in |
name | string | required | |
content_type | string | required | |
size | integer | required | Bytes |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope, or the group is not one of the key's
Body application/json: ErrorResponse
413 The file is larger than the limit
Body application/json: ErrorResponse
415 That kind of file cannot be attached
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Contacts
GET /v1/contacts
List or find contacts
The workspace's contacts, by name, a page at a time. Every filter narrows the list; email, phone and external_system with external_id are exact matches, so they find the one person you know. A key sees every contact, whatever its groups; the counts are of the cases it sees.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
q | query | string or null | Words to search for in the name, email address, phone number and customer number | |
email | query | string or null | Exactly this email address e.g. kari@example.no | |
phone | query | string or null | Exactly this phone number, compared as digits e.g. +47 900 00 000 | |
external_system | query | string or null | With e.g. shop | |
external_id | query | string or null | With e.g. C-1042 | |
organization_id | query | string (uuid) or null | Only people in this company | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CustomerList
{
"items": [
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Customer | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/contacts
Create a contact
Make a contact. A person is never made twice: when the email address, the phone number (as digits) or an id in external_ids already belongs to a contact, nothing is written and the answer is 409 contact_exists with that contact's id in details.contact_id. Look the person up first with GET /v1/contacts, or just create and read the 409. A person with an address on a company's domain joins that company, as for a mail.
Send an Idempotency-Key so that a retry makes nothing twice.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: CustomerIn
{
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"language": "nb",
"name": "Kari Kunde",
"phone": "+47 900 00 000"
}| Field | Type | About | |
|---|---|---|---|
name | string | required | Their name |
email | string (email) or null | Their email address | |
phone | string or null | Their phone number | |
language | string or null |
| |
customer_no | string or null | ||
member_no | string or null | ||
organization_id | string (uuid) or null | The company they belong to. Left out, a company with their email's domain is used | |
external_ids | map of string | Their ids in your own systems, by the system's name: |
201 Successful Response
Body application/json: Customer
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | Lowercased. Unique in the workspace |
phone | string or null | required | Digits with a leading |
language | string or null | required |
|
customer_no | string or null | required | Their customer number |
member_no | string or null | required | Their member number |
external_ids | map of string | required | Their ids in your own systems, by the system's name: |
organization_id | string (uuid) or null | required | The company they belong to |
organization_name | string or null | required | |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
409 The email, phone number or external id is held by another, or the name is taken; `details` names the existing one
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/contacts/{contact_id}
Get a contact
One contact by id, with the counts of their cases the key sees.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
contact_id | path | string (uuid) | required | The contact's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
200 Successful Response
Body application/json: Customer
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | Lowercased. Unique in the workspace |
phone | string or null | required | Digits with a leading |
language | string or null | required |
|
customer_no | string or null | required | Their customer number |
member_no | string or null | required | Their member number |
external_ids | map of string | required | Their ids in your own systems, by the system's name: |
organization_id | string (uuid) or null | required | The company they belong to |
organization_name | string or null | required | |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
PATCH /v1/contacts/{contact_id}
Change a contact
Change the fields you send; the rest are not touched. external_ids is merged key by key (a system's id is set, or forgotten with null). Giving the contact an email address, phone number or external id that another contact holds is 409 contact_exists with that contact's id, and nothing is changed.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
contact_id | path | string (uuid) | required | The contact's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
Request body application/json: CustomerPatchIn
{
"external_ids": {
"shop": "C-2000"
},
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"phone": "+47 911 22 333"
}| Field | Type | About | |
|---|---|---|---|
name | string or null | Never null | |
email | string (email) or null | Another email address; | |
phone | string or null | ||
language | string or null | ||
customer_no | string or null | ||
member_no | string or null | ||
organization_id | string (uuid) or null |
| |
external_ids | map of string or null | Merged key by key: a system's id is set, or forgotten with |
200 Successful Response
Body application/json: Customer
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | Lowercased. Unique in the workspace |
phone | string or null | required | Digits with a leading |
language | string or null | required |
|
customer_no | string or null | required | Their customer number |
member_no | string or null | required | Their member number |
external_ids | map of string | required | Their ids in your own systems, by the system's name: |
organization_id | string (uuid) or null | required | The company they belong to |
organization_name | string or null | required | |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
409 The email, phone number or external id is held by another, or the name is taken; `details` names the existing one
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/contacts/{contact_id}/cases
List a contact's cases
The cases this person is the customer of, newest first, in the same shape as GET /v1/cases. It needs the scope cases:read besides contacts:read, and a case in a group the key does not see is not listed.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
contact_id | path | string (uuid) | required | The contact's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CaseList
{
"items": [
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Case | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/organizations
List or find companies
The workspace's companies, by name, a page at a time. external_system with external_id finds the one you know by its id in your own system.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
q | query | string or null | Words in the company's name e.g. fliser | |
external_system | query | string or null | With e.g. shop | |
external_id | query | string or null | With e.g. C-1042 | |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: OrganizationList
{
"items": [
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Organization | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/organizations
Create a company
Make a company. One with the same name, or the same id in external_ids, is not made twice: nothing is written and the answer is 409 organization_exists with the existing company's id in details.organization_id. Send an Idempotency-Key so that a retry makes nothing twice.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: OrganizationIn
{
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"name": "Fliser AS"
}| Field | Type | About | |
|---|---|---|---|
name | string | required | |
domains | list of string | Email domains; a new contact with an address there joins the company | |
external_ids | map of string | Its ids in your own systems, by the system's name |
201 Successful Response
Body application/json: Organization
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
domains | list of string | required | Email domains whose people belong here, lowercased |
external_ids | map of string | required | Its ids in your own systems, by the system's name |
people | integer | required | How many contacts belong to it |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
409 The email, phone number or external id is held by another, or the name is taken; `details` names the existing one
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/organizations/{organization_id}
Get a company
One company by id, with how many people belong to it and the counts of its cases the key sees.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
organization_id | path | string (uuid) | required | The company's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9 |
200 Successful Response
Body application/json: Organization
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
domains | list of string | required | Email domains whose people belong here, lowercased |
external_ids | map of string | required | Its ids in your own systems, by the system's name |
people | integer | required | How many contacts belong to it |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
PATCH /v1/organizations/{organization_id}
Change a company
Change the fields you send; the rest are not touched. domains replaces the list; external_ids is merged key by key. A name or an external id that another company has is 409 organization_exists with that company's id, and nothing is changed.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
organization_id | path | string (uuid) | required | The company's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9 |
Request body application/json: OrganizationPatchIn
{
"domains": [
"fliser.no",
"fliser.com"
],
"external_ids": {}
}| Field | Type | About | |
|---|---|---|---|
name | string or null | Never null | |
domains | list of string or null | Replaces the domains | |
external_ids | map of string or null | Merged key by key: set, or forgotten with |
200 Successful Response
Body application/json: Organization
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
domains | list of string | required | Email domains whose people belong here, lowercased |
external_ids | map of string | required | Its ids in your own systems, by the system's name |
people | integer | required | How many contacts belong to it |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
409 The email, phone number or external id is held by another, or the name is taken; `details` names the existing one
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/organizations/{organization_id}/contacts
List a company's contacts
The people who belong to the company, by name, a page at a time, in the shape of GET /v1/contacts.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
organization_id | path | string (uuid) | required | The company's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9 |
after | query | string or null | The e.g. eyJ2Ijog | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: CustomerList
{
"items": [
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Customer | required | |
next_cursor | string or null | required | Send it as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such contact or company, or a company named in the body does not exist
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Knowledge
GET /v1/articles
List or search articles
The workspace's articles, newest first, a page at a time. q is the search the app's Knowledge page uses: it matches words, not meaning, and a request never calls a model. An article an import has retired is not listed.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
q | query | string or null | Words to look for in the title and text, in Norwegian or English, or letters of the title e.g. returfrist | |
status | query | draft | published or null | Only drafts, or only published | |
audience | query | internal | public or null | Only | |
language | query | string or null | Only this language e.g. nb | |
brand_id | query | string (uuid) or null | Only this brand's articles | |
topic | query | string or null | A case type's key; e.g. return | |
origin | query | written | gap | import or null | Only articles that came in this way | |
after | query | string or null | The e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 | |
limit | query | integer | How many to return, at most |
200 Successful Response
Body application/json: ArticleList
{
"items": [
{
"audience": "public",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of ArticleSummary | required | |
next_cursor | string or null | Send as |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/articles
Create an article
Write an article. It is a draft and internal unless you say otherwise, and it is indexed for search as soon as it is saved, exactly as one written in the app is. The key is named in the workspace's audit log.
Send an Idempotency-Key so that a retry writes one article.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: ArticleIn
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"language": "nb",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
]
}| Field | Type | About | |
|---|---|---|---|
title | string | required | The title |
body | string | required | The text, Markdown or plain. If it holds HTML tags, scripts, styles and anything else a page may not carry are removed before it is stored |
language | string or null | The language of the text. Left out, the workspace's default language | |
brand_id | string (uuid) or null | A brand of the workspace; null is every brand | |
audience | internal | public |
| |
status | draft | published |
| |
topics | list of string | Case types, by key, the article serves |
201 Successful Response
Body application/json: Article
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/articles/{article_id}
Get an article
One article with its text. An id from another workspace, or of an imported page the site no longer has, is a 404.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
article_id | path | string (uuid) | required | The article's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
200 Successful Response
Body application/json: Article
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such article
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
PATCH /v1/articles/{article_id}
Change an article
Change what you send: title, text, language, brand, audience or topics. A change to the title or text is indexed again. Whether it is published is publish and unpublish.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
article_id | path | string (uuid) | required | The article's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
Request body application/json: ArticlePatch
{
"audience": "public",
"body": "Returfristen er 60 dager."
}| Field | Type | About | |
|---|---|---|---|
title | string or null | A new title | |
body | string or null | A new text; HTML is cleaned as on create | |
language | string or null | A new language | |
brand_id | string (uuid) or null | A brand of the workspace, or null for every brand | |
audience | internal | public or null |
| |
topics | list of string or null | Replaces the case types the article serves |
200 Successful Response
Body application/json: Article
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such article
Body application/json: ErrorResponse
422 The request did not validate
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/articles/{article_id}/publish
Publish an article
Make a draft usable: the team can insert it, and the bot can answer from it if its audience is public. Publishing a published article changes nothing.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
article_id | path | string (uuid) | required | The article's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
200 Successful Response
Body application/json: Article
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such article
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/articles/{article_id}/unpublish
Unpublish an article
Take an article out of use: it becomes a draft again, and neither the team's search nor the bot finds it. Nothing is deleted, and publishing it again brings it back.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
article_id | path | string (uuid) | required | The article's id e.g. 0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6 |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
200 Successful Response
Body application/json: Article
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope
Body application/json: ErrorResponse
404 No such article
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Webhooks
GET /v1/webhook-endpoints
List webhook endpoints
The endpoints this key could have made: all of the workspace's when the key sees every group, otherwise those that hear only groups the key sees.
200 Successful Response
Body application/json: WebhookEndpointList
{
"items": [
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of WebhookEndpoint | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/webhook-endpoints
Make a webhook endpoint
Nolmo sends the chosen events to url as signed POSTs (see the webhooks section for the body and the signature). The address must be https and public; it is checked now and on every send. The answer holds the signing secret: it is shown only here.
An endpoint hears only the cases a key with its groups could read. A key can give only the groups it sees itself; all_groups needs a key that sees every group.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: WebhookEndpointCreate
{
"all_groups": false,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}| Field | Type | About | |
|---|---|---|---|
name | string | required | What the receiver is, for the list |
url | string | required |
|
events | list of case.created | case.updated | case.message.created | case.note.created | ||
all_groups | boolean | ||
group_ids | list of string (uuid) |
201 Successful Response
Body application/json: WebhookEndpointCreated
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw9o7cXx1b0hE=",
"url": "https://returns.example.com/hooks/nolmo"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required | |
secret | string | required | Signs every send ( |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/webhook-endpoints/{endpoint_id}
Read a webhook endpoint
The endpoint's address, events, groups and state. The secret is never returned.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
200 Successful Response
Body application/json: WebhookEndpoint
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
PATCH /v1/webhook-endpoints/{endpoint_id}
Change a webhook endpoint
Only what is sent changes. enabled: true turns a stopped endpoint on again and sends what waited, as long as it is within 24 hours.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
Request body application/json: WebhookEndpointUpdate
{
"enabled": false
}| Field | Type | About | |
|---|---|---|---|
name | string or null | ||
url | string or null | ||
events | list of case.created | case.updated | case.message.created | case.note.created or null | ||
enabled | boolean or null | ||
all_groups | boolean or null | ||
group_ids | list of string (uuid) or null |
200 Successful Response
Body application/json: WebhookEndpoint
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
DELETE /v1/webhook-endpoints/{endpoint_id}
Delete a webhook endpoint
The endpoint and its delivery log are gone, and nothing more is sent to it.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
204 Successful Response
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret
Rotate an endpoint's secret
A new secret, shown only in this answer. The old one keeps signing for 24 hours beside it (webhook-signature then holds both), so the receiver can change over without a gap.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
200 Successful Response
Body application/json: WebhookEndpointCreated
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw9o7cXx1b0hE=",
"url": "https://returns.example.com/hooks/nolmo"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required | |
secret | string | required | Signs every send ( |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/webhook-endpoints/{endpoint_id}/deliveries
The delivery log
What was sent to the endpoint in the last 30 days, newest first: the event, how many attempts, the receiver's last status code. Page with cursor.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
cursor | query | string or null |
| |
limit | query | integer | How many deliveries to return | |
status | query | pending | delivered | failed or null | Only deliveries in this state |
200 Successful Response
Body application/json: WebhookDeliveryList
{
"items": [
{
"attempts": 1,
"case_number": 1042,
"delivered_at": "2026-11-10T08:20:42Z",
"event": "case.message.created",
"id": "0192f3a4-2222-7e2d-a3f4-5b6c7d8e9f01",
"last_attempt_at": "2026-11-10T08:20:42Z",
"occurred_at": "2026-11-10T08:20:41Z",
"response_code": 200,
"status": "delivered"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of WebhookDelivery | required | |
next_cursor | string or null | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/webhook-endpoints/{endpoint_id}/deliveries/{delivery_id}/resend
Send one event again
The same event with the same webhook-id, as a new series of attempts. The receiver dedupes by that id.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
delivery_id | path | string (uuid) | required | The delivery's id, as the receiver got it in |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
200 Successful Response
Body application/json: WebhookDelivery
{
"attempts": 1,
"case_number": 1042,
"delivered_at": "2026-11-10T08:20:42Z",
"event": "case.message.created",
"id": "0192f3a4-2222-7e2d-a3f4-5b6c7d8e9f01",
"last_attempt_at": "2026-11-10T08:20:42Z",
"occurred_at": "2026-11-10T08:20:41Z",
"response_code": 200,
"status": "delivered"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The event's id, as the receiver got it in |
event | case.created | case.updated | case.message.created | case.note.created | required | |
case_number | integer | required | |
status | pending | delivered | failed | required |
|
attempts | integer | required | |
response_code | integer or null | required | The receiver's last status code, when it answered |
error | string or null | required | Why the last attempt failed when there was no answer: |
occurred_at | string (date-time) | required | |
last_attempt_at | string (date-time) or null | required | |
next_attempt_at | string (date-time) or null | required | |
delivered_at | string (date-time) or null | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
POST /v1/webhook-endpoints/{endpoint_id}/replay
Send everything since a time again
Every event in the log since since, delivered or not, again and in order, each with its own webhook-id. For a receiver that was down for longer than the 24 hours events are retried. Only the last 30 days are kept.
Parameters
| Name | In | Type | About | |
|---|---|---|---|---|
endpoint_id | path | string (uuid) | required | The webhook endpoint's id |
Idempotency-Key | header | string or null | A value you make up, such as a UUID, for this one request. Send the same key again, after a timeout or a crash, and you get the first answer back ( e.g. 7c9e6679-7425-40de-944b-e07fc1f90ae7 |
Request body application/json: Replay
{
"since": "2026-11-10T08:00:00Z"
}| Field | Type | About | |
|---|---|---|---|
since | string (date-time) | required |
200 Successful Response
Body application/json: Replayed
{
"queued": 12
}| Field | Type | About | |
|---|---|---|---|
queued | integer | required | How many events will be sent again |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `webhooks:manage`, or the groups
Body application/json: ErrorResponse
404 No such endpoint, or one the key could not have made
Body application/json: ErrorResponse
409 The endpoint is off, or the workspace is at its limit
Body application/json: ErrorResponse
422 Not accepted, e.g. `webhook_url_refused` (see `details.reason`)
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Lookups
GET /v1/groups
List groups
The workspace's groups, by name. A key limited to some groups sees those only. Use an id as group_id when creating a case or searching.
200 Successful Response
Body application/json: GroupList
{
"items": [
{
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "Support"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Group | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/agents
List the team
The people who work in the workspace: an id, a first name and the groups they are in. No surname, email or phone number is shared. Use an id as assignee_id when searching.
A key limited to some groups sees the people who are in one of them, and only which of the key's groups they are in.
200 Successful Response
Body application/json: AgentList
{
"items": [
{
"first_name": "Astrid",
"group_ids": [
"0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
],
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Agent | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/case-types
List case types
The workspace's case types: what type takes when you create a case, and what type filters on in a search. Only the ones in use.
200 Successful Response
Body application/json: CaseTypeList
{
"items": [
{
"key": "return",
"name": "Return"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of CaseType | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/case-fields
List case fields
The workspace's own fields on a case: the key to use in custom_fields, what it is called, its kind, and for a select its choices.
200 Successful Response
Body application/json: CaseFieldList
{
"items": [
{
"key": "order_no",
"kind": "text",
"label": "Order number",
"options": []
},
{
"key": "carrier",
"kind": "select",
"label": "Carrier",
"options": [
{
"label": "Posten",
"value": "posten"
},
{
"label": "DHL",
"value": "dhl"
}
]
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of CaseField | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
GET /v1/tags
List tags
The tags on the cases the key sees, with how many cases have each, the most used first. A key limited to some groups counts its own cases only.
200 Successful Response
Body application/json: TagList
{
"items": [
{
"count": 12,
"tag": "delivery"
}
]
}| Field | Type | About | |
|---|---|---|---|
items | list of Tag | required |
401 No key, or a key that is wrong, revoked or expired
Body application/json: ErrorResponse
403 The key lacks the scope `cases:read`
Body application/json: ErrorResponse
429 Over the limit; see `Retry-After`
Body application/json: ErrorResponse
Webhook events
POST case.created
case.created
A case was made.
Nolmo sends a POST with a JSON body and these headers:
webhook-id: this event's id, the same on every retry and replay. Dedupe by it.webhook-timestamp: seconds since the epoch, when this attempt was made.webhook-signature:v1,<base64>for each valid secret, space separated. The signature is HMAC-SHA256 over<webhook-id>.<webhook-timestamp>.<body>with the secret's bytes (the base64 afterwhsec_), as in Standard Webhooks. Refuse a timestamp more than five minutes from your clock and a body whose signature does not match.
Answer with any 2xx within ten seconds. Anything else, or no answer, is retried with a growing pause for 24 hours; events of one case arrive in order. An endpoint that answers nothing for three days is turned off and the workspace's admins are told. Redirects are not followed.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool:
"""True when `body` was signed with `secret` (the whsec_ value) and is fresh."""
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(expected, part.removeprefix("v1,"))
for part in headers["webhook-signature"].split()
if part.startswith("v1,")
)Request body application/json: CaseCreatedEvent
{
"created_at": "2026-11-10T08:15:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
}
},
"id": "0192f3a4-3333-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.created"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.created | ||
data | CaseData | required |
200 Successful Response
Body application/json: any
POST case.updated
case.updated
A case's status, assignee, priority or type changed (one event per change).
Nolmo sends a POST with a JSON body and these headers:
webhook-id: this event's id, the same on every retry and replay. Dedupe by it.webhook-timestamp: seconds since the epoch, when this attempt was made.webhook-signature:v1,<base64>for each valid secret, space separated. The signature is HMAC-SHA256 over<webhook-id>.<webhook-timestamp>.<body>with the secret's bytes (the base64 afterwhsec_), as in Standard Webhooks. Refuse a timestamp more than five minutes from your clock and a body whose signature does not match.
Answer with any 2xx within ten seconds. Anything else, or no answer, is retried with a growing pause for 24 hours; events of one case arrive in order. An endpoint that answers nothing for three days is turned off and the workspace's admins are told. Redirects are not followed.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool:
"""True when `body` was signed with `secret` (the whsec_ value) and is fresh."""
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(expected, part.removeprefix("v1,"))
for part in headers["webhook-signature"].split()
if part.startswith("v1,")
)Request body application/json: CaseUpdatedEvent
{
"created_at": "2026-11-10T08:18:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"changes": [
{
"field": "assignee",
"to": "Astrid"
}
]
},
"id": "0192f3a4-4444-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.updated"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.updated | ||
data | CaseUpdatedData | required |
200 Successful Response
Body application/json: any
POST case.message.created
case.message.created
A reply the customer sees was added to a case.
Nolmo sends a POST with a JSON body and these headers:
webhook-id: this event's id, the same on every retry and replay. Dedupe by it.webhook-timestamp: seconds since the epoch, when this attempt was made.webhook-signature:v1,<base64>for each valid secret, space separated. The signature is HMAC-SHA256 over<webhook-id>.<webhook-timestamp>.<body>with the secret's bytes (the base64 afterwhsec_), as in Standard Webhooks. Refuse a timestamp more than five minutes from your clock and a body whose signature does not match.
Answer with any 2xx within ten seconds. Anything else, or no answer, is retried with a growing pause for 24 hours; events of one case arrive in order. An endpoint that answers nothing for three days is turned off and the workspace's admins are told. Redirects are not followed.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool:
"""True when `body` was signed with `secret` (the whsec_ value) and is fresh."""
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(expected, part.removeprefix("v1,"))
for part in headers["webhook-signature"].split()
if part.startswith("v1,")
)Request body application/json: CaseMessageCreatedEvent
{
"created_at": "2026-11-10T08:20:41Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"message": {
"author_kind": "customer",
"created_at": "2026-11-10T08:20:41Z",
"id": "0192f3a4-7b1c-7e2d-a3f4-5b6c7d8e9f01",
"kind": "public",
"text": "Hi, three of the tiles in the pallet were cracked."
}
},
"id": "0192f3a4-5555-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.message.created"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.message.created | ||
data | MessageData | required |
200 Successful Response
Body application/json: any
POST case.note.created
case.note.created
An internal note was added to a case. Only sent to an endpoint that asks for it.
Nolmo sends a POST with a JSON body and these headers:
webhook-id: this event's id, the same on every retry and replay. Dedupe by it.webhook-timestamp: seconds since the epoch, when this attempt was made.webhook-signature:v1,<base64>for each valid secret, space separated. The signature is HMAC-SHA256 over<webhook-id>.<webhook-timestamp>.<body>with the secret's bytes (the base64 afterwhsec_), as in Standard Webhooks. Refuse a timestamp more than five minutes from your clock and a body whose signature does not match.
Answer with any 2xx within ten seconds. Anything else, or no answer, is retried with a growing pause for 24 hours; events of one case arrive in order. An endpoint that answers nothing for three days is turned off and the workspace's admins are told. Redirects are not followed.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool:
"""True when `body` was signed with `secret` (the whsec_ value) and is fresh."""
msg_id = headers["webhook-id"]
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > tolerance:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(expected, part.removeprefix("v1,"))
for part in headers["webhook-signature"].split()
if part.startswith("v1,")
)Request body application/json: CaseNoteCreatedEvent
{
"created_at": "2026-11-10T08:22:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"message": {
"author_kind": "team",
"created_at": "2026-11-10T08:20:41Z",
"id": "0192f3a4-7b1c-7e2d-a3f4-5b6c7d8e9f01",
"kind": "internal",
"text": "Replacement tiles ordered."
}
},
"id": "0192f3a4-6666-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.note.created"
}| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.note.created | ||
data | MessageData | required |
200 Successful Response
Body application/json: any
Schemas
Agent
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | Use it as |
first_name | string | required | The first name only: no surname, email or phone number is shared |
group_ids | list of string (uuid) | required | The groups the person is in. A key limited to some groups sees those of them only |
{
"first_name": "Astrid",
"group_ids": [
"0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
],
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
}AgentList
| Field | Type | About | |
|---|---|---|---|
items | list of Agent | required |
{
"items": [
{
"first_name": "Astrid",
"group_ids": [
"0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
],
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6"
}
]
}Article
One article, with its text.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
body | string | required | The text, as it is stored: Markdown or plain text |
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}ArticleIn
A new article. It is a draft until you publish it, and an internal one until you say public.
| Field | Type | About | |
|---|---|---|---|
title | string | required | The title |
body | string | required | The text, Markdown or plain. If it holds HTML tags, scripts, styles and anything else a page may not carry are removed before it is stored |
language | string or null | The language of the text. Left out, the workspace's default language | |
brand_id | string (uuid) or null | A brand of the workspace; null is every brand | |
audience | internal | public |
| |
status | draft | published |
| |
topics | list of string | Case types, by key, the article serves |
{
"audience": "public",
"body": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"language": "nb",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
]
}ArticleList
A page of articles, newest first.
| Field | Type | About | |
|---|---|---|---|
items | list of ArticleSummary | required | |
next_cursor | string or null | Send as |
{
"items": [
{
"audience": "public",
"created_at": "2026-10-20T08:00:00Z",
"excerpt": "Returfristen er 30 dager. Send varene uåpnet i retur sammen med kvitteringen.",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"origin": "written",
"published_at": "2026-10-20T08:15:00Z",
"status": "published",
"title": "Returfrist for fliser",
"topics": [
"return"
],
"updated_at": "2026-10-20T08:15:00Z"
}
]
}ArticlePatch
Only what is sent changes. brand_id: null clears the brand; topics replaces the list whole. Publishing and unpublishing have their own routes.
| Field | Type | About | |
|---|---|---|---|
title | string or null | A new title | |
body | string or null | A new text; HTML is cleaned as on create | |
language | string or null | A new language | |
brand_id | string (uuid) or null | A brand of the workspace, or null for every brand | |
audience | internal | public or null |
| |
topics | list of string or null | Replaces the case types the article serves |
{
"audience": "public",
"body": "Returfristen er 60 dager."
}ArticleSummary
An article without its body, as the list shows it.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The article's id |
title | string | required | The title |
excerpt | string | required | The body's first 200 characters |
language | string | required | The language of the text, such as |
status | draft | published | required |
|
audience | internal | public | required |
|
origin | written | gap | import | required |
|
topics | list of string | required | The case types (by key) the article serves; empty is no topic |
brand_id | string (uuid) or null | required | The brand it belongs to; null is every brand |
source_url | string or null | required | An imported page's address, else null |
published_at | string (date-time) or null | required | When it was first published; null for a draft |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
Attachment
| Field | Type | About | |
|---|---|---|---|
id | string | required | Use it in |
name | string | required | |
content_type | string | required | |
size | integer | required | Bytes |
Author
| Field | Type | About | |
|---|---|---|---|
type | customer | team | api | ai | required |
|
name | string or null | required |
Body_upload_file_v1_uploads_post
| Field | Type | About | |
|---|---|---|---|
file | string | required |
Case
A case, as far as a key may see it.
| Field | Type | About | |
|---|---|---|---|
number | integer | required | The case's number in the workspace; the id in the paths |
subject | string | required | |
status | new | open | pending | hold | solved | closed | required |
|
priority | low | normal | high | urgent | required | |
type | string or null | required | The case type's key |
tags | list of string | required | |
custom_fields | object | required | |
external_ref | string or null | required | Your own id for the case, if it has one |
source | string or null | required | The |
channel | string | required | How the case arrived: |
group_id | string (uuid) or null | required | |
brand_id | string (uuid) or null | required | |
assignee_first_name | string or null | required | The first name of the person who has the case |
contact | Contact | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
solved_at | string (date-time) or null | required | |
last_message_at | string (date-time) or null | required | |
version | integer | required | Send it back in |
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}CaseCounts
How many cases a person or a company has, counting only the cases the key may see.
| Field | Type | About | |
|---|---|---|---|
cases | integer | required | Cases, spam left out |
open | integer | required | Of those, the ones still with the team: new, open, pending or on hold |
last_contact_at | string (date-time) or null | required | When they last wrote or were written to |
CaseCreatedEvent
A case was made, by a person, a customer's message or another system.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.created | ||
data | CaseData | required |
{
"created_at": "2026-11-10T08:15:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
}
},
"id": "0192f3a4-3333-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.created"
}CaseData
| Field | Type | About | |
|---|---|---|---|
case | PublicCase | required |
CaseEvent
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
kind | created | customer_replied | team_replied | status_changed | reopened | assigned | priority_changed | type_changed | required |
|
actor | EventActor | required | |
from | string or null | The value before, where there is one | |
to | string or null | The value after, where there is one | |
message_id | string (uuid) or null | The message, for a reply | |
at | string (date-time) | required |
{
"actor": {
"name": "Astrid",
"type": "team"
},
"at": "2026-11-09T11:02:00Z",
"from": "open",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "status_changed",
"to": "solved"
}CaseEventList
| Field | Type | About | |
|---|---|---|---|
items | list of CaseEvent | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"actor": {
"name": "Astrid",
"type": "team"
},
"at": "2026-11-09T11:02:00Z",
"from": "open",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "status_changed",
"to": "solved"
}
]
}CaseField
| Field | Type | About | |
|---|---|---|---|
key | string | required | The key in a case's |
label | string | required | |
kind | text | number | select | date | checkbox | required |
|
options | list of FieldOption | required | The choices of a |
{
"key": "order_no",
"kind": "text",
"label": "Order number",
"options": []
}CaseFieldList
| Field | Type | About | |
|---|---|---|---|
items | list of CaseField | required |
{
"items": [
{
"key": "order_no",
"kind": "text",
"label": "Order number",
"options": []
},
{
"key": "carrier",
"kind": "select",
"label": "Carrier",
"options": [
{
"label": "Posten",
"value": "posten"
},
{
"label": "DHL",
"value": "dhl"
}
]
}
]
}CaseFile
| Field | Type | About | |
|---|---|---|---|
id | string | required | Download it with |
name | string | required | |
content_type | string | required | |
size | integer | required | Bytes |
message_id | string (uuid) | required | The message it hangs on |
on | message | note | required |
|
created_at | string (date-time) | required | When the message was written |
{
"content_type": "image/jpeg",
"created_at": "2026-11-09T10:30:00Z",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"message_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "damaged-tiles.jpg",
"on": "message",
"size": 183204
}CaseFileList
| Field | Type | About | |
|---|---|---|---|
items | list of CaseFile | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"content_type": "image/jpeg",
"created_at": "2026-11-09T10:30:00Z",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"message_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "damaged-tiles.jpg",
"on": "message",
"size": 183204
}
]
}CaseIn
A new case: a person, a subject and what they wrote. It is routed by the workspace's triggers, gets its clocks and is assessed, exactly as a mail would.
| Field | Type | About | |
|---|---|---|---|
contact | ContactIn | required | |
subject | string | required | The case's title |
message | FirstMessageIn | required | |
priority | low | normal | high | urgent |
| |
type | string or null | One of the workspace's case types, by its key | |
tags | list of string | Labels to put on the case | |
brand_id | string (uuid) or null | The brand or store the case belongs to | |
group_id | string (uuid) or null | The group the case goes to. It must be one of the key's groups ( | |
custom_fields | object | Values for the workspace's case fields, by the field's key | |
external_ref | string or null | Your own id for the case. A second create with the same id from the same | |
source | string or null | What you call your system, such as |
{
"contact": {
"email": "kari@example.no",
"name": "Kari Kunde"
},
"external_ref": "order-12345",
"message": {
"body_text": "Three of the tiles in the pack were cracked. Order 12345."
},
"priority": "normal",
"source": "Returns page",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
]
}CaseList
| Field | Type | About | |
|---|---|---|---|
items | list of Case | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"channel": "api",
"contact": {
"email": "kari@example.no",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde"
},
"created_at": "2026-11-09T09:00:00Z",
"custom_fields": {},
"external_ref": "order-12345",
"group_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"last_message_at": "2026-11-09T09:00:00Z",
"number": 1042,
"priority": "normal",
"source": "Returns page",
"status": "new",
"subject": "Cracked tiles in the delivery",
"tags": [
"delivery"
],
"updated_at": "2026-11-09T09:00:00Z",
"version": 1
}
]
}CaseMessageCreatedEvent
A reply the customer sees was added to a case: theirs, the team's or the AI's. Internal notes are case.note.created.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.message.created | ||
data | MessageData | required |
{
"created_at": "2026-11-10T08:20:41Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"message": {
"author_kind": "customer",
"created_at": "2026-11-10T08:20:41Z",
"id": "0192f3a4-7b1c-7e2d-a3f4-5b6c7d8e9f01",
"kind": "public",
"text": "Hi, three of the tiles in the pallet were cracked."
}
},
"id": "0192f3a4-5555-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.message.created"
}CaseNoteCreatedEvent
An internal note was added to a case. Opt-in: an endpoint hears it only when it asks for it.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.note.created | ||
data | MessageData | required |
{
"created_at": "2026-11-10T08:22:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"message": {
"author_kind": "team",
"created_at": "2026-11-10T08:20:41Z",
"id": "0192f3a4-7b1c-7e2d-a3f4-5b6c7d8e9f01",
"kind": "internal",
"text": "Replacement tiles ordered."
}
},
"id": "0192f3a4-6666-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.note.created"
}CasePatchIn
What a caller may change on a case. Status and assignment stay the team's: sending them is a 422.
| Field | Type | About | |
|---|---|---|---|
tags | list of string or null | Replaces the case's tags | |
priority | low | normal | high | urgent or null |
| |
custom_fields | object or null | Values for the case fields, merged key by key | |
external_ref | string or null | Your own id for the case; | |
version | integer or null | The |
{
"priority": "high",
"tags": [
"delivery",
"refund"
],
"version": 3
}CaseType
| Field | Type | About | |
|---|---|---|---|
key | string | required | What |
name | string | required |
{
"key": "return",
"name": "Return"
}CaseTypeList
| Field | Type | About | |
|---|---|---|---|
items | list of CaseType | required |
{
"items": [
{
"key": "return",
"name": "Return"
}
]
}CaseUpdatedData
| Field | Type | About | |
|---|---|---|---|
case | PublicCase | required | |
changes | list of Change | required | What this event is about; one event per change |
CaseUpdatedEvent
A case's status, assignee, priority or type changed. data.case is the case as it is when the event is sent; data.changes says what this event is about.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | This event for this endpoint, the same on every retry and every replay: the receiver's key to dedupe by |
created_at | string (date-time) | required | When it happened, not when it was sent |
type | case.updated | ||
data | CaseUpdatedData | required |
{
"created_at": "2026-11-10T08:18:00Z",
"data": {
"case": {
"assignee": "Astrid",
"channel": "email",
"contact": {
"email": "kari@example.com",
"id": "7c2e4a1b-9d3f-4f6a-8b5e-1a2b3c4d5e6f",
"name": "Kari Kunde"
},
"created_at": "2026-11-10T08:15:00Z",
"id": "5d0b8f0e-2a4c-7d3e-9c1a-3b6f2e8a1c42",
"number": 1042,
"priority": "high",
"status": "open",
"subject": "Cracked tile in the delivery",
"tags": [
"delivery",
"tiles"
],
"type": "complaint",
"updated_at": "2026-11-10T08:20:41Z"
},
"changes": [
{
"field": "assignee",
"to": "Astrid"
}
]
},
"id": "0192f3a4-4444-7e2d-a3f4-5b6c7d8e9f01",
"type": "case.updated"
}Change
One thing that changed on a case. from and to are the status, the priority or the type's key, or the first name of the assignee.
| Field | Type | About | |
|---|---|---|---|
field | status | assignee | priority | type | required | |
from | string or null | required | |
to | string or null | required |
Contact
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | |
phone | string or null | required | |
language | string or null | required |
|
ContactIn
The person the case is about, found or made. Give at least one of email, phone and external_ids: the person is found by the email, then the phone number, then an id in another system (exact matches only), and made when none is known. A person who is found is not changed.
| Field | Type | About | |
|---|---|---|---|
name | string | required | Their name, used when they are new |
email | string (email) or null | Their email address | |
phone | string or null | Their phone number | |
language | string or null | Their language, | |
external_ids | map of string | Their id in your own systems, by the system's name: |
{
"email": "kari@example.no",
"language": "nb",
"name": "Kari Kunde",
"phone": "+47 900 00 000"
}Customer
A customer: a person who writes to the workspace.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | Lowercased. Unique in the workspace |
phone | string or null | required | Digits with a leading |
language | string or null | required |
|
customer_no | string or null | required | Their customer number |
member_no | string or null | required | Their member number |
external_ids | map of string | required | Their ids in your own systems, by the system's name: |
organization_id | string (uuid) or null | required | The company they belong to |
organization_name | string or null | required | |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}CustomerIn
A new contact. A person whose email, phone number or id in another system is already known is not made twice: the answer is 409 contact_exists with the existing contact's id in details.contact_id.
| Field | Type | About | |
|---|---|---|---|
name | string | required | Their name |
email | string (email) or null | Their email address | |
phone | string or null | Their phone number | |
language | string or null |
| |
customer_no | string or null | ||
member_no | string or null | ||
organization_id | string (uuid) or null | The company they belong to. Left out, a company with their email's domain is used | |
external_ids | map of string | Their ids in your own systems, by the system's name: |
{
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"language": "nb",
"name": "Kari Kunde",
"phone": "+47 900 00 000"
}CustomerList
| Field | Type | About | |
|---|---|---|---|
items | list of Customer | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"case_counts": {
"cases": 3,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 1
},
"created_at": "2026-09-02T10:00:00Z",
"customer_no": "10442",
"email": "kari@example.no",
"external_ids": {
"shop": "C-1042"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"language": "nb",
"name": "Kari Kunde",
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"organization_name": "Fliser AS",
"phone": "+4790000000",
"updated_at": "2026-10-20T08:15:00Z"
}
]
}CustomerPatchIn
What to change on a contact. Fields left out are not touched; null empties an optional field.
| Field | Type | About | |
|---|---|---|---|
name | string or null | Never null | |
email | string (email) or null | Another email address; | |
phone | string or null | ||
language | string or null | ||
customer_no | string or null | ||
member_no | string or null | ||
organization_id | string (uuid) or null |
| |
external_ids | map of string or null | Merged key by key: a system's id is set, or forgotten with |
{
"external_ids": {
"shop": "C-2000"
},
"organization_id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"phone": "+47 911 22 333"
}ErrorBody
| Field | Type | About | |
|---|---|---|---|
code | string | required | |
message | string | required | |
details | object or null |
ErrorResponse
Declare on operations that can fail: responses={404: {"model": ErrorResponse}}.
| Field | Type | About | |
|---|---|---|---|
error | ErrorBody | required |
EventActor
| Field | Type | About | |
|---|---|---|---|
type | customer | team | api | ai | automation | system | required |
|
name | string or null | required |
FieldOption
| Field | Type | About | |
|---|---|---|---|
value | string | required | |
label | string | required |
FirstMessageIn
What the customer wrote.
| Field | Type | About | |
|---|---|---|---|
body_text | string | required | The words, as plain text |
body_html | string or null | The same as HTML, if you have it. It is cleaned before it is stored: scripts, styles and anything else a mail may not carry are removed | |
attachments | list of string | Ids from |
Group
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required |
{
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "Support"
}GroupList
| Field | Type | About | |
|---|---|---|---|
items | list of Group | required |
{
"items": [
{
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "Support"
}
]
}Me
Who is calling, so an integration can check its setup.
| Field | Type | About | |
|---|---|---|---|
key | MeKey | required | |
tenant | MeTenant | required | |
scopes | list of cases:read | cases:write | cases:reply | contacts:read | contacts:write | webhooks:manage | knowledge:read | knowledge:write | required | What the key may do |
all_groups | boolean | required | The key sees every group's cases |
groups | list of MeGroup | required | The groups it sees; empty with |
{
"all_groups": false,
"groups": [
{
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f8",
"name": "Support"
}
],
"key": {
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "Contact form",
"prefix": "nolmo_live_k3J9fQ2a"
},
"scopes": [
"cases:read",
"cases:write"
],
"tenant": {
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f7",
"name": "Furu & Lin",
"slug": "furu-lin"
}
}MeGroup
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required |
MeKey
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
prefix | string | required |
|
expires_at | string (date-time) or null | required | When the key stops working; null for never |
MeTenant
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
slug | string | required |
Message
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
kind | customer | note | reply | required |
|
author | Author | required | |
body_text | string | required | |
body_html | string or null | required | |
attachments | list of Attachment | required | |
delivery | queued | sent | failed or null | required | For a reply that leaves by email: where it is. Null for everything else |
created_at | string (date-time) | required |
{
"attachments": [],
"author": {
"name": "Astrid",
"type": "team"
},
"body_text": "Hi Kari, we are sending new tiles today.",
"created_at": "2026-11-09T10:30:00Z",
"delivery": "sent",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "reply"
}MessageData
| Field | Type | About | |
|---|---|---|---|
case | PublicCase | required | |
message | PublicMessage | required |
MessageIn
Something to add to a case.
| Field | Type | About | |
|---|---|---|---|
kind | customer | note | reply | required |
|
body_text | string | required | |
body_html | string or null | Cleaned before it is stored, as in a new case | |
attachments | list of string | Ids from |
{
"body_text": "Any news on this? I still have not heard anything.",
"kind": "customer"
}MessageList
| Field | Type | About | |
|---|---|---|---|
items | list of Message | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"attachments": [],
"author": {
"name": "Astrid",
"type": "team"
},
"body_text": "Hi Kari, we are sending new tiles today.",
"created_at": "2026-11-09T10:30:00Z",
"delivery": "sent",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"kind": "reply"
}
]
}Organization
A company the workspace serves.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
domains | list of string | required | Email domains whose people belong here, lowercased |
external_ids | map of string | required | Its ids in your own systems, by the system's name |
people | integer | required | How many contacts belong to it |
case_counts | CaseCounts | required | |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required |
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}OrganizationIn
A new company. One with the same name or the same id in another system is not made twice: the answer is 409 organization_exists with the existing company's id in details.organization_id.
| Field | Type | About | |
|---|---|---|---|
name | string | required | |
domains | list of string | Email domains; a new contact with an address there joins the company | |
external_ids | map of string | Its ids in your own systems, by the system's name |
{
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"name": "Fliser AS"
}OrganizationList
| Field | Type | About | |
|---|---|---|---|
items | list of Organization | required | |
next_cursor | string or null | required | Send it as |
{
"items": [
{
"case_counts": {
"cases": 9,
"last_contact_at": "2026-10-20T08:15:00Z",
"open": 2
},
"created_at": "2026-09-02T10:00:00Z",
"domains": [
"fliser.no"
],
"external_ids": {
"crm": "ORG-77"
},
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f9",
"name": "Fliser AS",
"people": 4,
"updated_at": "2026-10-20T08:15:00Z"
}
]
}OrganizationPatchIn
What to change on a company. Fields left out are not touched.
| Field | Type | About | |
|---|---|---|---|
name | string or null | Never null | |
domains | list of string or null | Replaces the domains | |
external_ids | map of string or null | Merged key by key: set, or forgotten with |
{
"domains": [
"fliser.no",
"fliser.com"
],
"external_ids": {}
}PublicCase
A case as /v1 shows it.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
number | integer | required | The case's number in the workspace, the one people see and type |
subject | string | required | |
status | new | open | pending | hold | solved | closed | required | |
priority | low | normal | high | urgent | required | |
type | string or null | required | The case type's key, when the case has one |
tags | list of string | required | |
assignee | string or null | required | First name of whoever has the case; null when nobody does |
created_at | string (date-time) | required | |
updated_at | string (date-time) | required | |
contact | PublicContact | required | |
channel | string or null | required | How the case came in: |
PublicContact
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
email | string or null | required | The contact's address, when they have one |
PublicMessage
A message on a case as /v1 shows it.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
kind | public | internal | required |
|
text | string | required | |
author_kind | customer | team | ai | system | required | |
created_at | string (date-time) | required |
Replay
Send every event since a time again, in order, as new attempts with the same ids.
| Field | Type | About | |
|---|---|---|---|
since | string (date-time) | required |
{
"since": "2026-11-10T08:00:00Z"
}Replayed
| Field | Type | About | |
|---|---|---|---|
queued | integer | required | How many events will be sent again |
{
"queued": 12
}Tag
| Field | Type | About | |
|---|---|---|---|
tag | string | required | |
count | integer | required | How many of the cases the key sees have it |
{
"count": 12,
"tag": "delivery"
}TagList
| Field | Type | About | |
|---|---|---|---|
items | list of Tag | required |
{
"items": [
{
"count": 12,
"tag": "delivery"
}
]
}Upload
A file waiting to be put on a message.
| Field | Type | About | |
|---|---|---|---|
id | string | required | Name it in |
name | string | required | |
content_type | string | required | |
size | integer | required | Bytes |
{
"content_type": "image/jpeg",
"id": "0192f3c4-6a1b-7c2d-8e3f-a1b2c3d4e5f6",
"name": "damaged-tiles.jpg",
"size": 183204
}WebhookDelivery
One event for one endpoint, as the log shows it (kept 30 days).
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | The event's id, as the receiver got it in |
event | case.created | case.updated | case.message.created | case.note.created | required | |
case_number | integer | required | |
status | pending | delivered | failed | required |
|
attempts | integer | required | |
response_code | integer or null | required | The receiver's last status code, when it answered |
error | string or null | required | Why the last attempt failed when there was no answer: |
occurred_at | string (date-time) | required | |
last_attempt_at | string (date-time) or null | required | |
next_attempt_at | string (date-time) or null | required | |
delivered_at | string (date-time) or null | required |
{
"attempts": 1,
"case_number": 1042,
"delivered_at": "2026-11-10T08:20:42Z",
"event": "case.message.created",
"id": "0192f3a4-2222-7e2d-a3f4-5b6c7d8e9f01",
"last_attempt_at": "2026-11-10T08:20:42Z",
"occurred_at": "2026-11-10T08:20:41Z",
"response_code": 200,
"status": "delivered"
}WebhookDeliveryList
| Field | Type | About | |
|---|---|---|---|
items | list of WebhookDelivery | required | |
next_cursor | string or null | required |
{
"items": [
{
"attempts": 1,
"case_number": 1042,
"delivered_at": "2026-11-10T08:20:42Z",
"event": "case.message.created",
"id": "0192f3a4-2222-7e2d-a3f4-5b6c7d8e9f01",
"last_attempt_at": "2026-11-10T08:20:42Z",
"occurred_at": "2026-11-10T08:20:41Z",
"response_code": 200,
"status": "delivered"
}
]
}WebhookEndpoint
Where events are sent. The secret is not here: it is shown once, when the endpoint is made or its secret rotated.
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required |
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}WebhookEndpointCreate
| Field | Type | About | |
|---|---|---|---|
name | string | required | What the receiver is, for the list |
url | string | required |
|
events | list of case.created | case.updated | case.message.created | case.note.created | ||
all_groups | boolean | ||
group_ids | list of string (uuid) |
{
"all_groups": false,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}WebhookEndpointCreated
| Field | Type | About | |
|---|---|---|---|
id | string (uuid) | required | |
name | string | required | |
url | string | required | |
events | list of case.created | case.updated | case.message.created | case.note.created | required | |
enabled | boolean | required | |
disabled_reason | failing | manual or null | required |
|
disabled_at | string (date-time) or null | required | |
all_groups | boolean | required | Hears every case, whatever its group |
group_ids | list of string (uuid) | required | The groups it hears; empty with |
previous_secret_until | string (date-time) or null | required | While set, the secret that was rotated out still signs beside the new one, until this time |
failing_since | string (date-time) or null | required | Since when no send has been answered with a 2xx |
last_success_at | string (date-time) or null | required | |
created_at | string (date-time) | required | |
secret | string | required | Signs every send ( |
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw9o7cXx1b0hE=",
"url": "https://returns.example.com/hooks/nolmo"
}WebhookEndpointList
| Field | Type | About | |
|---|---|---|---|
items | list of WebhookEndpoint | required |
{
"items": [
{
"all_groups": false,
"created_at": "2026-11-09T12:00:00Z",
"enabled": true,
"events": [
"case.created",
"case.updated",
"case.message.created"
],
"group_ids": [
"0b1c1f0e-8f6e-4d8e-9a47-0c8f5d0e6a11"
],
"id": "0192f3a4-1111-7e2d-a3f4-5b6c7d8e9f01",
"last_success_at": "2026-11-10T08:20:42Z",
"name": "Returns page",
"url": "https://returns.example.com/hooks/nolmo"
}
]
}WebhookEndpointUpdate
Only what is sent changes. Turning an endpoint on clears its failing mark.
| Field | Type | About | |
|---|---|---|---|
name | string or null | ||
url | string or null | ||
events | list of case.created | case.updated | case.message.created | case.note.created or null | ||
enabled | boolean or null | ||
all_groups | boolean or null | ||
group_ids | list of string (uuid) or null |
{
"enabled": false
}Changelog
- Knowledge:
/v1/articlesto list, search, read, write, publish and unpublish the articles the team and the bot read (scopesknowledge:read,knowledge:write). - Customers and companies:
/v1/contacts(list, find by email, phone or your own id, get, create, change, and/v1/contacts/{id}/cases) and/v1/organizations(the same, and/contacts), a person never made twice (409contact_exists), and the scopecontacts:read(contacts:writeis now in use). - More about cases:
GET /v1/cases/search, the lists/v1/groups,/v1/agents,/v1/case-types,/v1/case-fieldsand/v1/tags, a case's files (/v1/cases/{number}/attachments,/v1/attachments/{id}) and its event log (/v1/cases/{number}/events), all withcases:read. - The reference at
api.nolmo.no/docs, the document at/v1/openapi.json, and three guides on nolmo.no: send cases in, answer a customer, receive webhooks. - Webhooks out:
/v1/webhook-endpointswith signed, replayablecase.created,case.updated,case.message.createdandcase.note.createdevents (scopewebhooks:manage). - Cases and contacts:
/v1/cases,/v1/cases/{number},/v1/cases/{number}/messagesand/v1/uploads, withIdempotency-Keyon every POST (scopescases:read,cases:write,cases:reply,contacts:write). - The public door:
api.nolmo.no/v1with API keys, andGET /v1/me.