> ## 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.

# Rate limits

> Every request counts against a ceiling per credential, which API conventions covers. This page is the other layer: exact limits on the operations worth attacking — starting a sign-in, redeeming a link, creating an account, sending a text — counted in the database so every API process shares them.

## What a refusal looks like, and how to wait

Every limit here refuses with 429. Most use `rate_limited`, and put the wait in the refusal's `fix` as "Retry after N seconds" — only the per-credential ceiling also sets a `retry-after` header. Mail Lath sends on your behalf has its own code, `auth_send_limited`, because it is a separate budget with a separate remedy.

A refused call is never stored against an `Idempotency-Key`, so retrying with the same key is safe and still cannot run the operation twice. Wait for the stated seconds, and back off exponentially when there are none.

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

/** Runs `call` again after a 429, waiting as long as the refusal asks, up to `attempts` tries. */
export async function withRetry<T>(call: () => Promise<T>, attempts = 5): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    try {
      return await call();
    } catch (e) {
      if (!(e instanceof LathError) || e.status !== 429 || attempt === attempts) throw e;
      const stated = /(\d+) seconds?/.exec(`${e.message} ${e.fix}`)?.[1];
      const seconds = stated ? Number(stated) : 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
    }
  }
}

export const sendReceipt = (orderId: string, to: string) =>
  withRetry(() => lath.email.send({ to, subject: "Your receipt", text: "Thanks for your order." }, { idempotencyKey: `receipt-${orderId}` }));
```

## Signing in

**Starting a sign-in** — `auth.signin.start` and `auth.password.signin` — is limited to 5 attempts per identifier and 30 per IP address, each in 10 minutes. The identifier is the email address or phone number, hashed before it is counted, so the table that holds the counts holds no addresses.

`auth.passkey.signin.start` and `auth.identity.add` count against the same per-address setting as starting a sign-in.

**Finishing one** — `auth.link.complete`, which is the page a magic link opens — allows 30 per IP address in 10 minutes. `auth.session.exchange`, which turns the handoff code that page returns into a session, allows the same.

Those three settings — per identifier, per address to start, per address to complete — are the ones you can change. The rest on this page are fixed.

## Tune the sign-in limits

`auth.settings.set` takes `rateLimits` with `startPerIdentifier`, `startPerIp` and `completePerIp`, each a `max` from 1 to 100 and a `windowSeconds` from 60 to 86,400. Objects merge key by key, so sending only `max` keeps the window you had.

It needs a secret key with `auth:write`, and takes effect on the next request. Raise the per-address limits when many people sign in from one office or one mobile carrier's address; lower the per-identifier limit when you would rather a person wait than an attacker keep guessing.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://platform.trylath.com/auth/settings/set \
    -H "Authorization: Bearer $LATH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"rateLimits":{"startPerIp":{"max":100},"completePerIp":{"max":100}}}'
  ```

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

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

  const { result } = await lath.auth.settings.set({
    rateLimits: { startPerIp: { max: 100 }, completePerIp: { max: 100 } },
  });
  ```

  ```bash CLI theme={null}
  lath auth settings set \
    --rateLimits '{"startPerIp":{"max":100},"completePerIp":{"max":100}}'
  ```
</CodeGroup>

## Creating accounts and users

**A sign-in that would create a new user** is counted per environment and IP address while bot protection is on, which it is by default — 5 an hour unless changed, settable as `botProtection.signupPerIp` in `auth.settings.set` with the same `max` and `windowSeconds` ranges. Sign-ins by people who already have an account are not counted here, which is what lets an office sign in a hundred times without looking like a hundred sign-ups.

`account.signup`, which creates a Lath account, allows 30 an hour per IP address. `account.member.accept` allows 20 per IP address in 10 minutes.

## Things a signed-in user does

Counted per user: `auth.identity.add` 5 an hour; `auth.factor.totp.enroll` 10 an hour; `auth.factor.totp.activate` 10 in 10 minutes; `auth.passkey.register.start` 10 an hour; `auth.password.set` 10 an hour; `auth.org.create` 10 an hour.

Counted per organization: `auth.org.invite.create` 50 an hour.

Counted per IP address, for the device flow a CLI or a TV uses: `auth.device.start` and `auth.device.approve` 20 in 10 minutes; `auth.device.describe` 30 in 10 minutes. The preference page a recipient opens from an email allows 60 reads and changes per IP address in 10 minutes.

## Texts, and mail Lath sends for you

`sms.send` is limited to 10 texts a second and 500 an hour per environment. A call over either is refused rather than queued, so a job sending one text at a time should pace itself — or use an SMS broadcast, which is not counted against this limit.

Sign-in codes, magic links and invitations are mail Lath sends on your behalf, and they share a budget of 500 an hour and 5,000 a day per environment, refused with `auth_send_limited`. It does not apply to `email.send`. It exists because `auth.signin.start` works with a publishable key, which is public by design — so a burst that reaches it is more often a key being abused than real traffic, and `developers.key.rotate` replaces the key.
