Quickstart
Five requests, and the last one is the one your test suite keeps.
1. Mint a key
Section titled “1. Mint a key”From the application: Settings → API keys → Create a key, at organization level. You need to be an owner or an admin.
Take the defaults unless you know otherwise — read-only, aimed at your main workspace, ninety days. A test suite reads messages; it rarely needs to create anything.
The secret is shown once. Only a digest of it is stored, so neither you nor we can recover it afterwards. Paste it straight into your CI secrets.
export FACTEUR_KEY=fk_...2. Check the key works
Section titled “2. Check the key works”curl -H "Authorization: Bearer $FACTEUR_KEY" \ https://api.facteur.eu/v1/inboxesAn empty array is a valid answer: the key works, you have no inboxes yet. A
401 means the key is unknown, revoked or expired — the code says which.
3. Open an inbox
Section titled “3. Open an inbox”Inboxes live inside a workspace, so name the one you want. GET /v1/workspaces
lists them; every organization has at least a default one.
curl -X POST https://api.facteur.eu/v1/workspaces/main/inboxes \ -H "Authorization: Bearer $FACTEUR_KEY" \ -H "content-type: application/json" \ -d '{"name": "signup flow"}'The response carries domain — the address this inbox receives at:
{ "id": "j3k9x2mq", "name": "signup flow", "domain": "j3k9x2mq.inbox.facteur.eu", "retentionDays": 15}4. Send mail to it
Section titled “4. Send mail to it”Point your application at that address, and use subaddressing to tell one test’s mail from another’s:
signup+run-4821@j3k9x2mq.inbox.facteur.euEverything after the + is yours. Put the test run, the user, the branch —
whatever makes the message you are waiting for unambiguous. This is what lets
two tests share an inbox without racing.
5. Wait for the message
Section titled “5. Wait for the message”The request your suite keeps:
curl -X POST https://api.facteur.eu/v1/messages/search \ -H "Authorization: Bearer $FACTEUR_KEY" \ -H "content-type: application/json" \ -d '{"inbox": "j3k9x2mq", "sentTo": "signup+run-4821", "wait": 30000}'Issue it before triggering whatever sends the mail. It will wait, and it answers the moment the message lands rather than at the deadline.
{ "messages": [ { "id": "…", "status": "parsed", "from": { "address": "no-reply@example.test", "name": "Example" }, "to": "signup+run-4821@j3k9x2mq.inbox.facteur.eu", "subject": "Your confirmation code", "otp": "418302", "links": ["https://example.test/confirm?token=…"], "text": { "body": "…" } } ], "nextCursor": null}otp is the field most suites assert on. It is populated by the ingestion
engine, which is why status reads parsed — see
Limits and waiting for what the other statuses mean and when
you will see them.
In a test
Section titled “In a test”const search = (body) => fetch('https://api.facteur.eu/v1/messages/search', { method: 'POST', headers: { authorization: `Bearer ${process.env.FACTEUR_KEY}`, 'content-type': 'application/json', }, body: JSON.stringify(body), }).then((r) => r.json());
test('sign-up sends a confirmation code', async () => { const address = `signup+${crypto.randomUUID()}@j3k9x2mq.inbox.facteur.eu`;
// Start waiting before the mail is sent, not after. const arriving = search({ inbox: 'j3k9x2mq', sentTo: address, wait: 30_000 });
await signUp(address);
const { messages } = await arriving; expect(messages).toHaveLength(1); expect(messages[0].otp).toMatch(/^\d{6}$/);});Two details separate a reliable test from a flaky one, and both are in that
snippet: filter on sentTo rather than on the subject or on “the latest
message”, and start the search first.
- Authentication — scopes, which workspaces a key reaches, rotating without downtime.
- Errors — the shape every failure has.
- API reference — every operation, in full.