Messages
Messages belong to a ticket. sender_type says who wrote it: contact (the customer), operator, bot (system messages and messages sent through the API), ai. is_internal: true marks an internal note the customer doesn’t see.
sender_id: the contact’s id for customer messages, the user’s id for operator messages. Abotmessage sent through the API has the project’s id here (that’s what sets it apart from system messages, which havenull).edited_at: when the message was edited, otherwisenull.media: attachments. Theirurlis relative (/api/v1/media/…); downloading is covered on the Media page.
/api/v1/tickets/{ticket_id}/messagesA ticket’s messages
In order, oldest first. Internal notes are included. A ticket that doesn’t exist gives an empty list.
Query parameters:
since: ISO 8601, messages created at or after that moment. A time with an offset is converted to UTC (13:30+02:00is 11:30 UTC); a time without one is taken as UTCpage(default 1),page_size(default 50, max 200)
{
"items": [
{
"id": "f1...",
"ticket_id": "8a3f...",
"workspace_id": "1c2b...",
"sender_type": "contact",
"sender_id": "44e1...",
"content": "Hello!",
"is_internal": false,
"media": [
{
"id": "m1...",
"url": "/api/v1/media/m1...",
"mime_type": "image/png",
"original_name": "screenshot.png",
"file_type": "png",
"width": 1280,
"height": 720,
"size": 184523
}
],
"created_at": "2026-04-06T10:12:33.123456",
"edited_at": null
}
],
"total": 4,
"page": 1,
"page_size": 50
}/api/v1/tickets/{ticket_id}/messagesPost to a ticket as the bot
The message is saved with sender_type: bot and the project’s id in sender_id. Body fields:
content: the text, up to 10,000 characters. It can be empty when there aremedia_ids; the message is then saved with the text[Attachment]. Empty text with no files gives 422MESSAGE_EMPTYis_internal:truefor an internal note,falseby defaultmedia_ids: ids of this project’s files uploaded withPOST /api/v1/media/upload. An id that isn’t a UUID gives 422VALIDATION_ERROR; so does a file that doesn’t exist or belongs to another project, with those ids listed indetail.media_ids. Either way nothing is saved. A file already attached to another message is copied: it comes back with a new id, and the other message keeps it
{
"content": "Thanks! Here are the instructions.",
"is_internal": false,
"media_ids": ["m1...", "m2..."]
}The response is 201 with the message and its attached files.
message.created event already lists them in media./api/v1/messages/{message_id}Edit a message’s text
Changes the content of a customer’s or operator’s message, or of a message sent through this API, and sets edited_at. If an operator’s message was sent to Telegram, the text changes there too. Limits:
- Of
botmessages, only those sent through the project’s API (with the project’s id insender_id) can be edited; operators can’t edit them in the dashboard. Systembotmessages andaireplies give 422VALIDATION_ERRORwith the reasonSystem messages cannot be edited - The project’s edit window:
message_edit_window_minutesminutes after sending (15 by default,0means no limit) - In
closedandresolvedtickets, only if the project hasmessage_edit_after_closeon - Empty text gives 422
MESSAGE_EMPTY
{
"content": "The updated message text"
}The response is the whole message. Sends the message.edited webhook. The current window and policy are in GET /api/v1/workspace/settings:
{
"id": "1c2b...",
"name": "My project",
"slug": "my-project",
"timezone": "Europe/Warsaw",
"message_edit_window_minutes": 15,
"message_edit_after_close": false,
"language": "ru"
}/api/v1/messagesAn incoming message from an external contact
For when an external system (a CRM, a bot, a form on your site) passes on a customer’s message. The contact is looked up by telegram_id, then by email, and created if not found. The message goes to the contact’s latest ticket that isn’t closed or resolved; if there is none, a new ticket is created (the subject is the first 120 characters of the text, and the ticket counts towards the plan’s monthly limit).
contact: required,telegram_id(a number) and/oremail, plusname. Without both identifiers: 422VALIDATION_ERRORmessage: the text, 1–10,000 characters, requiredsession_id: accepted but not used
This endpoint takes no attachments.
{
"contact": {
"email": "john@example.com",
"name": "John Smith"
},
"message": "Hi, the code never arrives"
}curl -X POST https://api.support.forestsnet.com/api/v1/messages \
-H "Authorization: Bearer sk_xxx" \
-H "Content-Type: application/json" \
-d '{"contact": {"email": "john@example.com"}, "message": "Hello"}'{
"ok": true,
"message": { "id": "f2...", "ticket_id": "8a3f...", "sender_type": "contact", ... }
}If the API channel is turned off on the platform, the response is 200 with {"ok": false, "message": "API channel is disabled"}. If the contact is blocked, nothing is saved and the response is 200 with {"ok": false, "message": null, "reason": "contact_blocked"}.
contact.extra_data.last_ip (the time goes to last_ip_at). When your server sends the request, that’s your server’s address, not the customer’s.
