How tickets work
The ticket queue, the three ways a ticket gets created, and what the statuses and priorities actually mean
How tickets work
A ticket is a support request with an email address attached to it. Tickets live in Support Tickets in your dashboard, separate from the live conversations in your Inbox. The difference matters: a conversation is a chat session, a ticket is a durable record you can reply to by email long after the visitor has closed the widget.
Every ticket belongs to one account. All ticket endpoints are scoped to the logged-in account — a ticket owned by someone else responds as if it does not exist.
<!-- SCREENSHOT: Support Tickets panel showing the All / Open / In Progress / Resolved tabs with a ticket list -->
The ticket queue
The queue lists your tickets newest first, 20 per page.
Four filter tabs sit at the top:
- All
- Open
- In Progress
- Resolved
Two things about this list are worth knowing before you rely on it:
- There is no Closed tab. Closed tickets are only reachable through All. If you close tickets, they do not disappear, but they have no dedicated view.
- Search only searches the page you are on. The search box filters the 20 tickets already loaded — matching on email, message body and name — rather than querying your whole queue. To search older tickets, page to them first.
- The count next to All is a true total. The counts next to Open, In Progress and Resolved are computed from the tickets loaded on the current page, so they are not whole-queue totals.
Opening a ticket shows the full message, the reply thread, and — when the same email address has more than one ticket — a Past tickets list of up to five earlier tickets from that customer.
Statuses
A ticket is always in one of four states:
open — the default for every newly created ticket
in_progress — set automatically the first time you reply to an open ticket, or manually with Start
resolved
closed
The API does not validate the status string it is given. The dashboard only styles these four values; anything else is rendered as the raw string.
Priorities
low
normal — the default
high
urgent
Priority is not validated either. It is stored exactly as sent. One consequence worth knowing: the chat widget's own ticket form offers Low / Medium / High, and "Medium" is sent as medium — which is not one of the four values above. Such a ticket saves fine and works normally, but it shows the neutral default badge and none of the four priority buttons appear selected until you set a real priority.
You can change priority at any time from the ticket detail view.
Where tickets come from
There are exactly three ways a ticket gets created.
1. Automatically, when a chat conversation escalates
This is the main path — most tickets arrive this way. When a chat conversation escalates to a human, Wallu creates a ticket provided it has an email address for the visitor.
The ticket body is assembled for you: the escalation reason in plain language, the AI handoff summary when one was generated, the last 12 messages of the conversation, and the visitor's latest message. The whole thing is capped at 5000 characters.
Priority is set from the reason:
- AI flagged it for a human →
high
- AI was unsure of the answer (low confidence) →
high
- Customer asked to speak with a human →
normal
The ticket is tagged with a JSON blob recording source: "chat_escalation", the originating conversation id, and the reason.
Two emails go out on this path: a notification to your account email, and a confirmation to the customer telling them their ticket exists.
If there is no email address, behaviour splits. When a visitor explicitly asks for a human, the bot asks for their email first and creates the ticket once they reply with one. When the AI escalates on its own and no email is on file, no ticket is created — the conversation still shows up in your Inbox, but nothing lands in Support Tickets.
2. Convert to Ticket, from the Inbox
Open a conversation in the Inbox and use Convert to Ticket. The modal asks for the customer's email address; the name is filled in from the conversation and is read-only.
The ticket body becomes the customer's own messages joined together, capped at 2000 characters. Priority is always normal and status is open.
Two caveats:
- No emails are sent on this path — not to you, not to the customer. The customer does not know a ticket exists until you reply.
- The ticket is not linked back to the conversation. Converting the same conversation twice creates two separate tickets.
<!-- SCREENSHOT: Convert to Ticket modal in the Inbox asking for the customer email -->
3. POST /api/tickets
The public endpoint, used by the chat widget's ticket form and available for your own integrations. It is the only ticket endpoint that needs no login. See the next article for exact request and response shapes.
Outside business hours
If you have business hours enabled with a timezone set, and a message arrives when your schedule says you are closed, Wallu suppresses the *live* handoff but still captures the request:
- The visitor gets your offline message instead of a handoff reply.
- The conversation stays
open — it is not marked escalated, so nobody is paged at 2am.
- A ticket is still created (tagged with reason
after_hours), together with an AI handoff summary, as long as Wallu has an email address for the visitor — and you get the same "[New Ticket]" notification email you would during hours. What business hours suppress is only the *live* handoff: the conversation is not marked escalated, so no agent is paged to jump into the chat in real time.
So a request that arrives at 2am is in your ticket queue in the morning — provided the visitor gave an email. If Wallu has no email for them (the AI escalated on its own and nothing is on file), there is nothing to reply to, so no ticket is created; the conversation is still in your Inbox.
A day that is missing from your schedule, or present but not enabled, counts as closed. These checks only run when business hours are enabled and a timezone is set — if either is missing, escalation proceeds normally at any hour and the team is notified as usual.
Re-escalating: what happens on the second escalation
When an escalation creates a ticket, the ticket id is cached on the conversation. If that same conversation escalates again, Wallu looks the ticket up:
- If the ticket is still
open or in_progress, the escalation folds into it. No second ticket, and no duplicate notification emails.
- If the ticket was resolved, closed or deleted, a new ticket is created.
So it is not "one ticket per conversation" — it is at most one *live* ticket per conversation. A returning customer whose earlier ticket you already resolved gets a fresh ticket rather than reopening the old one, which is usually what you want, since chat conversation ids are reused across visits.
Escalation does not silence the AI
Worth setting expectations: creating a ticket does not stop the bot from replying to that conversation. The AI only stands down when a conversation is escalated and has an assigned agent. Assignment only happens if you have configured routing (round-robin or least-busy) and have eligible team members — the default routing mode is manual, which assigns nobody. On the default setup an escalated conversation still gets AI replies.
To reliably stop the AI on a conversation, open it in the Inbox and use Pause AI (there is no separate "take over" button — see the inbox guide).
Note also that this round-robin assignment applies to conversations, not tickets. Tickets are never auto-assigned.
Replies, resolving, and the API
Reply to a ticket, add internal notes, resolve it, and the exact REST endpoints with their real responses
Replies, resolving, and the API
Replying to a ticket
Open a ticket and type in the reply box at the bottom. Your reply is stored in the thread and emailed to the address on the ticket.
The email is signed with your agent display name, or Support Team if you have not set one — your own email address is never used as the sign-off. It quotes the customer's original message back to them. For chat-escalated tickets, it quotes the visitor's actual last message rather than the internal handoff summary that forms the ticket body.
Any reply moves an open ticket to in_progress — including an internal note.
<!-- SCREENSHOT: Ticket detail view with the reply box, internal-note lock toggle, and Start / Resolve / Close buttons -->
Customer replies to those emails go nowhere
This is a real limitation, and it is the one most likely to bite you.
Reply emails are sent with a Reply-To of ticket+<id>@wallu.ai, and the email footer reads *"Reply directly to this email to respond."* The inbound handler for that address is not mounted. If a customer replies to the email, nothing arrives — the reply does not appear on the ticket, it does not reach your inbox, and neither side is told it was dropped.
Email threading on tickets does not currently work. Until it does, treat ticket emails as outbound-only: ask customers to come back through chat, or give them an address you actually monitor.
Internal notes
The lock icon above the reply box switches the composer into internal-note mode. An internal note is stored on the thread and shown in amber, and is not emailed to the customer. Use it for context for your team.
Remember that a note still bumps an open ticket to in_progress.
Resolving and closing
The ticket detail view has Start, Resolve and Close buttons; a button is hidden when the ticket is already in that state. You can also select tickets with the checkboxes in the list to resolve or delete several at once.
When a ticket moves to resolved or closed:
resolvedAt is stamped with the current time.
- The customer is emailed *"Your support ticket has been closed"*, inviting them back if the issue returns. This only fires on an actual change of status, so re-saving the same status does not email them twice. It needs an email on the ticket, and its
Reply-To is your account email when you have one set.
Reopening
There is no Reopen button — the detail view only offers Start, Resolve and Close. You can move a ticket back to open through the API, but be aware of what that does not do:
resolvedAt is not cleared on reopen. It keeps the timestamp from when the ticket was previously resolved, so a reopened ticket carries a resolution time that has already passed.
- Re-sending
resolved or closed on an already-resolved ticket overwrites resolvedAt with a fresh timestamp.
If you report on resolution times, do not treat resolvedAt as "the moment this ticket was first resolved".
Deleting
Deleting a ticket is permanent and takes its replies and internal notes with it. There is no archive to recover it from.
API reference
All authenticated routes use your dashboard session cookie and are scoped to your account. A ticket that belongs to another account returns 404, not 403.
In production, 5xx bodies are stripped of internal detail — a failed request returns only the short label shown below, with no details field, whatever the underlying cause.
POST /api/tickets
Public. No authentication. Rate limited to 5 submissions per IP per 10 minutes.
Body:
email — required
message — required
widgetToken or agentId — one of the two is required, to route the ticket to an account
name — optional
priority — optional, defaults to normal
widgetToken is the widget's server-verified secret and takes precedence: when a token is present, any agentId in the body is ignored entirely. agentId still works for direct API use.
Responses:
200 — {"success":true,"ticketId":"...","message":"Ticket created successfully"}
400 — {"error":"Email and message are required"}
400 — {"error":"widgetToken or agentId is required to route the ticket"}
404 — {"error":"Invalid widget token"} when a token was sent but matches no account
404 — {"error":"Agent not found"} when an agentId was sent but matches no account
429 — {"error":"Too many ticket submissions. Please try again later."}
500 — {"error":"Failed to create ticket"}
A malformed token or id is treated as not found, so it returns 404 rather than a validation error. Branch on the status code plus the error string: an unroutable ticket is a 404, not a 400.
This endpoint emails you about the new ticket. It does not send the customer a confirmation — only the chat-escalation path does that.
POST /api/tickets/from-inbox
Authenticated. Backs Convert to Ticket.
Body: conversationId, name, email, message, priority.
conversationId is accepted but not stored — the resulting ticket has no link back to the conversation, and calling this twice for the same conversation creates two tickets. No emails are sent.
200 — {"success":true,"ticketId":"...","message":"Ticket created from conversation"}
400 — {"error":"Email and message are required"}
GET /api/tickets
Authenticated. Lists your tickets.
Query parameters:
status — filter by status; all (or omitted) returns everything
limit — defaults to 50
offset — defaults to 0
sortBy — defaults to createdAt; use a real ticket column
sortOrder — defaults to desc
Returns {"tickets":[...],"total":N,"limit":N,"offset":N}. Each ticket carries replyCount and lastReply (the most recent reply, or null).
GET /api/tickets/:id
Authenticated. Returns the ticket with all replies, oldest first. Internal notes are included, so do not pass this response through to a customer unfiltered.
404 — {"error":"Ticket not found"}
GET /api/tickets/:id/replies
Authenticated. Returns {"replies":[...]}, oldest first. Internal notes included here too.
PATCH /api/tickets/:id
Authenticated. Updates a ticket. Accepts status, priority, tags and assignedTo. tags is serialised to JSON for you.
Returns the updated ticket, or 404 {"error":"Ticket not found"}.
Setting status to resolved or closed stamps resolvedAt and may email the customer, as described above.
DELETE /api/tickets/:id
Authenticated. Returns {"success":true}. Deletes the ticket's replies with it.
POST /api/tickets/:id/reply
Authenticated. Body:
message — required, must not be blank
isInternal — optional, defaults to false
Sends the reply as email when isInternal is false; stores it without emailing when true. Either way, an open ticket becomes in_progress.
200 — {"success":true,"reply":{...},"message":"Reply sent successfully"}, or "Internal note added" for an internal note
400 — {"error":"Reply message is required"}
404 — {"error":"Ticket not found"}
The 200 reports that the reply was stored. Email delivery is logged separately, so a successful response does not guarantee the email reached the customer.
Known limits
Collected in one place, so nothing here surprises you later:
- You cannot assign a ticket to a teammate. Tickets have an
assignedTo field and PATCH accepts it, but no part of the dashboard sets it and nothing reads it. Round-robin auto-assignment applies to Inbox conversations, not tickets.
- Customer email replies are dropped. Ticket emails invite a direct reply, but the inbound address is not wired up. See above.
- No Closed tab, and search covers only the current page.
- No Reopen button, and
resolvedAt survives a reopen.
- Converting a conversation twice creates two tickets — there is no link back to the conversation on that path.
For how escalation decides to hand a conversation over in the first place, see Unified Inbox.