Tickets
For Developers

Tickets

A ticket is a customer request. Through the API you can read, create, update, close and reopen tickets. Tickets can’t be deleted through the API.

  • Statuses: new, open, in_progress, pending (snoozed), resolved, closed.
  • Priorities: low, normal, high, urgent.
  • channel_type is the channel the ticket came from (telegram, widget, email, vk, whatsapp, billmgr…). Tickets created through the API have null channel_id and channel_type.
GET/api/v1/tickets

List tickets

All of the project’s tickets, paginated, newest first (by creation date).

Query parameters:

  • status, priority: one of the values above; anything else gives 422
  • contact_id: a contact’s UUID, only that contact’s tickets
  • since: ISO 8601, tickets 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
  • include_history: true adds last_message_preview (the first 100 characters of the latest message) and message_count to every ticket; internal notes aren’t counted
  • page (default 1), page_size (default 20, max 100)
curl
curl "https://api.support.forestsnet.com/api/v1/tickets?status=open&page_size=10&include_history=true" \
  -H "Authorization: Bearer sk_xxx"
200 OK
{
  "items": [
    {
      "id": "8a3f...",
      "workspace_id": "1c2b...",
      "contact_id": "44e1...",
      "channel_id": "c7d0...",
      "channel_type": "widget",
      "assigned_to": null,
      "department_id": null,
      "subject": "The confirmation code never arrives",
      "status": "open",
      "priority": "normal",
      "tags": ["billing"],
      "first_response_at": null,
      "resolved_at": null,
      "closed_at": null,
      "rating": null,
      "created_at": "2026-04-06T10:12:33.123456",
      "updated_at": "2026-04-06T10:12:33.123456",
      "last_message_preview": "Hi, the code never arrives",
      "message_count": 4
    }
  ],
  "total": 137,
  "page": 1,
  "page_size": 10
}
GET/api/v1/tickets/{ticket_id}

A ticket with its latest messages

Returns the ticket, its latest messages (in order, oldest first) and the contact. Operators’ internal notes are in the list too, with is_internal: true. The message format is on the Messages page.

Query parameters:

  • last_messages: how many of the latest messages to return (1–200, default 20)
curl
curl "https://api.support.forestsnet.com/api/v1/tickets/8a3f...?last_messages=50" \
  -H "Authorization: Bearer sk_xxx"
200 OK
{
  "ticket":   { "id": "8a3f...", "status": "open", ... },
  "messages": [ { "id": "f1...", "sender_type": "contact", "content": "Hello", ... } ],
  "contact":  { "id": "44e1...", "full_name": "John", "email": "john@example.com", ... }
}
POST/api/v1/tickets

Create a ticket

Always creates a new ticket, even when the contact already has an open one. The contact is looked up by telegram_id, then by email, and created if not found. contact needs at least one of the two; the name can go in name or full_name (it updates the name of a contact that was found).

  • contact: required, telegram_id (a number) and/or email, plus name
  • message: the first message’s text, saved as a message from the customer
  • subject: if missing, the first 120 characters of message
  • priority: normal by default
  • tags: an array of strings
POST /api/v1/tickets
{
  "subject": "Payment doesn't go through",
  "priority": "high",
  "tags": ["billing", "vip"],
  "contact": {
    "email": "john@example.com",
    "name": "John Smith"
  },
  "message": "I'm trying to pay for the plan and get error 500."
}
curl
curl -X POST https://api.support.forestsnet.com/api/v1/tickets \
  -H "Authorization: Bearer sk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "priority": "high",
    "contact": {"email": "john@example.com", "name": "John"},
    "message": "The code never arrives"
  }'

The response is 201 with the ticket object (as in the list, without the history fields) in status new. Errors: 400 without telegram_id and email, 422 for an unknown priority, 403 TICKET_LIMIT_REACHED when the monthly ticket limit is used up, 403 for a blocked contact.

PATCH/api/v1/tickets/{ticket_id}

Update a ticket

Changes status, priority, tags (the whole list is replaced) and subject. Send only the fields you need; null leaves a value unchanged. An unknown status or priority gives 422 and the ticket stays as it was.

PATCH body
{ "priority": "urgent", "tags": ["vip", "p1"] }
PATCH writes the fields and, like an edit in the dashboard, sends the ticket.updated webhook (ticket_id, workspace_id). It doesn’t set closed_at and doesn’t send the customer a closing message. A status change shows up in /updates as ticket.status_change. To close or reopen a ticket, use the endpoints below.
POST/api/v1/tickets/{ticket_id}/close

Close a ticket

Sets the closed status and closed_at (and resolved_at if it wasn’t set yet), records the resolution SLA, posts a system message about the closing in the conversation and sends it to the customer’s channel. Sends the ticket.closed webhook. An already closed ticket gives 409 TICKET_ALREADY_CLOSED. The response is the ticket object.

A close through the API is your side’s action, so the customer reads that support closed the conversation, with no operator name: «🔒 Conversation closed» or «🔒 Обращение закрыто оператором», in the customer’s language (the widget’s language, then the contact’s, then the project’s default). Widget tickets use the widget builder’s “ticket closed” text if it’s set; if that message is turned off there, none is posted. If ratings are on for the channel, the customer is asked to rate the conversation.

POST/api/v1/tickets/{ticket_id}/reopen

Reopen a ticket

Works only for tickets in closed or resolved; otherwise 422 TICKET_INVALID_STATUS_TRANSITION. Sets the open status, clears closed_at, resolved_at and first_response_at and restarts the SLA. The customer gets a notification if the channel’s settings have it on. Sends the ticket.reopened webhook.

More on tickets: a contact’s ticket history, ticket rating.

Was this page helpful?