What you need
- An
identityId. The wait endpoint is per-identity. If you let Sente auto-provision the identity (noidentityIdpassed to connect/register), read it back off the account:registration.identityId. - An email code. Only email lands in a Sente inbox. SMS codes never arrive here — a run that hits SMS 2FA blocks and pages a human (human takeover); nothing is worked around.
The complete flow
Stamp a timestamp before you trigger the email, then wait with thatsince. A code that lands
in under a second cannot be missed, and a stale code from an earlier attempt cannot be picked up by
mistake.
otp.message, r.message),
and null / None on timeout.
The wait contract
GET /v1/messages/wait — the endpoint both helpers call.
How the extraction works
Every inbound email gets anannotation before anything observable happens (webhook delivery,
wait matching, kind filtering):
@sente.run address:
- The system prompt is fixed and never composed from email content.
- The email is passed as untrusted data inside an
<email>block, explicitly labelled as data, not instructions. - The model has no tools. The worst a hostile email can do is make itself be classified wrong.
- The output is schema-constrained and re-validated server-side before it is stored.
When nothing arrives
Annotation failed for a message — is it lost?
Annotation failed for a message — is it lost?
No. Annotation failure never blocks delivery: the message is stored and the
message.received
webhook still fires — the message just has annotation: null when you fetch it. A background sweep
re-annotates recent unannotated
inbound messages roughly once a minute, so a kind wait started shortly after arrival still
matches once it heals. The sweep stops retrying messages older than about 15 minutes — by then the
code has expired anyway.Two flows on the same identity at once
Two flows on the same identity at once
Give each flow its own explicit
since, stamped immediately before that flow triggers its email.
Without since, both waits fall back to the same 60-second lookback and can match each other’s
code. If both codes come from the same sender, also read message.subject before submitting.I fetched the message directly and annotation was null
I fetched the message directly and annotation was null
Annotation runs right after the message is persisted, so a
GET /v1/messages/:id issued in that
window can see annotation: null. Waiting with kind set never has this problem — an unannotated
message simply doesn’t match, and matches as soon as it is annotated.Push instead of poll
If your service has a public HTTPS endpoint, register a webhook formessage.received instead. Sente pushes a notification for every inbound email and you fetch the
full message — annotation included — by id. Long-polling is the right tool for a process with no
public URL (a laptop, a CI job, a container behind NAT).
Next steps
The account's inbox
List, read, send, reply, and archive.
Webhooks
Get pushed
message.received events instead of polling.Messages API
Every parameter and response shape.
