> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trylath.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Every Lath operation is POST https://platform.trylath.com/<operation name with dots replaced by slashes>, with a JSON body and `Authorization: Bearer <key>`. `email.send` is POST /email/send.
> Branch on `error.code`, never on `error.message`. Every refusal also carries `error.fix`, which names the next step.
> Send an `Idempotency-Key` header on any operation that is not retry-safe, so a retry cannot run it twice.
> A `lath_test_` key emails only the account's own members and sends no SMS; a `lath_live_` key reaches real recipients and is billed.
> The OpenAPI document, generated from the same registry as the routes, is at https://platform.trylath.com/openapi.json.

# API conventions

> The rules every one of the 206 operations follows — how a call is addressed, how a list is paged, how a retry is made safe, and what happens when you go too fast.

## Every call is a POST, and the operation name is the path

There is one route per operation and nothing else: the name, with its dots turned into slashes, straight off the origin. `email.send` is `POST /email/send`; `auth.user.list` is `POST /auth/user/list`.

So there are no verbs to choose and no path parameters to build. An id is a field in the JSON body like any other field, which is why the same operation name works unchanged as an MCP tool, a CLI command and an SDK method — only the envelope around it differs.

The body is JSON and may be at most 1,048,576 bytes. A larger one is refused with `body_too_large` before it is read, from the `Content-Length` alone where the client declares it. An absent or empty body means `{}`, so an operation with no required fields needs no body at all.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/auth/user/list \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"limit":50}'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  const { result } = await lath.auth.user.list({ limit: 50 });
  ```

  ```bash CLI theme={null}
  lath auth user list --limit 50
  ```
</CodeGroup>

## Every response carries a request id

`lath-request-id` comes back as a header on every response, and inside the body of every error as `error.requestId`. The body carries it too because a customer reading a refusal in their own logs has the body and not always the headers.

You can supply your own instead of being given one: send `lath-request-id` on the request and it is honoured if it is 1–64 characters of letters, digits, dot, underscore, colon or hyphen. Anything else is replaced with a generated id rather than rejected — a malformed trace id should not cost you the call. That is what lets one id join your logs to ours.

## Paging: the response shape tells you which kind it is

Three shapes, and which one an operation returns is fixed. **Lists** return `{ …rows, nextBefore }` — newest first, 14 of them. Pass that `nextBefore` value back as `before` to get the next page.

**Exports** return `{ …rows, nextAfter }` — oldest first, so a full copy stays consistent while you walk it. `nextAfter` is opaque: pass it back unchanged as `after`, and do not build one yourself. `auth.user.export`, `audience.contact.export` and `sms.conversation.get` are the three that work this way; constructing a cursor gets `invalid_input` naming exactly that.

**Small bounded sets** return just the rows, with no cursor at all — your own sessions, your automations, your invoices. There is nothing to page because the whole set is already there.

For both cursor shapes the stop condition is the same, and it is the one to code against: a page holding fewer rows than you asked for is the last page, and the cursor comes back `null`. Do not stop on an empty page — stop on a short one, or you will make one extra call every time.

`limit` is where the bounds vary. Most lists allow up to 200 and default to 50; the two exports allow up to 500 and default to 200; a few cap at 100. Each operation's reference page carries its own, and asking for more than the maximum is `invalid_input`, not a silent trim.

<CodeGroup>
  ```bash cURL theme={null}
  # First page. The reply is {"contacts":[…],"nextBefore":"2026-09-12T18:04:11.522Z"}
  curl -X POST https://platform.trylath.com/audience/contact/list \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"limit":200}'

  # Next page: hand that nextBefore straight back as before.
  # Repeat until nextBefore comes back null.
  curl -X POST https://platform.trylath.com/audience/contact/list \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"limit":200,"before":"2026-09-12T18:04:11.522Z"}'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  // First page. The reply is {"contacts":[…],"nextBefore":"2026-09-12T18:04:11.522Z"}
  await lath.audience.contact.list({ limit: 200 });

  // Next page: hand that nextBefore straight back as before.
  // Repeat until nextBefore comes back null.
  await lath.audience.contact.list({ limit: 200, before: "2026-09-12T18:04:11.522Z" });
  ```

  ```bash CLI theme={null}
  # First page. The reply is {"contacts":[…],"nextBefore":"2026-09-12T18:04:11.522Z"}
  lath audience contact list --limit 200

  # Next page: hand that nextBefore straight back as before.
  # Repeat until nextBefore comes back null.
  lath audience contact list --limit 200 --before 2026-09-12T18:04:11.522Z
  ```
</CodeGroup>

## Idempotency keys make a retry safe

A client that never heard the reply has no way to know whether the send happened. Send `Idempotency-Key` with any string you choose and the answer to the first request is stored and replayed to the second, so retrying costs nothing and sends nothing twice.

It is offered on exactly the operations that need it: the 57 that change something and are not already safe to repeat. Reads ignore the header entirely — repeating a read is already harmless, and a stored answer would only go stale. `email.message.list` does not take one; `email.send` does.

The key is scoped to your environment, your credential and that one operation, so the same string used against a different operation is a different key. Two requests carrying the same key at the same time do not race: the second waits for the first and then replays its answer, rather than both running and one losing.

Reusing a key with **different** input is refused with `idempotency_mismatch` (409) rather than quietly answering the old question. A stored answer lasts 24 hours; after that the same key is simply a new request.

On the other surfaces it is the same mechanism under a different name: the CLI takes `--idempotency-key`, and the MCP tools carry an `idempotencyKey` field on precisely the operations that are neither reads nor retry-safe.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/email/send \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Idempotency-Key: order-4182-receipt" \
    -H "Content-Type: application/json" \
    -d '{"to":"buyer@example.com","subject":"Your receipt","text":"Thanks."}'
  ```

  ```ts TypeScript theme={null}
  import { createLath } from "@trylath/sdk";

  const lath = createLath({ key: process.env.LATH_API_KEY });

  const { result } = await lath.email.send({
    to: "buyer@example.com",
    subject: "Your receipt",
    text: "Thanks.",
  }, { idempotencyKey: "order-4182-receipt" });
  ```

  ```bash CLI theme={null}
  lath email send \
    --to buyer@example.com \
    --subject 'Your receipt' \
    --text Thanks. \
    --idempotency-key order-4182-receipt
  ```
</CodeGroup>

## Rate limits, and the header that says how long to wait

600 requests a minute per credential. A request with no credential at all is counted against its address instead, at 60 a minute — that is the sign-in and magic-link surface, not the API you call with a key.

Going over returns `rate_limited` (429) with a `retry-after` header in seconds, and the same number in the message. Wait that long rather than retrying immediately; a client that retries at once is the reason the limit exists.

The limit is counted per credential, not per account, so rotating a key or issuing a second one gives you a second budget. Individual operations have their own tighter limits where sending an unlimited number would be the abuse — starting a sign-in, redeeming a link, sending a text. Those are exact, and the rate limits guide lists every one.
