---
id: saas/suporte-por-cliente
industry: SaaS and technology
usage: specific
effort: medium
grain: Ticket (fact) and customer x month (aggregate)
---

# Support by customer, with wait time, resolution and reason

How many tickets each paying customer opened, how long they waited for a person and why, with the same volume your support tool's report shows.

## Executive summary

**For:** SaaS companies that need to know which customers lean on support the most, how long they wait and what is breaking in the product.

**What it solves**

- Every ticket tied to the paying customer, with their MRR next to it.
- Wait time measured from the moment the customer wrote, split between AI and human.
- Reason and module of every ticket ready for product to prioritize.

**Why it matters:** Tickets per customer is one of the most used health score signals, and it is almost always skewed: tickets without a company, chats that started before the ticket, a bot counted as an agent. Without the link to billing, support prioritizes whoever writes the most, not whoever pays the most. And with an AI agent handling much of the queue, first response improves while whoever needs a person keeps waiting.

**Business questions it answers**

- Which customers opened the most tickets this month, compared with last month?
- How long does a customer wait to talk to a person, at the median and p90?
- How much of the queue did the AI agent resolve on its own and how much was handed to the team?
- Which modules generate the most bug tickets, and from which customers?
- Are we meeting the SLA for top accounts?
- How many tickets have no owner right now?

**What you need**

- Your support tool connected to Nekt (conversations, tickets and messages)
- The customer key stored on the ticket or the contact, tied to billing
- A closed list of reasons and modules, and the SLA agreed per customer type

**What you get**

- A ticket table with customer, channel, who handled it, times and reason
- A customer x month aggregate ready for the health score
- A dashboard of SLA, unowned queue and reopens by agent and team
- An AI agent that answers support questions with the right cut already applied

## Detailed

### The problem

The support tool counts conversations the customer never opened, tickets without a company and bot messages as replies. Response time starts when the ticket is created, not when the customer wrote, and an empty time becomes zero in the average. The result is a per-customer volume that doesn't match the tool's report, a response time that looks too good and a health score that punishes the wrong customer.

### Business rules

