Conversations
Methods in this section: 5. All methods
GET /conversations Available PII
#List conversations
Tenant conversations in ascending updated_at order, paged by cursor: to pick up changes, keep the updated_at of the last one you saw and pass it as updated_after. Conversations have no soft deletion, so include_deleted adds nothing for now. The response contains customer personal data: every disclosure is recorded in the disclosure log; on RU an IP allowlist and recipient country RU are required (otherwise 403 pii_transfer_not_allowed). Personal-mailbox conversations and student-portal chats are not returned. Personal-data fields (external_customer_id of a conversation; content and attachments of a message) are returned only when named in fields=. On the EN and ID cells their values may come in JWE format (the integration mode and the Encryption-Key-Id header); in mode required without a key — 409 encryption_key_required.
- Scopes
conversations:read
- Rate limit
- read
- Idempotency
- not required
- Successful response
200 A page of conversations.
Parameters
| Field | Type | Description |
|---|
limit | integer optional | In: query. Page size: 1 to 100, default 25. |
cursor | string optional | In: query. The next_cursor value from the previous response. Valid for 24 hours; start over when filters change. |
channel | string optional | In: query. Channel type: api, telegram, website … |
status | string optional | In: query. Conversation state, for example open. |
mode | string optional | In: query. ai — handled by the AI, operator — handled by a human. |
updated_after | string optional | In: query. Conversations changed strictly after the moment. ISO 8601 with a zone. |
include_deleted | boolean optional | In: query. Accepted for uniformity; changes nothing for now. |
fields | string optional | In: query. Comma-separated fields to return (id is always present). Without the parameter every field EXCEPT personal-data fields is returned; a personal-data field is returned only when named explicitly: top-level (title, content) or nested (contact.email, score.reason). An unknown name — 422. Only the returned fields are recorded in the disclosure log. |
Encryption-Key-Id | string optional | In: header. EN and ID cells only: which registered key (kid) to build the JWE with — the active one or the previous one for 7 days after a change. The header enables the JWE format even in mode off. |
Response body
| Field | Type | Description |
|---|
data | array<Conversation> required | |
next_cursor | string required, may be null | Cursor of the next page; null — the list is exhausted. Valid for 24 hours. |
Examples
Request
curl -X GET 'https://wfacademy.id/api/public/v1/conversations' \
-H 'Authorization: Bearer '"$API_KEY"
Response 200
{
"data": [
{
"id": "EXAMPLE_ID",
"channel": null,
"contact_id": null,
"external_customer_id": null,
"status": "string",
"mode": "ai",
"language": null,
"created_at": null,
"updated_at": "2026-10-11T09:00:00Z",
"_enc": [
null
]
}
],
"next_cursor": "EXAMPLE_CURSOR"
}
Error example
{
"type": "https://wfacademy.id/developers/errors#scope_missing",
"title": "Scope is missing",
"status": 403,
"code": "scope_missing",
"detail": "EXAMPLE",
"request_id": "req_EXAMPLE"
}
Examples are illustrative: the key in them is a placeholder (EXAMPLE), field values are made up.
Possible errors
invalid_api_key wrong_cell integration_inactive tenant_inactive ip_not_allowed feature_not_in_plan rate_limited wallet_suspended service_unavailable internal_error scope_missing pii_transfer_not_allowed validation_failed encryption_key_required
POST /conversations Available PII
#Find or create a conversation
A conversation of your channel customer (the integration api channel) by the external id: an active one is returned (200), otherwise a new one is created (201) with a conversation.created event. The same external_customer_id of two integrations means two different customers. Name, email and phone are written into empty contact fields only. Connect the channel in the dashboard (Settings → API); without it — 409 channel_not_configured. A retry with the same Idempotency-Key returns the same conversation. The response contains customer personal data: every disclosure is recorded in the disclosure log; on RU an IP allowlist and recipient country RU are required (otherwise 403 pii_transfer_not_allowed). Personal-mailbox conversations and student-portal chats are not returned.
- Scopes
conversations:write
- Rate limit
- write
- Idempotency
- Idempotency-Key is required
- Successful response
201 The conversation (new — 201, existing — 200).
Parameters
| Field | Type | Description |
|---|
Idempotency-Key | string required | In: header. A unique string (usually a UUID v4), new for every new operation. A retry with the same key and body yields the same result. |
Request body Your channel customer.
| Field | Type | Description |
|---|
external_customer_id | string required | A stable customer id in your system (not a phone or email). The platform finds or creates the contact and the conversation by it; ids of different integrations never mix. |
name | string optional | Customer name. Written only into an empty contact field. |
email | string optional | Customer email. Written only into an empty contact field. |
phone | string optional | Phone in international format (+…). Written only into an empty contact field. |
language | string optional | Language of a new conversation (ISO 639-1). The AI reply language is limited to the cell languages. |
Response body
| Field | Type | Description |
|---|
id | string required | Conversation id (uuid). |
channel | string required, may be null | Channel type: api, telegram, website … |
contact_id | string required, may be null | The platform contact (customer) id. |
external_customer_id | string optional, may be null | The customer id in your system. Channel api only; null for other channels (the messenger external id is not disclosed). Personal data: on reads it is returned only when named in fields=. On the EN and ID cells in JWE format — a compact JWE string. |
status | string required | Conversation state: open, handoff_pending, resolved, closed … The set may grow — do not treat the list as closed. |
mode | string required | Who handles the conversation: ai — the AI answers (modes auto and supervised), operator — a human answers (copilot and manual). Values: ai operator |
language | string required, may be null | Conversation language (ISO 639-1); null — not detected. |
created_at | string required, may be null | Conversation start, ISO 8601 UTC. |
updated_at | string required | Last change or message, ISO 8601 UTC. The walk order. The unread counter does not change it. |
_enc | array<string> optional | EN and ID cells only, when the JWE format is in effect: fields of this object whose values are replaced by a compact JWE string (the plaintext is the JSON of the original value). |
Examples
Request
curl -X POST 'https://wfacademy.id/api/public/v1/conversations' \
-H 'Authorization: Bearer '"$API_KEY" \
-H 'Idempotency-Key: '"$(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"external_customer_id": "EXAMPLE_ID",
"name": "string",
"email": "[email protected]",
"phone": "string",
"language": "string"
}'
Response 201
{
"id": "EXAMPLE_ID",
"channel": "string",
"contact_id": "EXAMPLE_ID",
"external_customer_id": "EXAMPLE_ID",
"status": "string",
"mode": "ai",
"language": "string",
"created_at": "2026-10-11T09:00:00Z",
"updated_at": "2026-10-11T09:00:00Z",
"_enc": [
"string"
]
}
Error example
{
"type": "https://wfacademy.id/developers/errors#scope_missing",
"title": "Scope is missing",
"status": 403,
"code": "scope_missing",
"detail": "EXAMPLE",
"request_id": "req_EXAMPLE"
}
Examples are illustrative: the key in them is a placeholder (EXAMPLE), field values are made up.
Possible errors
invalid_api_key wrong_cell integration_inactive tenant_inactive ip_not_allowed feature_not_in_plan rate_limited wallet_suspended service_unavailable internal_error scope_missing pii_transfer_not_allowed validation_failed bad_request payload_too_large channel_not_configured idempotency_key_required idempotency_conflict operation_in_progress invalid_state
GET /conversations/{id} Available PII
#Get a conversation
One conversation. A foreign, missing or non-public conversation all answer not_found alike. The response contains customer personal data: every disclosure is recorded in the disclosure log; on RU an IP allowlist and recipient country RU are required (otherwise 403 pii_transfer_not_allowed). Personal-mailbox conversations and student-portal chats are not returned. Personal-data fields (external_customer_id of a conversation; content and attachments of a message) are returned only when named in fields=. On the EN and ID cells their values may come in JWE format (the integration mode and the Encryption-Key-Id header); in mode required without a key — 409 encryption_key_required.
- Scopes
conversations:read
- Rate limit
- read
- Idempotency
- not required
- Successful response
200 The conversation.
Parameters
| Field | Type | Description |
|---|
id | string required | In: path. Conversation id (uuid). |
fields | string optional | In: query. Comma-separated fields to return (id is always present). Without the parameter every field EXCEPT personal-data fields is returned; a personal-data field is returned only when named explicitly: top-level (title, content) or nested (contact.email, score.reason). An unknown name — 422. Only the returned fields are recorded in the disclosure log. |
Encryption-Key-Id | string optional | In: header. EN and ID cells only: which registered key (kid) to build the JWE with — the active one or the previous one for 7 days after a change. The header enables the JWE format even in mode off. |
Response body
| Field | Type | Description |
|---|
id | string required | Conversation id (uuid). |
channel | string required, may be null | Channel type: api, telegram, website … |
contact_id | string required, may be null | The platform contact (customer) id. |
external_customer_id | string optional, may be null | The customer id in your system. Channel api only; null for other channels (the messenger external id is not disclosed). Personal data: on reads it is returned only when named in fields=. On the EN and ID cells in JWE format — a compact JWE string. |
status | string required | Conversation state: open, handoff_pending, resolved, closed … The set may grow — do not treat the list as closed. |
mode | string required | Who handles the conversation: ai — the AI answers (modes auto and supervised), operator — a human answers (copilot and manual). Values: ai operator |
language | string required, may be null | Conversation language (ISO 639-1); null — not detected. |
created_at | string required, may be null | Conversation start, ISO 8601 UTC. |
updated_at | string required | Last change or message, ISO 8601 UTC. The walk order. The unread counter does not change it. |
_enc | array<string> optional | EN and ID cells only, when the JWE format is in effect: fields of this object whose values are replaced by a compact JWE string (the plaintext is the JSON of the original value). |
Examples
Request
curl -X GET 'https://wfacademy.id/api/public/v1/conversations/EXAMPLE_ID' \
-H 'Authorization: Bearer '"$API_KEY"
Response 200
{
"id": "EXAMPLE_ID",
"channel": "string",
"contact_id": "EXAMPLE_ID",
"external_customer_id": "EXAMPLE_ID",
"status": "string",
"mode": "ai",
"language": "string",
"created_at": "2026-10-11T09:00:00Z",
"updated_at": "2026-10-11T09:00:00Z",
"_enc": [
"string"
]
}
Error example
{
"type": "https://wfacademy.id/developers/errors#scope_missing",
"title": "Scope is missing",
"status": 403,
"code": "scope_missing",
"detail": "EXAMPLE",
"request_id": "req_EXAMPLE"
}
Examples are illustrative: the key in them is a placeholder (EXAMPLE), field values are made up.
Possible errors
invalid_api_key wrong_cell integration_inactive tenant_inactive ip_not_allowed feature_not_in_plan rate_limited wallet_suspended service_unavailable internal_error scope_missing pii_transfer_not_allowed not_found validation_failed encryption_key_required
GET /conversations/{id}/messages Available PII
#Conversation messages
Conversation messages from oldest to newest, paged by cursor (created_at, id). Operator internal notes are not returned. Attachments are temporary signed links; external messenger file addresses are not disclosed. The response contains customer personal data: every disclosure is recorded in the disclosure log; on RU an IP allowlist and recipient country RU are required (otherwise 403 pii_transfer_not_allowed). Personal-mailbox conversations and student-portal chats are not returned. Personal-data fields (external_customer_id of a conversation; content and attachments of a message) are returned only when named in fields=. On the EN and ID cells their values may come in JWE format (the integration mode and the Encryption-Key-Id header); in mode required without a key — 409 encryption_key_required.
- Scopes
conversations:read
- Rate limit
- read
- Idempotency
- not required
- Successful response
200 A page of messages.
Parameters
| Field | Type | Description |
|---|
id | string required | In: path. Conversation id (uuid). |
limit | integer optional | In: query. Page size: 1 to 100, default 25. |
cursor | string optional | In: query. The next_cursor value from the previous response. Valid for 24 hours; start over when filters change. |
include_deleted | boolean optional | In: query. true — also return deleted messages as tombstones (deleted: true, no text). |
fields | string optional | In: query. Comma-separated fields to return (id is always present). Without the parameter every field EXCEPT personal-data fields is returned; a personal-data field is returned only when named explicitly: top-level (title, content) or nested (contact.email, score.reason). An unknown name — 422. Only the returned fields are recorded in the disclosure log. |
Encryption-Key-Id | string optional | In: header. EN and ID cells only: which registered key (kid) to build the JWE with — the active one or the previous one for 7 days after a change. The header enables the JWE format even in mode off. |
Response body
| Field | Type | Description |
|---|
data | array<Message> required | |
next_cursor | string required, may be null | Cursor of the next page; null — the list is exhausted. Valid for 24 hours. |
Examples
Request
curl -X GET 'https://wfacademy.id/api/public/v1/conversations/EXAMPLE_ID/messages' \
-H 'Authorization: Bearer '"$API_KEY"
Response 200
{
"data": [
{
"id": "EXAMPLE_ID",
"conversation_id": "EXAMPLE_ID",
"sender": "customer",
"content_type": "string",
"content": null,
"attachments": null,
"cards": [
null
],
"deleted": false,
"created_at": "2026-10-11T09:00:00Z",
"_enc": [
null
]
}
],
"next_cursor": "EXAMPLE_CURSOR"
}
Error example
{
"type": "https://wfacademy.id/developers/errors#scope_missing",
"title": "Scope is missing",
"status": 403,
"code": "scope_missing",
"detail": "EXAMPLE",
"request_id": "req_EXAMPLE"
}
Examples are illustrative: the key in them is a placeholder (EXAMPLE), field values are made up.
Possible errors
invalid_api_key wrong_cell integration_inactive tenant_inactive ip_not_allowed feature_not_in_plan rate_limited wallet_suspended service_unavailable internal_error scope_missing pii_transfer_not_allowed not_found validation_failed encryption_key_required
POST /conversations/{id}/messages Available PII
#Customer message and AI reply
Saves a customer message into a conversation of the integration channel (conversations:write) and, with ai_reply=auto and the ai_seller:invoke scope, starts the AI seller reply asynchronously: 202 with message_id, ai_reply.status=queued and a conversation.ai_reply operation. ?wait=N (0–20 s) waits for the SAME job: in time — 200 with the result (ai_reply, reply.id, operation.usage.cost in the wallet currency), otherwise — 202. Without ai_seller:invoke or with ai_reply=none — 201, ai_reply.status=skipped, reason=not_requested; the AI is not called and nothing is charged. The AI reply text is not here — read GET /conversations/{id}/messages or wait for message.created. A retry with the same Idempotency-Key while running returns the same 202, afterwards the result; the message is never written or charged twice. Paid part: at most 5 concurrent operations per integration (429 concurrency_limit), the per-operation cost cap and the integration daily limit (402 spend_limit_reached) are checked before writing; a refusal writes nothing and frees the Idempotency-Key. A conversation of another channel or tenant — 404, a closed one — 409 invalid_state.
- Scopes
conversations:write
- Rate limit
- write
- Idempotency
- Idempotency-Key is required
- Successful response
202 Accepted; the AI reply is being prepared (202), ready within wait (200) or not requested (201).
Parameters
| Field | Type | Description |
|---|
id | string required | In: path. Conversation id (uuid). |
Idempotency-Key | string required | In: header. A unique string (usually a UUID v4), new for every new operation. A retry with the same key and body yields the same result. |
wait | integer optional | In: query. Seconds to wait for the AI reply, 0–20. Not part of the idempotency key. |
Request body The customer message.
| Field | Type | Description |
|---|
content | string required | The customer message text, up to 4000 characters. |
ai_reply | string required | auto — request an AI seller reply (needs the ai_seller:invoke scope; without it the message is only saved); none — only save the message. Values: auto none |
Response body
| Field | Type | Description |
|---|
message_id | string required | The id of the saved customer message. |
conversation_id | string required | The conversation. |
ai_reply | AiReply required | |
reply | object optional | replied only: the AI reply id. No text here — read it with GET /conversations/{id}/messages (the disclosure is logged). |
reply.id | string required | |
operation | Operation optional | The conversation.ai_reply operation: queued or running with 202, finished with 200 (usage.cost — charged to the wallet in its currency). Absent when no AI reply was requested. |
Examples
Request
curl -X POST 'https://wfacademy.id/api/public/v1/conversations/EXAMPLE_ID/messages' \
-H 'Authorization: Bearer '"$API_KEY" \
-H 'Idempotency-Key: '"$(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"content": "string",
"ai_reply": "auto"
}'
Response 202
{
"message_id": "EXAMPLE_ID",
"conversation_id": "EXAMPLE_ID",
"ai_reply": {
"status": "replied",
"reason": "operator_active"
},
"reply": {
"id": "EXAMPLE_ID"
},
"operation": {
"id": "EXAMPLE_ID",
"type": "string",
"status": "queued",
"created_at": "2026-10-11T09:00:00Z",
"completed_at": "2026-10-11T09:00:00Z",
"resource": {
"type": "string",
"id": "EXAMPLE_ID"
},
"error": {
"code": "string",
"message": "string"
},
"usage": {
"cost": null,
"model": null
}
}
}
Error example
{
"type": "https://wfacademy.id/developers/errors#scope_missing",
"title": "Scope is missing",
"status": 403,
"code": "scope_missing",
"detail": "EXAMPLE",
"request_id": "req_EXAMPLE"
}
Examples are illustrative: the key in them is a placeholder (EXAMPLE), field values are made up.
Possible errors
invalid_api_key wrong_cell integration_inactive tenant_inactive ip_not_allowed feature_not_in_plan rate_limited wallet_suspended service_unavailable internal_error scope_missing pii_transfer_not_allowed validation_failed bad_request payload_too_large not_found channel_not_configured invalid_state idempotency_key_required idempotency_conflict operation_in_progress concurrency_limit spend_limit_reached upstream_unavailable