# e2email API

Base URL: https://h88e3.viberunner.org

Authentication: sign in through AuthLock at `/login`; the session token is the `e2sid` cookie
or `Authorization: Bearer <token>`. Message bodies are end-to-end encrypted — the API moves
ciphertext envelopes; encryption and decryption happen on the device (see `/shared/crypto.js`:
RSA-OAEP-3072 identity keys, AES-256-GCM per message, passphrase-wrapped private key).

## Sign-in

### POST /api/authlock/session

Exchange an AuthLock widget handoff code for a session.

- Auth: none (same-origin)
- Body: `{ code, next? }`
- Response: `{ token, csrf, redirectTo }`
- The email must be verified at AuthLock. The token is also set as the e2sid cookie; native apps send it as Bearer.

### DELETE /api/authlock/session

Sign out of this service.

- Auth: session
- Response: `{ ok }`

### POST /api/authlock/webhook

AuthLock events (blocked/deleted users are signed out everywhere).

- Auth: authlock-signature
- Response: `{ ok }`

## Account

### GET /api/me

The signed-in user, public key and change cursor.

- Auth: session
- Response: `{ user, seq, unlockedOnServer, sessions }`

### POST /api/identity

Upload the identity key made on the device (passphrase-wrapped).

- Auth: session
- Body: `{ publicKey, wrappedKey, replace?, stolen? }`
- Response: `{ ok, fingerprint, keyChange?, stripped? }`
- Only when the account has no key yet, or within 10 minutes of signing in with { replace: true }; at most 3 replacements a day (429). 409 for a key that is (or was) another account's. A replacement is recorded as a pending key change for the other members of your shared mailboxes to approve, and your old fingerprint is removed from your own messages and from the copies of the mailboxes you are a member of — never from anyone else's private mail. Until approved, the new key reads only your own copies.

## Mailboxes

### GET /api/saas/mailboxes

WorkflowEmail mailboxes this user can reach.

- Auth: session
- Response: `{ mailboxes: [{ id, accountId, address, canSend, lastSync, error }] }`

### GET /api/saas/mailboxes/:id/members

Members of one of your mailboxes (:id = your account id or mailbox id) and their keys.

- Auth: session (member)
- Response: `{ mailbox, members: [{ userId, address, name, fingerprint, publicKey, trusted, rejected, lastKeyChange, recovery? }] }`

### GET /api/saas/seal-keys?account=

Extra keys to seal what you store for an account to: the other members' trusted keys (first key or approved; not rejected for this mailbox), your recovery key.

- Auth: session
- Response: `{ keys: [{ address, publicKey, fingerprint }] }`

### POST /api/saas/mailboxes/refresh

Ask WorkflowEmail again (after being added to a domain).

- Auth: session
- Response: `{ ok, reason?, mailboxes }`

## Key changes

### GET /api/saas/people

Settings → People & keys: everyone who shares a mailbox with you (one entry per person) and your own key.

- Auth: session
- Response: `{ me: { userId, address, name, fingerprint, recovery, keyChange }, people: [{ userId, name, address, mailboxes, fingerprint, publicKey, status: "trusted"|"pending"|"rejected", trust: "first"|"approved"|null, approvedBy, approvedByYou, rejectedByYou, canApprove, change, rekey: { scope: "change", changeId } | { scope: "own", with }, recovery }], sharedMailboxes }`

### GET /api/saas/key-changes

Your latest key change, and colleagues' new keys waiting for you.

- Auth: session
- Response: `{ mine, toApprove, approvedByMe, recoveryKey, notices }`

### POST /api/saas/notices/:id/seen

Dismiss a security notice (e.g. "A recovery key was added to your account").

- Auth: session
- Response: `{ ok }`

### POST /api/saas/key-changes/:id/approve

Approve a colleague's new key after checking its fingerprint with them by phone or in person.

- Auth: session, signed in ≤ 10 min ago, member of a shared mailbox
- Body: `{ fingerprint }`
- Response: `{ ok, keyChange }`
- 409 unless the fingerprint is exactly the pending one; 403 for a stale session or a non-member.

### POST /api/saas/key-changes/:id/reject

