Auth, email and SMS.One contact.

Sign people in and talk to them without keeping three copies of the same person at three vendors — and without writing the webhooks that keep those copies agreeing.

Start building

No card to get keys. Set a spending cap before you send, and nothing can run past it.

Reachable by agent, MCP, API, CLI, SDK or dashboard.
Avery Chenone contact
AuthGoogle
Emaildelivered
SMSverified
Auth67 operationsEmail38 operationsSMS26 operations

Three products. One system underneath.

Take one of them or all three. They read and write the same contact, through the same API, rather than three services you keep in step yourself.

The contact they share
problem

One person, three vendors, and the code that keeps them agreeing.

An identity provider knows their password. An email provider knows their address. A carrier knows their number. None of the three knows they are the same human, so you are the one who has to.

Two copies of the same person. The auth system calls her Avery Chen; the email system still calls her Avery C.

The copies stop matching

A sync job you wrote keeps two records in step. When it lags nothing raises an error — the second record simply stops being true, and you find out when somebody is greeted by the wrong name.

emailProduct newsunsubscribed
smsProduct newsstill on
Product news consent: the email vendor has it as unsubscribed, the carrier still has it on, and the link between them is broken.

One of them was told

An unsubscribe is a fact about a person that arrives at exactly one vendor. Copying it to the other is your job, and being wrong about it is a compliance problem rather than a bug.

Product news
email
sms
One surface showing Product news consent for both channels at once — off for email, on for SMS — so the difference is visible and changeable in one place.

Both, in one place

Email and SMS stay separate decisions — a bounce or a STOP arrives for an address, not for a person. What is shared is the contact they hang off and the single page where somebody changes both.

$100
Identity$74
$50at cap
Email$47
$250
SMS$93

Three ceilings, no total

Each vendor meters its own unit on its own schedule. One of them has stopped sending and the other two do not know, because none of them has heard of the other two.

$214of $500
Auth$74Email$47SMS$93

One ceiling

Checked before anything is queued, on every send path and inside the automation engine — so the limit is a refusal rather than a line item you find afterwards.

architecture

The same person, drawn twice.

The difference is not that three products arrive on one invoice. It is that they read and write the same customer state, which is the half a bundle cannot give you.

identity provider
nameemail
email vendor
emailconsent
carrier
phoneconsent
Three separate records of one person, each holding different fields and each with gaps.

Three copies, none of them whole

An identity provider, an email vendor and a carrier each hold a slice of the same person — and a different slice each. The gaps are the code you write.

contact
nameemailphoneconsent
One complete record holding all four fields: name, email, phone and consent.

One contact, whole

Both addresses, consent per channel, and the history — on one row that every product reads and writes.

Checked, not asserted —db/src/schema/audience.tscore/src/ops/audience-consent.tscore/src/ops/audience-preferences.tscore/src/audience/policy.tscore/src/billing/usage.ts

suppression
consent
quiet hours
spending cap
A message part-way through four policy checks: two cleared, two still ahead.

Every send falls through the same four

Suppression, consent, quiet hours and your spending cap — checked before anything is queued. Your broadcasts, your text messages, and the sign-in codes Lath posts on your behalf all take this route.

One consent record, both channels

Consent history · one contactappend-only
  1. 09:12Product newsemailopted in
    source: signup-form“Email me product updates and release notes.”
  2. 11:46Order updatessmsopted in
    source: checkout“Text me when my order ships.”
  3. 14:03Product newsemailopted out
    source: preference-page
Their preference pagehosted, your theme
Product news
EmailoffSMSoff
Order updates
EmailonSMSon
Security alerts
EmailonSMSon

Both channels, one page, one contact. Security alerts are transactional — an unsubscribe does not reach them.

Email and SMS stay separate decisions — a bounce or a STOP arrives for an address, not for a person. What is shared is the contact they hang off and the single page where somebody changes both.

contact

One record. Everything hangs off it.

Identity, both addresses, what they agreed to on each channel, and what you have sent them — on the same contact, not assembled from three consoles.

Avery Chenc7f1a2e4-5b90-4c31-9d6e-2a8f0b31d745
AuthGoogle · passkey3 sessionsauth.user.get
Emailavery@example.comopted in · product newsemail.message.list
SMS+1 555 0147verified · order updatessms.message.list
today
  1. 16:12Signed in with Googleauth
  2. 16:13Welcome email deliveredemail
  3. 16:15Opted in to order updatesaudience
  4. 16:16Phone verified by codesms
  5. 16:17Order confirmation deliveredsms

Email and SMS consent stay separate decisions — a bounce or a STOP arrives for an address, not for a person. What is shared is the contact they hang off.

