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.
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.
Idempotency keys make a retry safe
A client that never heard the reply has no way to know whether the send happened. SendIdempotency-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.
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 returnsrate_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.
