sente-sdk is a thin Python client for the Sente REST API. Stdlib only (urllib) — no
dependencies. The PyPI distribution is sente-sdk; the import is sente.
Sente(api_key, base_url=...) — base_url defaults to https://api.sente.run. Get a key from the
dashboard or with sente login && sente token. One key authenticates the
whole organization — keep it in an environment variable.
Most methods take a leading positional id and keyword arguments after it; connections.connect and
registrations.register are fully keyword-only. Results are dataclasses and NamedTuples with
snake_case attributes (run.live_view_url) — except session dicts, which come straight off the
wire in camelCase (s["cdpUrl"]).
Create an account
An account is the object. Two ways to get one, and neither needs an identity first — omitidentity_id 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.connect returns a ConnectResult(connection, run), so the tuple unpack above works; keep the
result if you prefer result.connection.
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.get_credentialson a connected account raisesSenteErrorwith status 403 andbody["code"] == "CREDENTIALS_WRITE_ONLY". - A vaulted
totp_seedmakes 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 atlive_view_url, thenruns.resume(run.id).
connections.revoke(connection_id) disables the connection but keeps the vault, so a later
connect re-enables it. connections.delete(connection_id) revokes and purges the vaulted
secrets.
Register a new one
registrations.register drives a browser through signup at app_url under a fresh email address
and completes email verification from that address’s own inbox.
get_session, login,
export_session).
Wait for a verification code
Every inbound email is classified server-side on arrival —message.annotation carries kind
("otp" / "magic_link" / "other"), code, link, and 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 return None 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
get_session 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. s["healed"] is True when that happened, and a cold call can take a minute.
Keyword options: verify=True (set False to skip the freshness check), max_stale_sec=None to
override the freshness window, and timeout_ms=240_000 capping the whole re-login-and-retry loop.
open_session(registration_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 storage_state.
Note it returns the state dict directly, not a wrapper:
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 live_view_url, then fails with BLOCKED_TIMEOUT.
x-sente-secret header — verify it with
hmac.compare_digest. message.received payloads are thin notifications with no body; fetch
content with sente.messages.get(message_id). Full payloads in Webhooks.
Errors
Non-2xx responses raiseSenteError with status (the HTTP status) and body — the parsed
error payload, usually a dict with error and code. (The TypeScript SDK hands back raw text
instead; the two differ here.)
SESSION_STALE and SESSION_PENDING (also 409) are handled inside get_session — it polls the
run they name and retries — so you only see them from the raw open_session.
Method reference
set_credentials overwrites the vault only — it does not change the password at the app, so
the two can desync.
Identities, for when you want to control the address
Identities, for when you want to control the address
identity_id 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).local_part raises a 409 SenteError (on_conflict="error" is the default); pass
on_conflict="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. sente.messages.stream(identity_id) is a 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.
- TypeScript SDK · CLI · API reference.
