# Tickets (/tickets)

What a ticket is, how its orders are linked and verified, and when to refresh it.

A **ticket** is one support conversation together with its customer and the
orders linked to it. You read tickets in OneInbox, through [MCP](/mcp), or with the
[Context API](/reference/tickets).

A conversation has a ticket when the support classifier judged its latest
customer message a support request. See [List tickets](/reference/tickets#list-tickets)
for the details.

## Stored, not live [#stored-not-live]

Reading a ticket returns what EcomSquad has already stored. It never contacts the
store. A separate [refresh](#refresh-a-ticket) asks the store for current order
information, stores the result, and returns the ticket with any newly matched
orders.

So a connected store does not by itself mean a ticket has current order
information. An order's `details` is null until the orders sync or a store lookup
has stored it, and its `items` stay empty until a store lookup or the shipping
read has read the order.

## Linked orders [#linked-orders]

Each order on a ticket says how it was linked in `matchedBy`:

* **`email_lookup`:** the store returned the order for a lookup by the customer's
  email address.
* **`order_email`:** the order's own synced record carries the customer's email
  address, compared exactly: ASCII only, trimmed and lowercased, with no folding
  of dots or `+tags`. No lookup is needed.
* **`second_check`:** the conversation names the order, and the customer wrote
  the order's email address in their own reply, and it matched. This verifies
  that one order for reading only, never for a refund, cancellation or change.
* **`order_mention`:** the conversation names the order number.
* **`agent_link`:** an agent linked the order.

The first three verify an order. An `order_mention` also verifies when the
customer quotes the number and their full first and last name matches the
store's customer record, after normalizing case, accents and whitespace.
The name comes from their From display name or their own message or signature.
A number alone stays unverified. `verificationMethod` records `email` or
`order_number_and_name`, or null when unverified; `verified` says whether the bar was met.
An unverified order
keeps its name, dates, statuses and items, but has no tracking, no estimated
delivery and no shipping updates: an empty `tracking` or `events` there means
withheld, not absent.

A verified shipment on its way carries `inTransitSince`, when it went in
transit. Some carriers report only milestones, such as in transit and delivered,
so a parcel can go days without a new update. Only after 14 days with none does
the ticket list a `no_carrier_update` unknown, whose `detail` is the time of the
newest update.

Email verification means the address matched, not that the sender controls it. The
sender's address comes from the email and is never proven. Neither a mention nor
an email match verifies who sent a message. An order read by name or ID alone, as
[Get an order](/reference/orders-and-customers#get-an-order) does, names no
customer, so it is always unverified.

## Refresh a ticket [#refresh-a-ticket]

A refresh asks the Shopify store again for the ticket's orders and its customer.
It needs a key with **Support context → Refresh** (`context:refresh`) and access
to both the mailbox and the store. Read the ticket first, and refresh only when
the answer depends on order information it does not have yet:

| What the ticket shows                                   | Refresh?                                                 |
| ------------------------------------------------------- | -------------------------------------------------------- |
| No `orders`, and the answer needs the customer's orders | Yes. The lookup by the customer's email may find them.   |
| An order whose `details` is null                        | Yes. Nothing about that order has been stored.           |
| An order whose `details.items` is empty                 | Yes, if the answer needs its items.                      |
| Orders with `details`, items and fulfillments           | No. Answer from the stored ticket, checking `checkedAt`. |

```sh title="Terminal"
curl --fail-with-body --silent --show-error --request POST \
  --header "x-api-key: ${ECOMSQUAD_API_KEY:?Set ECOMSQUAD_API_KEY first}" \
  "${ECOMSQUAD_API_BASE_URL}/tickets/${ECOMSQUAD_TICKET_ID}/refresh"
```

Allow up to about 75 seconds. Keep routine reads on the GET endpoints. Answer from
the returned `ticket.orders`, as after a plain read. If `refresh.skipped` is above
zero, refresh again to continue. Refreshing twice in a row is safe: answers from
the last five minutes are reused, so a retry after a timeout does not repeat store
calls. Every field is in [Refresh a ticket](/reference/tickets#refresh-a-ticket).

## Refresh states [#refresh-states]

A refresh reports one `refresh.state` for the lookups it answered, with
`refresh.checkedAt` saying when the newest ran.

| State                  | Meaning                                                                                             | What to do                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `none`                 | This refresh answered no lookup: the ticket gave it nothing to look up, or it skipped every lookup. | If `refresh.skipped` is above zero, refresh again. Otherwise ask for an order number.        |
| `fresh`                | The lookups deciding the answer succeeded.                                                          | Answer from the ticket, checking the timestamps.                                             |
| `not_found`            | A lookup matched no order.                                                                          | Read `refresh.attempts` to see which. If the ticket names no order, ask for an order number. |
| `outside_history`      | The store did not return the order's history.                                                       | Ask the merchant to locate it.                                                               |
| `access_missing`       | No usable store is connected, or the store has not granted the access needed.                       | Ask your administrator to check the connection.                                              |
| `provider_unavailable` | The store failed, rate-limited, or timed out.                                                       | Wait, then refresh. Recent attempts are reused for five minutes.                             |
| `not_permitted`        | Your credentials can read the mailbox but not the store.                                            | Ask for access to the store connection as well.                                              |

`refresh.attempts` has one entry for each order name the ticket's conversation
mentions or an agent linked. `refresh.customer` is the lookup by the customer's
email address. It is shared by every conversation with that customer, so it may
have run from another one. When the conversation names an order, the order
lookups decide the state and the customer lookup is extra context. When it names
none, the customer lookup decides.

An order the customer wrote that a store answered `outside_history` appears in
the ticket's `unknowns` as `mentioned_order_outside_history`, with the order name
as its `detail`: the store did not return that order's history. It does not
say the order exists, so look it up in Shopify rather than tell the customer no
store has it. A number no store has stays `mentioned_order_not_found`.

When a store's customer record counts more orders for the customer's email than
that store's email lookup returned and the synced orders hold, the ticket's
`unknowns` carry `customer_orders_outside_history`, with how many more orders the
store counts as its `detail`, such as `2`. The customer has orders: look them up in Shopify rather
than tell the customer they have none. It takes the place of
`no_order_for_email`.

A missing order can be a valid result. Explain what is missing rather than
guessing. See [Troubleshooting](/troubleshooting#order-context-is-missing-or-stale)
when order information is missing.
