Authentication
Every request carries one Authorization header. There is no other way in — the
session cookie a browser holds is not something a CI runner has.
curl -H "Authorization: Bearer fk_your_key" \ https://api.facteur.eu/v1/inboxesThe Basic form works too, with the key as the username and an empty password:
curl -u "fk_your_key:" https://api.facteur.eu/v1/inboxesThat second form exists because some SDKs written for other platforms send it. If you are writing your own client, send the bearer.
Two axes, never one
Section titled “Two axes, never one”A key answers two questions separately. That is what makes it possible to grant a little at a time.
What it may do — four scopes, two families
Section titled “What it may do — four scopes, two families”| Scope | What it opens |
|---|---|
ws:read |
read the inboxes and messages of the workspaces it aims at |
ws:write |
create, modify and delete inboxes there; manage extractors |
org:read |
list the workspaces |
org:write |
create a workspace, rename it, mark it |
Write implies read within a family. Nothing crosses families. org:write
grants no access to your mail: a script that provisions workspaces has no
business reading the messages inside them.
Every operation in the reference states the scope it demands, and an invariant in this repository fails the build if that statement ever drifts from what the API enforces.
Where it may do it
Section titled “Where it may do it”All your workspaces, or only the ones you name. Say nothing and the key aims at your main workspace only — the narrowest reading, so a forgotten field never grants too much. The creation response always states the scope you actually got.
If you choose all workspaces, that covers the ones you create later too. A list frozen at creation time would be a key that quietly stops covering your new workspaces.
What a key never reaches
Section titled “What a key never reaches”Not a matter of scope — no scope opens these:
| Refusal | Why |
|---|---|
| Key management itself | A key that mints keys cannot be revoked: whoever holds it issues a fresh one the moment you take it away. |
| Your account — password, second factor, passkeys | A leaked CI secret must not be able to take an account over. |
403 KEY_CANNOT_INVITE — inviting anybody, by address or by link |
Human access granted by a key outlives that key’s revocation. Take the key away, the invitee is still a member. |
403 KEY_CANNOT_DELETE_WORKSPACE |
Deleting a workspace reattributes usage history and can be refused while a live key aims at it. That is a judgement call needing somebody to read it, and a script reads nothing. |
The last two answer a refusal that says so, rather than a bare 403. A “no key
reaches this” with no reason sends its reader looking for the scope that would
fix it, and there is none.
The key belongs to the organization
Section titled “The key belongs to the organization”Not to the person who created it, and not to a workspace. It outlives its author’s departure — a key tied to somebody would break your continuous integration on the day of a leaving party, with nobody making the connection.
Minting one requires an owner or an admin, and that act is the authorisation: a key has no role to look up, so the decision is made once, deliberately, by somebody who could have made it directly.
Expiry and rotation
Section titled “Expiry and rotation”Expiry is mandatory. Ninety days by default, a year at most. The only bound that closes without anybody having to act is the clock, and a key with no expiry outlives the pipeline it was minted for, the contractor who minted it, and the laptop it was pasted on.
Rotation exists so a running pipeline never sees a 401. It mints a fresh
key and leaves the old one answering for 24 hours (a week at most). You deploy
the new secret, watch one green build, and the old one dies on its own. A
rotation that cuts is a rotation nobody performs.
An already-revoked key cannot be rotated — that would keep it alive for hours and undo the revocation.
Revoking
Section titled “Revoking”Immediate, with no cache window: the next call fails, including one from a pipeline in flight. That is the intended behaviour.
Three refusals are kept distinct on purpose, because they call for different actions:
| Code | What happened | What to do |
|---|---|---|
KEY_UNKNOWN |
this key never existed | check what your CI is actually sending |
KEY_REVOKED |
somebody took it away | mint a new one |
KEY_EXPIRED |
the clock did its job | rotate, and next time before the deadline |
Folding them into one UNAUTHENTICATED would save a line in our code and cost an
hour in yours.
What does not exist yet
Section titled “What does not exist yet”- IP restriction — an allowlist, for runners that egress from a fixed address.
- Published SDKs. Everything here is plain HTTP. The clients the marketing site mentions are not released.