Conversations & Reply SLA
Quolle automatically threads the inbound mail you receive with the replies you send, and tracks your First Response Time against a target you set. Use it to see which customer emails are still waiting on a reply — and which have breached your SLA.
How threading works
When an inbound email arrives on one of your routes, Quolle groups it into a conversation by
the sender's address and the normalized subject (so "Re: Where is my order?"
threads onto the original "Where is my order?"). The clock starts on that first
inbound message.
When you reply — by sending a single-recipient email back to
that same person with the same subject — Quolle records it as your response, stops the clock,
and marks the conversation replied. A new inbound message on a replied thread
reopens it.
Setting the SLA target
The SLA is configured per inbound route, in hours, when you create the route. It defaults to 24 hours — the common target for email support.
curl -X POST https://api.quolle.com/v1/inbound/routes \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"matchAddress": "[email protected]",
"webhookUrl": "https://yourapp.com/inbound/quolle",
"slaHours": 4
}'
slaHours accepts 1–168 (up to a week). You can also set it from the
Settings → Inbound tab in your dashboard.
Authentication
The conversation endpoints are authenticated with your dashboard session (a JWT bearer token), the same as inbound route management — not an API key. They power the dashboard's Conversations view.
List conversations
GET /v1/conversations
| Query | Description |
|---|---|
status | Filter by awaiting_reply, replied, or closed |
limit | Page size, max 100 (default 50) |
offset | Pagination offset (default 0) |
// Response 200
{
"conversations": [
{
"id": "uuid",
"participant": "[email protected]",
"subject": "Where is my order?",
"status": "awaiting_reply",
"firstInboundAt": "2026-07-14T10:03:00.000Z",
"lastInboundAt": "2026-07-14T10:03:00.000Z",
"firstReplyAt": null,
"firstResponseSeconds": null,
"slaSeconds": 86400,
"slaBreached": false,
"messageCount": 1,
"updatedAt": "2026-07-14T10:03:00.000Z",
"awaitingReply": true,
"secondsAwaiting": 5400,
"breached": false
}
],
"total": 1,
"counters": { "awaiting": 1, "breached": 0 }
}
| Field | Description |
|---|---|
status | awaiting_reply, replied, or closed |
firstResponseSeconds | Time to your first reply, set once you respond |
slaSeconds | The route's SLA target in seconds |
slaBreached | Frozen result once replied — whether FRT exceeded the SLA |
awaitingReply | Live: is the thread currently waiting on you |
secondsAwaiting | Live: how long the newest unanswered message has waited |
breached | Live: past SLA and still awaiting (or the frozen result once replied) |
counters.breached counts every awaiting conversation past its own SLA target —
across all pages, not just the returned page.
Get one conversation
GET /v1/conversations/:id — returns the conversation plus its full message thread.
// Response 200
{
"conversation": { "id": "uuid", "participant": "[email protected]", "status": "replied", "...": "" },
"messages": [
{
"id": "uuid",
"direction": "inbound",
"fromAddr": "[email protected]",
"toAddr": "[email protected]",
"subject": "Where is my order?",
"snippet": "Hi, order ORD-8821 hasn't arrived…",
"messageId": "<[email protected]>",
"emailId": null,
"createdAt": "2026-07-14T10:03:00.000Z"
},
{
"id": "uuid",
"direction": "outbound",
"toAddr": "[email protected]",
"subject": "Re: Where is my order?",
"emailId": "the-sent-email-id",
"createdAt": "2026-07-14T11:20:00.000Z"
}
]
}
Outbound messages carry emailId, linking back to the sent email in your
logs. Only a short snippet of the body is stored.
Close or reopen
PATCH /v1/conversations/:id
curl -X PATCH https://api.quolle.com/v1/conversations/<id> \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{ "status": "closed" }'
Set status to closed to resolve a thread, or
awaiting_reply to reopen it.