lifecycle

What a signup actually looks like.

One person, from a Google button to a text message they asked for. Every operation named here is real — the test opens the registry and checks.

one contact, throughoutc7f1a2e4…
  1. Signs in with Googleauth.oauth.complete
    email not yetphone not yetconsent not yetsegment not yet
  2. You create the contactaudience.contact.create
    email heldphone not yetconsent not yetsegment not yet
  3. Welcome email goes outemail.send
    email heldphone not yetconsent not yetsegment not yet
  4. Phone verified by codeauth.identity.verify

    The phone lands on the row that already holds the email.

    email heldphone heldconsent not yetsegment not yet
  5. They opt inaudience.consent.grant
    email heldphone heldconsent heldsegment not yet
  6. They fall into a segmentaudience.segment.create
    email heldphone heldconsent heldsegment held
agents

Describe what you want. Your agent does the rest.

Not 'an AI writes some integration code'. Every operation is a tool carrying its input and output schema, so an agent can list what exists, read what each one takes, and call it.

Add authentication to my Next.js app. Let people sign in with email or Google. When someone signs up, send them a welcome email. During onboarding, collect a phone number and verify it by SMS.

  1. auth.settings.set
  2. auth.oauth.app.set
  3. email.template.set
  4. email.template.publish
  5. developers.webhook.create
  6. developers.key.create
Authemail · Google
Emailtemplate live
SMSverification on

Not code it drafted for you to debug — configuration it made, by reading each operation's schema and calling it.

products

Three products. One contact underneath.

Take one of them or all three. What they share is there when you want a second channel and costs you nothing while you do not.

Auth

67 ops
A six-digit sign-in code being entered.waiting
Chrome · macOSsession · now
Safari · iPhonesession · 2 days ago

Get them in — by emailed code, magic link, password, passkey, or Google, Microsoft and GitHub.

Sign-in codes, magic links, passkeys, passwords, two-step, social sign-in, organizations, sessions.

Email

38 ops
A message moving from queued to sent to delivered.
queuedsentdelivered

Say something to one person or to a segment, from a template that knows their language.

Transactional send, templates with translations, broadcasts, automations, inbound, delivery feedback.

SMS

26 ops
An inbound STOP message, and the automatic reply.
stopped

Reach the phone, with the consent and quiet-hours rules the channel actually requires.

Send, templates, quiet hours, STOP, HELP and START keywords, numbers, carrier registration.

shared by all three

Audience

27 operations
Auth
Email
SMS
One shared contact row that all three products read and write.

One row, read and written by all three — not a fourth product you also have to buy.

and the plumbing
Developers21
API keys with scoped permissions, webhook endpoints, deliveries, replays, the event catalogue.
Account14
Projects, environments, members and roles, the theme every hosted page wears.
Billing13
Metered usage, spending caps, invoices, payment methods, credits.
surfaces

One operation. Any interface.

The same call — same operation, same arguments, same reply — written six ways. Only the syntax changes, so what you learn on one of them you keep on all six.

the operationemail.send
plain EnglishIt lists the tools, reads this one's schema, and calls it.
Send avery@example.com the welcome email,
with their first name in it.
same reply
{
  "messageId": "01J9F2...",
  "status": "queued",
  "to": "avery@example.com",
  "template": "welcome",
  "templateVersion": 3
}

Or start from nothing.

Copy any of these and run it. It creates the account, sets a spending cap, and sends — no key needed to begin, because creating an account is the one operation that does not take one.

# 1. Create an account. This is the only operation that needs no key.KEY=$(curl -s -X POST https://platform.trylath.com/account/signup \  -H "Content-Type: application/json" \  -d '{"email":"you@example.com","country":"US"}' | jq -r .result.live.key)# 2. Cap the month before anything can send.curl -s -X POST https://platform.trylath.com/billing/cap/set \  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{"monthlyCapCents":500}'# 3. Send.curl -s -X POST https://platform.trylath.com/sms/send \  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{    "to": "+14155550100",    "body": "Spring sale starts today.",    "topic": "promotions"  }'

Response

{  "activityId": "act_7Qw1e8",  "result": {    "messageId": "sms_2pK4mA",    "status": "suppressed",    "to": "+14155550100",    "segments": 1,    "encoding": "gsm-7",    "template": null,    "templateVersion": null,    "templateLocale": null,    "sendAfter": null,    "blocked": {      "reason": "No carrier registration has been filed for this project yet.",      "fix": "Check sms.registration.get for where it has got to. The message is stored and will send once the channel opens; nothing is lost."    }  }}

