Skip to content

Limits and waiting

POST /v1/messages/search takes a wait, in milliseconds. The request does not answer until something matches:

Terminal window
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 200 with 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.

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.

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.

Text criteria — sentTo, sentFrom, subject, body — are case-insensitive substrings, not full-text. Two consequences worth knowing:

  • % and _ are ordinary characters. Searching for 100% 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.

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.

  • Attachments. Storage anticipates them; the API does not serve them.
  • A published SDK. Everything here is plain HTTP.
  • Full-text search. See above.