@sente-labs/sdk is a thin typed client for the Sente REST API. Zero runtime dependencies (it uses
global fetch, so Node 18+), and types ship with the package — no @types/* install.
new Sente(opts) takes { apiKey, baseUrl?, fetch? }. baseUrl defaults to
https://api.sente.run; the fetch override exists for tests. Get a key from the
dashboard or with sente login && sente token. One key authenticates the
whole organization — keep it in an environment variable.
Create an account
An account is the object. Two ways to get one, and neither needs an identity first — omitidentityId and Sente provisions the backing email address from the app’s hostname.
Connect one you already own
You supply the credentials; Sente logs in, vaults them write-only, and keeps the account re-loginable.Limits worth knowing before you build on connect:
- SSO-only accounts cannot be connected. Google/Okta sign-in and passkeys have no username and password to vault.
- Credentials are write-only.
registrations.getCredentialson a connected account returns403 CREDENTIALS_WRITE_ONLY. - A vaulted
totpSeedmakes re-login autonomous. The server computes the 30-second code; the seed never reaches any model. - Email or SMS code MFA needs a human. The run goes
blockedwithMFA_REQUIRED— the code went to the owner’s inbox, which Sente cannot read. Someone enters it atliveViewUrl, thenruns.resume(run.id).
connections.revoke(id) disables the connection but keeps the vault, so a later connect
re-enables it. connections.delete(id) revokes and purges the vaulted secrets.
Register a new one
registrations.register drives a browser through signup at appUrl under a fresh email address
and completes email verification from that address’s own inbox.
getSession, login,
exportSession).
Wait for a verification code
Every inbound email is classified server-side on arrival —message.annotation is
{ kind: "otp" | "magic_link" | "other", code, link, confidence } — so a signup you drive can
block until the code arrives, already extracted.
timeout is in seconds, 1–60, default 25 — it’s a server-side long poll, and values outside
that range are a 400. Both helpers resolve null on timeout, so loop if you need longer. Accounts
created through connect / register need none of this: the run controller injects the code
itself.
Get a logged-in session
getSession checks the login is still fresh and, if it isn’t, re-logs in first — vaulted
credentials plus a fresh code from the account’s inbox — so the browser you get was just confirmed
signed in. healed: true says that happened, and a cold call can take a minute.
Options: { verify?, maxStaleSec?, timeoutMs? }. verify: false skips the freshness check;
maxStaleSec overrides the freshness window for this call; timeoutMs caps the whole
re-login-and-retry loop and defaults to 240000 (4 minutes). openSession(id) is the raw
primitive with no freshness check at all.
To use your own browser stack instead, export the logged-in state as a Playwright storageState:
Handle a blocked run
A run stops atblocked whenever it hits something only a person can clear. It holds for about
10 minutes with an interactive liveViewUrl, then fails with BLOCKED_TIMEOUT.
secret is returned once, at register time. Every delivery carries it in the x-sente-secret
header — verify with a timing-safe compare. message.received payloads are thin notifications with
no body; fetch content with messages.get(id). Full payloads in
Webhooks.
Errors
Non-2xx responses throwSenteError with status (the HTTP status) and body.
SESSION_STALE and SESSION_PENDING (also 409) are handled inside getSession — it polls the run
they name and retries — so you only see them from the raw openSession.
Method reference
Identities, for when you want to control the address
Identities, for when you want to control the address
identityId is optional everywhere, but pass one when several accounts should share a mailbox, or
when the address itself matters (it shows up in the app’s UI as the account’s email).localPart throws a 409 (onConflict: "error" is the default); pass
onConflict: "suffix" to append a random suffix instead. Role addresses (support, admin,
billing, security, postmaster, …) are reserved — support-bot is fine, support is not.Send, reply, and stream mail
Send, reply, and stream mail
text and/or html is required. messages.stream(identityId) is an async generator that
long-polls and swallows transient errors; for production prefer webhooks —
push, not poll. More in The account’s inbox.Next steps
- Runs — statuses, the full error-code table, and the run lifecycle.
- Human takeover — the live view, the 10-minute hold, and resuming.
- Python SDK · CLI · API reference.