This is enforced rather than intended: parity.test.ts derives the operations and fails the build if any one of them is missing a REST route, an MCP tool, a CLI command, an SDK method or a dashboard screen — and fails just as hard if a surface carries one the registry does not.

Read the API
proof

Built so customer infrastructure does not drift.

Each of these names the file that enforces it, because a guarantee you can go and read is a different kind of claim from one you are asked to take.

RESTMCPCLISDKDashboard
Build fails.

parity.test.ts

Every operation, on every surface

A test derives the operation list and fails the build if one is missing a REST route, an MCP tool, a CLI command, an SDK method or a dashboard screen — and fails just as hard if a surface has one the registry does not.

sent
replayed
1 messageIdempotency-Key

runtime.ts

Retries that cannot double-send

Declared per operation rather than assumed, and the stored response is encrypted at rest.

t=1757…v1=9f2c…
signed together
trusted

sign.ts

Webhooks you can verify

Compared in constant time, with a five-minute tolerance, and the endpoint's secret is rotatable without dropping a delivery.

1m · 5m · 30m · 2h · 12h · 24hdelivered

deliver.ts

Deliveries that survive your outage

Every attempt keeps its own row — status, error and duration — so a dead delivery can be read before it is replayed, one at a time or in bulk.

Nothing was sent.402 spending_cap_reached

usage.ts

A spending cap that stops the send

Applied on all four send paths and inside the automation engine, under a per-project lock so two racing sends cannot both slip past it.

  • Consent you can prove afterwards
  • A record of who did what
  • An SDK that cannot drift
  • Delivery, bounce and complaint receipts
  • Rate limits in two places

154 test files —generated/test/parity.test.tscore/src/runtime.tscore/src/webhooks/sign.tsworker/src/deliver.tscore/src/billing/usage.tscore/src/ops/audience-optin.tscore/src/ops/activity.tsgenerated/src/emit-sdk.tscore/src/messaging/delivery.tscore/src/auth/limits.ts

The suite runs against a real PostgreSQL and a real browser on every push. Nothing is mocked to make it faster.

ecosystem

Does it fit what you already use?

Attach it to the agent you have open, install a typed client, or POST to it from anything that speaks HTTP. Every name here is a package in this repository.

Lath
  • Claude CodeMCP
  • CodexMCP
  • CursorMCP
  • Any MCP clientPOST /mcp
  • TypeScript@trylath/sdk
  • React@trylath/react
  • Claude Code
  • Codex
  • Cursor
  • Any MCP client
  • TypeScript / Node
  • React
  • React Native
  • Drop-in sign-in UI
  • CLI
  • Anything that speaks HTTP
attach the server
{
  "mcpServers": {
    "lath": {
      "url": "https://platform.trylath.com/mcp",
      "headers": { "Authorization": "Bearer lath_live_..." }
    }
  }
}
  • A key you scope
  • A cap it cannot raise past
  • A test environment
  • A record of everything it did

Built on things you can check.

Every name here is a dependency this repository actually ships.

  • TypeScript
  • Next.js
  • React
  • Tailwind CSS
  • Radix UI
  • Hono
  • Zod
  • PostgreSQL
  • Model Context Protocol
  • Fly.io
  • Vitest

Early access pricing

You pay for what you send. You set the ceiling.

Usage-based, with a hard cap you choose. No plan tiers to size yourself against, and no per-seat charge for being reachable on more than one channel.

Card required to start?
No. Creating an account needs no key and no payment method.
What costs money today?
Sending — per email, per SMS segment — and monthly active users on sign-in.
Can usage run away?
No. The cap is checked before anything is queued and refuses with 402.
Is there a test environment?
Yes. Live and test keys exist from the moment a project does.

Email

per email sent

Transactional and broadcast are counted the same way. A send that is suppressed before it leaves is not billed.

Text messages

per segment

Segments, not messages — a body outside the GSM alphabet costs more than twice as much per character, and the reply says how many were counted.

Sign-in

per monthly active user

Counted once per person over the month — not again because you also emailed them and texted them.

Per-unit rates are published before general availability. The billing engine is built never to guess a price, so neither does this page — and nothing is charged before the numbers are here.

  • A spending cap you set, which stops the part of the bill that sending can run up.
  • Live and test keys from the moment a project exists; an environment is what a key names.
  • Usage priced against the period it happened in, so a later rate change cannot reach backwards.

Frequently asked questions

Questions before you build on this.

Including the two whose honest answer is still “not yet”. Those are read from the same place as the status section below, so they cannot say different things.

Stop keeping three copies of the same person.

Create an account without a card, set a cap, and send something in the next ten minutes — by hand, from your terminal, or by asking your agent to do it.