# Overview (/reference)

Base URL, authentication, pagination, and errors for the Context API.

The Context API reads tickets, their messages, orders, and customers, polls the
Needs reply feed, and asks for a mail check of your mailboxes. It returns JSON
and never sends email or changes merchant orders. The operation pages are generated from its OpenAPI document,
so they list exactly the fields the API returns.

## Three ways in [#three-ways-in]

Each operation is one procedure, served three ways with the same inputs, outputs,
and permission checks.

| Surface | Address                                          | For                                                              |
| ------- | ------------------------------------------------ | ---------------------------------------------------------------- |
| REST    | `https://api.meetwillow.cx/api/v1`               | Integrations with an API key.                                    |
| MCP     | `https://api.meetwillow.cx/api/v1/mcp`           | Assistants. See [MCP tools](/mcp#tools).                         |
| tRPC    | `https://api.meetwillow.cx/api/trpc/<procedure>` | EcomSquad's own apps, such as OneInbox, with a member's sign-in. |

## Operations [#operations]

| Procedure                     | REST                                                                                                              | MCP tool              | Permission                            |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------- |
| `context.ticket.list`         | [`GET /tickets`](/reference/tickets#list-tickets)                                                                 | `list_tickets`        | `context:read`                        |
| `context.ticket.get`          | [`GET /tickets/{ticketId}`](/reference/tickets#get-a-ticket)                                                      | `get_ticket`          | `context:read`                        |
| `context.ticket.messages`     | [`GET /tickets/{ticketId}/messages`](/reference/tickets#list-ticket-messages)                                     | `get_ticket_messages` | `context:read`                        |
| `context.ticket.refresh`      | [`POST /tickets/{ticketId}/refresh`](/reference/tickets#refresh-a-ticket)                                         | `refresh_ticket`      | `context:refresh`                     |
| `context.order.get`           | [`GET /orders`](/reference/orders-and-customers#get-an-order)                                                     | `get_order`           | `context:read`                        |
| `context.order.lookup`        | [`POST /orders/lookup`](/reference/orders-and-customers#look-up-an-order-by-email-and-order-number)               | None                  | `context:lookup`, API key only        |
| `context.order.lookupByEmail` | [`POST /orders/lookup-by-email`](/reference/orders-and-customers#find-a-shoppers-orders-by-an-email-you-verified) | None                  | `context:lookupByEmail`, API key only |
| `context.customer.get`        | [`GET /customers`](/reference/orders-and-customers#get-a-customer)                                                | `get_customer`        | `context:read`                        |
| `context.inbox.changes`       | [`GET /inbox/changes`](/reference/needs-reply-feed#list-needs-reply-changes)                                      | None                  | `inbox:read`                          |
| `context.inbox.refresh`       | [`POST /inbox/refresh`](/reference/mailbox-refresh#refresh-mailboxes)                                             | `refresh_inbox`       | `inbox:refresh`                       |
| `context.inbox.refreshStatus` | [`GET /inbox/refresh`](/reference/mailbox-refresh#get-mailbox-refresh-status)                                     | `get_inbox_refresh`   | `inbox:refresh`                       |

Reads return stored data and never contact a provider. Only a
[refresh](/reference/tickets#refresh-a-ticket) may call the store, and only a
[mailbox Refresh](/reference/mailbox-refresh) asks for a mail check. A key or
sign-in given `context:refresh` also gets `context:read`, because a refresh
returns the ticket; a key given `inbox:refresh` also gets `inbox:read`. The
Needs reply feed and the mailbox Refresh answer API keys only.

## Authentication [#authentication]

Send an EcomSquad [API key](/api-keys) in the `x-api-key` header on every request:

```sh title="Terminal"
curl --header "x-api-key: ${ECOMSQUAD_API_KEY}" \
  "https://api.meetwillow.cx/api/v1/tickets?limit=5"
```

The organization comes from the key; you never send an organization ID. Keys are
for server-side use only. Each key carries permissions and connection limits.

REST and tRPC also accept a signed-in member's session. It uses the member's
role and reaches every connection. When a request carries both a session and a
key, the session decides. MCP accepts a key or [MCP sign-in](/mcp#connect-with-sign-in),
limited to the scopes you approve.

## Pagination [#pagination]

List operations return `items` and a `nextCursor`. To read the next page, send
`nextCursor` as the URL-encoded `cursor` query parameter with the same filters.
On `/tickets` and `/tickets/{ticketId}/messages`, a null `nextCursor` means you
reached the end. Each cursor works only with the operation that returned it. The
Needs reply feed's `nextCursor` is always present, and `hasMore` says whether to
read again straight away.

## Errors [#errors]

Errors use an HTTP status and a JSON body with `code` and `message`, and `issues`
when input validation failed:

```json title="Error body"
{
  "code": "NOT_FOUND",
  "message": "Ticket not found"
}
```

| Status | `code`                   | Meaning                                                                                                                                                                                                                                                                                                             |
| ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `BAD_REQUEST`            | Invalid input: a malformed UUID or email, a value out of range, an unknown parameter, or an invalid cursor. Schema failures say `Input validation failed` and list `issues`, except on `POST /orders/lookup` and `POST /orders/lookup-by-email`, whose `400` has one fixed message and never repeats what was sent. |
| 400    | `PARSE_ERROR`            | A POST body is not valid JSON. Send no body, or a JSON object.                                                                                                                                                                                                                                                      |
| 401    | `UNAUTHORIZED`           | No session, and the `x-api-key` header is missing, invalid, expired, or revoked, or the member who created the key has left the organization. The message does not say which.                                                                                                                                       |
| 403    | `FORBIDDEN`              | The message names what is missing: the operation's permission (`This API key lacks the inbox:read permission`), an API key on a key-only operation, or a signed-in user's current organization.                                                                                                                     |
| 404    | `NOT_FOUND`              | The record does not exist, or is outside the key's organization or connection access.                                                                                                                                                                                                                               |
| 415    | `UNSUPPORTED_MEDIA_TYPE` | A POST carries a body that is not `application/json`.                                                                                                                                                                                                                                                               |
| 429    | `TOO_MANY_REQUESTS`      | `POST /orders/lookup` and `POST /orders/lookup-by-email` only: too many failed lookups for this email, order name or key, or too many lookups by email for this email or key, in the last 24 hours, or a wait of more than a second behind the organization's other lookups. The message does not say which.        |
| 500    | `INTERNAL_SERVER_ERROR`  | EcomSquad failed. The message is always `Request failed`. Retry reads; read the ticket before repeating a refresh.                                                                                                                                                                                                  |

An HTTP 200 means the read succeeded, not that an order was found or recently
checked. Inspect the returned orders and, after a refresh, `refresh.state`. See
[Troubleshooting](/troubleshooting#api-requests-fail) for next steps.

A `null` from [`GET /orders`](/reference/orders-and-customers#get-an-order) is
often an id in the wrong form. The numeric `orderId` on a ticket or in a
customer's synced `orders` matches nothing there. Send the order's name as
`orderName`, or put `gid://shopify/Order/` in front of the number.

Every response, success or error, carries an `x-request-id` header. Quote it when
you report a problem: it finds the request in EcomSquad's logs. To use your own
id, send it as `x-request-id`, 1 to 128 characters of letters, digits, `.`, `_`,
`:` and `-`, and the response returns it unchanged. Any other value is replaced
with a new id. Browsers can read the header.

## OpenAPI document [#openapi-document]

The API serves the document these pages are generated from:

```text title="OpenAPI document"
https://api.meetwillow.cx/api/v1/openapi.json
```

Use it to generate a client or to check exact request and response schemas.