Reject a colleague's new key for the shared mailboxes you have in common with them (your copies and that address's directory entry); their personal address is unaffected.

- Auth: session, signed in ≤ 10 min ago, member of a shared mailbox
- Response: `{ ok }`

### POST /api/saas/key-changes/:id/complete

Report the re-key job finished: no further re-key / re-seal ops under this change.

- Auth: session (the approving member)
- Response: `{ ok }`

### GET /api/saas/rekey/work?scope=change|own|recovery&change=&phase=theirs|mine&with=&after=&limit=

A page (≤ 200) of messages to re-key: key slots only, plus the target keys (each with trusted / approvedByYou) and stale fingerprints per account. scope=own&with=<userId>: only the mailboxes you share with that person.

- Auth: session
- Response: `{ items: [{ id, account_id, keys, mine, mineKeys?, resealed, refetchable }], accounts: { [accountId]: { target, drop } }, next, total?, since, subject, completed? }`

### POST /api/saas/rekey

Apply re-key ops, all or nothing (≤ 16 MB, ≤ 200 ops, at most 4 such requests at a time service-wide: 429).

- Auth: session
- Body: `{ ops: [{ kind: "message.rekey", id, keys } | { kind: "message.reseal", id, from }] }`
- Response: `{ ok, applied }`
- message.rekey changes only envelope.keys: added slots must be current members' (approved) keys, removed slots must belong to no current member, existing slots may not be rewritten. On a colleague's message you must share its mailbox (403) and have approved their current key (409), from a fresh sign-in or the approving session (for 24 h after approving, until the job is reported complete; 409 after). message.reseal (a colleague's copy that has no slot for their current key, once per copy) carries no envelope: the server copies your own copy `from` of the same message in the same shared mailbox — which must already have a slot for their new key — with its slots filtered to the current trusted members, and marks it "restored from your copy".

### POST /api/saas/rekey/refetch

Fetch mail that travelled in the clear again from WorkflowEmail and seal it to the owner's current key (≤ 20 ids; at most 2 such requests at a time service-wide: 429).

- Auth: session (owner, or the approving member)
- Body: `{ ids }`
- Response: `{ results: [{ id, ok, error? }] }`

### GET /api/saas/recovery-key

Your recovery key: public half and the phrase-wrapped private half.

- Auth: session
- Response: `{ exists, fingerprint?, publicKey?, wrappedKey? }`

### PUT /api/saas/recovery-key

Create or replace your recovery key (409 if it is another account's key). You are told about it on your next visit.

- Auth: session, signed in ≤ 10 min ago
- Body: `{ publicKey, wrappedKey }`
- Response: `{ ok, fingerprint }`

### DELETE /api/saas/recovery-key

Remove your recovery key (and its slots).

- Auth: session, signed in ≤ 10 min ago
- Response: `{ ok }`

## Mail

### GET /api/sync?since=<seq>

Changes since a cursor — messages, threads, labels, contacts, accounts.

- Auth: session
- Response: `{ changes, deleted, seq, more }`

### POST /api/ops

Idempotent batch of operations (flags, labels, send, draft, contacts, settings).

- Auth: session
- Body: `{ ops: [{ id, kind, payload }] }`
- Response: `{ results, seq }`
- send/draft carry an envelope already encrypted on the device.

### POST /api/fetch

Pull new mail from WorkflowEmail now and flush the outbox.

- Auth: session
- Response: `{ results, sent }`

### GET /api/search?q=

Search headers (bodies are encrypted; search them on the device).

- Auth: session
- Response: `{ items }`

## Keys

### GET /api/keys?address=a@x,b@y

Find public keys for recipients and file them as contacts.

- Auth: session
- Response: `{ found: [{ address, fingerprint, source }] }`

### GET /.well-known/e2email/keys?address=

Public key directory for users of this service.

- Auth: none (rate limited)
- Response: `{ address, publicKey, fingerprint, keys: [{ publicKey, fingerprint }] }`
- Point another e2email server here with E2E_KEY_DIRECTORY to encrypt to our users. An address several people share lists every key in keys[]; encrypt to all of them.

## Service

### GET /healthz

Liveness.

- Auth: none
- Response: `{ ok }`
