Limits and waiting
Waiting instead of polling
Section titled “Waiting instead of polling”POST /v1/messages/search takes a wait, in milliseconds. The request does not
answer until something matches:
curl -X POST https://api.facteur.eu/v1/messages/search \ -H "Authorization: Bearer $FACTEUR_KEY" \ -H "content-type: application/json" \ -d '{"inbox": "j3k9x2mq", "sentTo": "order-4821", "wait": 30000}'What that guarantees:
- It answers as soon as the message arrives, not at the deadline.
- The first look happens before any waiting. A message that arrived a second ago costs nothing — the common case in a suite, where the mail is often already there by the time the assertion runs.
- What comes back is fully parsed, subject, one-time code and links included. A wait that answered as soon as a row existed would hand you a message with no code in it.
- Nothing found by the deadline is a
200with an empty list, not an error. An absent message is an answer; whether that is a failure is your test’s call.
No wait, or wait: 0 |
One query, one answer. The search is still a search. |
| Maximum | 300000 ms (five minutes). Above that it is clamped, not refused. |
| Concurrent waits per organization | 20, then 429 TOO_MANY_WAITS |
The ceiling on concurrent waits is refused rather than queued: each wait is an HTTP connection held open, and a suite leaking requests would take the connection pool down with it — at which point every other customer times out, which is the worst way to find out.
Paging
Section titled “Paging”Two read routes, and only one of them pages:
| Route | Paging |
|---|---|
GET /v1/inboxes/{id}/messages |
None. The latest 50, newest first, no cursor. |
POST /v1/messages/search |
limit (50 by default, 200 at most) and nextCursor. |
Pass nextCursor back exactly as you received it. It is opaque — its contents
are ours to change — and it is null on the last page, so a client loops on
truthiness rather than comparing a count to a limit it may not have sent.
The cursor is keyed on the message rather than on an offset, deliberately: an offset over a table that receives mail while you page through it repeats and skips rows.
status, and the race it makes visible
Section titled “status, and the race it makes visible”Parsing happens after the SMTP acknowledgement, so a message exists for a few milliseconds with no subject, no code and no links.
status |
What it means |
|---|---|
received |
Accepted and stored, not parsed yet. Look again. |
parsed |
Everything derived from the body is populated. |
quota_exceeded |
Refused for want of plan. Nothing stored, nothing parsed. |
The search returns only parsed messages by default. Returning received
rows would make “the mail has not arrived” and “the mail is here but not read
yet” indistinguishable — the same failure looking like two different ones, one
time in fifty. Pass "status": "any" if you specifically want to watch that
transitional state.
GET /v1/inboxes/{id}/messages has no such filter: it returns what is in the
drawer, whatever state it is in. If you read that route in a loop, require
status === 'parsed' yourself.
Matching
Section titled “Matching”Text criteria — sentTo, sentFrom, subject, body — are case-insensitive
substrings, not full-text. Two consequences worth knowing:
%and_are ordinary characters. Searching for100%matches the literal string rather than everything.- Accents and casing are matched as written, so a word spelled differently will not be found. There is no stemming.
body is searched in the text part and the HTML part, and either one
matching counts — whatever match says about the criteria as a whole. You should
not have to know which part the sender used.
since is a bound, not a criterion: it narrows the window in both all and
any modes. Folding it into an any disjunction would make “since yesterday or
from Camille” return messages from before yesterday.
Retention
Section titled “Retention”Messages are kept for as long as the plan sells, per inbox. retentionDays: 0
means accept, measure, store nothing — a deliberate choice, not a misconfiguration.
Note that messageCount on an inbox counts what is stored, so retention
erodes it. What quotas are metered on is messagesThisMonth, which comes from
the usage ledger and does not shrink.
What does not exist yet
Section titled “What does not exist yet”- Attachments. Storage anticipates them; the API does not serve them.
- A published SDK. Everything here is plain HTTP.
- Full-text search. See above.