Messages
For Developers

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. A bot message sent through the API has the project’s id here (that’s what sets it apart from system messages, which have null).
  • edited_at: when the message was edited, otherwise null.
  • media: attachments. Their url is relative (/api/v1/media/…); downloading is covered on the Media page.
GET/api/v1/tickets/{ticket_id}/messages

A 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:00 is 11:30 UTC); a time without one is taken as UTC
  • page (default 1), page_size (default 50, max 200)
200 OK
{
  "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
}
POST/api/v1/tickets/{ticket_id}/messages

Post 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 are media_ids; the message is then saved with the text [Attachment]. Empty text with no files gives 422 MESSAGE_EMPTY
  • is_internal: true for an internal note, false by default
  • media_ids: ids of this project’s files uploaded with POST /api/v1/media/upload. An id that isn’t a UUID gives 422 VALIDATION_ERROR; so does a file that doesn’t exist or belongs to another project, with those ids listed in detail.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
body
{
  "content": "Thanks! Here are the instructions.",
  "is_internal": false,
  "media_ids": ["m1...", "m2..."]
}

The response is 201 with the message and its attached files.

The message appears in the ticket in the dashboard and in the website widget, but it is not sent to Telegram, VK, WhatsApp or email: customers on those channels won’t get it. The ticket status doesn’t change. Files are attached together with the message, so the message.created event already lists them in media.
PATCH/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 bot messages, only those sent through the project’s API (with the project’s id in sender_id) can be edited; operators can’t edit them in the dashboard. System bot messages and ai replies give 422 VALIDATION_ERROR with the reason System messages cannot be edited
  • The project’s edit window: message_edit_window_minutes minutes after sending (15 by default, 0 means no limit)
  • In closed and resolved tickets, only if the project has message_edit_after_close on
  • Empty text gives 422 MESSAGE_EMPTY
body
{
  "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:

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"
}
POST/api/v1/messages

An 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/or email, plus name. Without both identifiers: 422 VALIDATION_ERROR
  • message: the text, 1–10,000 characters, required
  • session_id: accepted but not used

This endpoint takes no attachments.

body
{
  "contact": {
    "email": "john@example.com",
    "name": "John Smith"
  },
  "message": "Hi, the code never arrives"
}
curl
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"}'
200 OK
{
  "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"}.

The request’s IP address is saved to 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.
Was this page helpful?