What already works, the moment you sign up
account.signup returns an account, a project, both environments, and four keys — a secret and a publishable one per environment, shown once and never again.
You can send email immediately, with nothing verified. Every project gets a sender on its own subdomain of Lath’s sending domain, and transactional mail, sign-in mail, broadcasts and automations can all go out from it. Verifying a domain of your own is what puts your name on it and keeps your reputation yours; it is not what sending needs.
Sign-in by email code and by email link work out of the box. Passwords are the one method off by default.
The owner seat is invited rather than active until somebody follows the emailed link. The keys work either way, which is why a script can sign up and send before anyone has opened the mail.
Before marketing email
Seven things are checked, and you do not have to discover them one at a time: email.broadcast.create returns them all as readiness problems, each with its own fix, long before anything sends. email.broadcast.send then refuses on the first one that still stands.
A sender you are allowed to send from, a topic that exists, a segment that exists if you named one, a template that exists and is published, not one of the reserved sign-in templates, a working unsubscribe link, and a postal address in the email footer.
Each problem carries its own code in that list — sender_unverified, topic_unknown, segment_unknown, template_unknown, template_reserved, unsubscribe_missing and legal_address_missing — so your own code can branch on which one it is rather than matching prose. Ignore readiness and send anyway, and email.broadcast.send refuses with the code of the first problem that still stands.
The last one catches almost everybody, because it is law rather than plumbing and it is empty by default. It lives in the theme’s copy, and it must be published, not merely saved as a draft.
Two more can refuse at send time and are about the account rather than the message: a closed account, and an invoice past due — and past due pauses marketing only, after a grace period. Transactional mail keeps going.
Before text messages
Two separate things: a carrier registration that has reached active for the project, and a phone number this environment owns. sms.registration.submit records the registration once per project and marks it registering; from there it becomes active, action_needed or failed as the carrier answers, and sms.registration.get returns a plain-language blocker for whatever state it is in.
Not built yet: sending the filing to the carrier. Submitting stores the details and does not itself file them — the tax id is dropped rather than sent anywhere. What is built is the other half: once a brand is filed with the carrier, Lath reads its decision on its own and moves the status. The SMS rules guide says more.
sms.send does not refuse while you wait. It stores the message and answers with a blocked object beside it, saying why and what would unblock it. The message is real, findable and not sent — which is a different thing from an error, and the reason a send that “worked” may not have gone anywhere. Read the reply.
Quiet hours are on by default, 21:00 to 08:00, with no timezone set — and without a timezone there is no clock to measure them against, so SMS broadcasts and automations are refused with quiet_hours_timezone_unset until you set one or turn quiet hours off. A single sms.send with a topic uses the contact’s own timezone property when there is one, and otherwise sends as asked. sms.settings.get reports the missing timezone on its own. Messages caught by quiet hours are delayed, never dropped.
A marketing text must tell people how to stop, and readiness says so when it does not. It is not added for you, because adding it would change the segment count you just measured.
Before the hosted sign-in pages
In live, allowed redirect origins start empty, so nothing is allowed. Until you list yours, sending somebody back to your app is refused with redirect_not_allowed, and a magic link has nowhere to land. This is the first thing to set and the easiest to forget, because the hosted pages look finished without it — and because test mode hides it, allowing any http://localhost origin.
Passkeys bind to an origin and are never bound to Lath’s hostname. In live the relying-party id derives from your first allowed origin; with none, passkeys refuse with passkey_unconfigured. In test it is localhost.
Social sign-in works through Lath’s own apps, which means the consent screen says Lath. Registering your own puts your name there instead — auth.oauth.app.list hands you the exact callback address to register with the provider.
Publish the theme. account.theme.set saves a draft; the hosted pages and your mail both read the published one. Publishing and resetting are live-only, and a palette below WCAG AA is refused with contrast_too_low — which names a nearby colour that would pass rather than leaving you to guess.
Lath will not let you switch off the last way in: a settings change that would leave no method both enabled and available is refused with no_usable_signin_method.
What test mode will not do
Anything that changes what live users receive needs the live key, refused otherwise with live_only: publishing or resetting a theme; publishing or deleting an email or SMS template; setting the sender; removing a domain or changing its tracking or return path; and setting or removing your own OAuth app.
Test email.send only reaches active members of your own account — a plus-tagged address folds to the member’s mailbox, and anyone else is refused with test_mode_recipient. An invited member who has not accepted yet counts as anyone else. Sign-in codes and links are the exception: they reach any address, so a sign-in flow can be tested end to end.
Test mode sends no text messages at all, to anybody, for the same reason: there is no safe pretend recipient for a carrier.
Fifty messages per environment per rolling day, after which email.send is refused with test_mode_limit. Test sends are never billed.
Broadcasts do not refuse on any of this — they count the recipients they held, so the number you see is the number that would have gone.
Ask the product, not this page
This page will go out of date; the fields below cannot, because they are computed from the same code that does the refusing. Prefer them to any checklist, including this one.
email.broadcast.create returns every readiness problem with its fix, and email.automation.activate refuses with the first one (409). sms.settings.get returns whether texting is ready and what is in the way. sms.registration.get and email.domain.get each return a single blocker, null when there is nothing wrong. auth.settings.get returns every sign-in method with whether it is enabled, whether it is available, and why not.
In the dashboard, the same answers are on the page that owns them, and anything account-wide that needs you is on the environment’s home page.Last modified on September 23, 2026