1. **Count only valid tickets, the way the tool's report does.** In Intercom, a conversation counts when it wasn't triggered by automation (source.delivered_as other than automated) and the customer replied (first_contact_reply filled in). Proactive messages, campaigns and reminders stay out. Reproduce the tool's native report rule first and only then add your own cuts, publishing the difference when there is one. (Avoid: Counting every conversation created, including those triggered by the team or by automation.)
2. **External and internal always separate.** Internal requests (finance asking support, operations tasks) live alongside customer tickets in the same tool. Separate them by a ticket type field, not by team name: one team can have both. Remove internal and test companies. When the question doesn't say which cut, the agent asks before answering. (Avoid: Adding the team's internal tickets to the customer's volume.)
3. **Every ticket tied to the customer, in a cascade.** Tickets often arrive without a company. Resolve in order: the ticket's own company, the contact's company, email, normalized phone (area code plus the last 8 digits). Email domain only counts if it isn't a free provider. A company mapping is used only when it is one-to-one. Record how the customer was identified (direct, mapping or unidentified) and publish the link rate each month. (Avoid: Counting only tickets that already come with a company, or picking the most frequent customer for an ambiguous id.)
4. **The clock starts at the customer's first message.** In chat, the customer writes before the ticket exists: in a real base, the first message was at 07:43 and the ticket at 07:59. Wait time is the company's first reply after the customer's first message, computed from the messages. If the company spoke first (outbound support), count from the customer's first message, otherwise the time comes out negative. Conversations with inferred timestamps, such as one crossing the new year without a full date, stay out of SLA. (Avoid: Measuring response from the ticket's created_at.)
5. **AI and human are different times.** With an AI agent replying first, first response drops to seconds and hides the wait for a person. Classify each ticket as AI only, AI with handoff, human only, no reply or no transcript, and measure the wait until the first human reply separately. Native agent-reply counters may ignore the AI's message: keep them for audit and compute from the messages. In Intercom, an assumed AI resolution (assumed_resolution in ai_agent.resolution_state) is not a resolution confirmed by the customer. (Avoid: A single first response, mixing AI and human.)
6. **Empty time is not zero, and an average doesn't summarize.** A ticket with no human reply has an empty time. Replacing empty with zero drags the average down. Summarize times by median and p90, only over eligible tickets (chat with a transcript and reliable timestamps), and show how many were left out. If the SLA is in business hours, use the business-hours time the tool provides (in Intercom, first response in office hours in the conversation statistics) and say which of the two you are showing. (Avoid: Average with empty replaced by zero, over every ticket.)
7. **Resolution to first close; reopens on their own.** Resolution time runs from creation to the first close. A reopened ticket (hs_ticket_reopened_at in HubSpot, count_reopens in Intercom) goes into its own reopen metric by agent, a sign of poor closing. In HubSpot, time_to_close, time_to_first_agent_reply and time per stage (hs_v2_cumulative_time_in_<stage>) come in milliseconds: convert once, in the first layer. (Avoid: Using the last close and hiding the reopen inside the time.)
8. **No owner and no category are data too.** In a real base, almost half the tickets had no owner and the category was empty for most of them. Label a null owner as Unassigned and never discard it: the unowned queue is a signal of support health. Use queue, pipeline or tier (L1, L2, L3) as the main breakdown when the category isn't reliable, and map statuses in more than one language (Resolvido and Closed) to open, pending and closed. (Avoid: Filtering out tickets with no owner or no category.)
9. **A customer's volume compared with itself.** A customer with 200 users opens more tickets than one with 5. For the health score, use the customer x month aggregate with change versus last month and a 3-month moving average, and normalize by active users or by MRR. Separate questions from bugs: questions point to onboarding and docs, bugs point to product. The share of questions uses only closed tickets classified as question or bug. (Avoid: Ranking customers by raw ticket volume.)
10. **SLA comes from the contract, by customer type.** The target depends on the customer, for example 24 hours for top accounts and 72 hours for the rest, in business days. Store the target on the ticket and flag a breach when it was resolved after the target or is still open past it. Old tickets from before SLA automation have no target and stay out of the denominator, never counted as met. (Avoid: One SLA for everyone, and tickets without a target counted as on time.)

### Who handled the ticket

From the transcript messages, after the customer's first message.

- **only AI messages** → Resolved by AI
- **AI first, then a person** → Handed off (`human wait = 1st human reply - customer's 1st message`)
- **only messages from team members** → Human
- **no message from the company** → No reply
- **no transcript or inferred timestamp** → Out of SLA, counts in volume

### Definitions

| Metric | Definition |
|---|---|
| Valid ticket | conversation or ticket opened by the customer, not triggered by automation or by the team, with a customer reply, excluding internal and test companies; counted on the creation date |
| Wait time | from the customer's first message to the company's first reply (AI or human) after it, per ticket, in seconds |
| Human wait | from the customer's first message to the first reply from a team member; empty when only the AI handled it |
| Resolution time | from creation to the first close, per ticket; reopens measured separately |
| AI resolution rate | chat tickets handled only by the AI / valid chat tickets, per month |
| Reopen rate | tickets closed in the month with at least one reopen / tickets closed in the month, by agent and team |
| SLA breach | ticket resolved after the target, or open past the target on the reference date / tickets with a target |
| Tickets per customer | valid external tickets from the customer in the month, with change versus last month and a 3-month moving average |
| Link rate | valid tickets tied to a customer / valid tickets, per month and by identification method |

### Pitfalls

- **One company id for more than a thousand customers.** The mapping picked the most frequent customer for each company id in the tool. One of those ids belonged to the internal agent who opened tickets on customers' behalf, and thousands of tickets landed on a single customer, who showed up as the heaviest support user. How to avoid: Accept only one-to-one mappings; the rest stays unidentified.
- **Tickets counted twice through the domain.** Joining a company x domain table to the tickets table duplicated rows, because one company had several login emails. The counter showed about 13% more tickets than existed and fed the health score that way. How to avoid: Resolve the customer per ticket and count by ticket id.
- **History load doubled.** The load of years of tickets wrote in append mode, and reprocessing a file repeated identical tickets. Every counter came out inflated without any error. How to avoid: Deduplicate by ticket id at the entrance of the metrics layer.
- **Empty became zero in the average.** The calculation replaced empty response times with zero before averaging, and the team's first response looked much better than it was. How to avoid: Remove empties from the average, use median and p90 and show the count.
- **The AI improved the average, the customer waited the same.** With the AI agent up front, first response dropped to seconds. The wait for a person on handed-off tickets didn't change, and nobody saw it because there was only one time column. How to avoid: Publish human wait next to first response.
- **Statuses in two languages.** Resolvido and Closed lived in the same field, and part of the closed tickets was counted as open. How to avoid: Map every status to open, pending and closed in a table.

Validated on real data: In Nekt's own Intercom account, about 85% of conversations involved the AI agent and more than 6 in 10 had no human reply at all. Replacing the empty time with zero drops the average first human response to just over a third of its real value, and even without the empties the average is dozens of times above the median because of the long tail. No conversation carries the company on its own record, and most AI resolutions are assumed, not confirmed by the customer.

### Acceptance test

- [ ] The number of valid tickets per month matches the tool's native report (difference under 2%).
- [ ] No ticket has a negative wait time, and no empty time is counted as zero.
- [ ] The customer link rate is published each month, and a ticket without a customer shows as unidentified rather than disappearing.
- [ ] AI only + handed off + human + no reply + out of SLA = total valid tickets.
- [ ] Each ticket appears once (unique id), even after reprocessing the load.

### Context document

```text
Support by customer
- Valid ticket = opened by the customer, not triggered by automation or by the team, with a customer reply. In Intercom: source.delivered_as other than automated and first_contact_reply filled in.
- Always separate customer (external) tickets from the team's internal tickets, by ticket type. If the question doesn't say which cut, ask.
- Internal and test companies are excluded. A ticket with no identified customer counts in volume as unidentified.
- Wait time starts at the customer's first message, not at ticket creation. Human wait is measured separately; empty when only the AI handled it.
- Handling type: AI only, handed off, human, no reply, out of SLA.
- Times are summarized by median and p90 over eligible tickets. Never replace an empty time with zero.
- Resolution = creation to first close. Reopens are their own metric.
- Null owner = Unassigned. Never discard.
- HubSpot times come in milliseconds.
- SLA: target per customer type stored on the ticket. Tickets without a target stay out of the SLA calculation.
Per-client exceptions:
- <client>: SLA of <hours> business hours by contract.
- <client>: tickets opened through channel <channel> are excluded from experience time.
```

### Variations

- **Support over WhatsApp:** The ticket is the group or the number: tie it to the customer by the normalized group name or the last 8 digits of the phone, and anonymize phone, email and tax ID before exporting the text.
- **Maintenance with a contractual SLA:** The backlog becomes a monthly snapshot of open tickets, with no cut by opening date, with aging and escalation across L1, L2 and L3.
- **Tickets by module for product:** An affected-module field on the ticket ties each bug to the part of the product and to the customer, and product prioritizes by affected customers and MRR.
