# Bills by email

Each organization gets an address like `k7f3q9x2m4p8@in.example.com`. Vendors send bills to it, or you forward them; PDF and image attachments land in the document inbox, including those of a forwarded email. A `+tag` is allowed (`k7f3q9x2m4p8+fuel@in.example.com`) and ignored.

Payvy doesn't run a mail server. Mail reaches it through a front end that posts each raw message to `POST /webhooks/email`, signed with a shared secret. The supported front end is [Cloudflare Email Routing](https://developers.cloudflare.com/email-routing/) with the small worker in the source repo (`workers/email-inbound`). It is free, and it is what hosted Payvy uses.

## Self-hosting with Cloudflare

You need a domain on Cloudflare (the free plan is enough). Use a subdomain for inbound mail, such as `in.example.com`, so it never touches mail for your main domain.

1. **Pick a secret** of at least 32 characters, e.g. `openssl rand -hex 32`. Keep it in your password manager; the server and the worker both need it.
2. **Configure the server**: set `PAYVY_INBOUND_DOMAIN=in.example.com` and `PAYVY_INBOUND_SECRET=<secret>`, and restart. `/webhooks/email` must be reachable from the internet (directly or through a Cloudflare Tunnel).
3. **Deploy the worker** from the source repo:
   ```bash
   cd workers/email-inbound
   npm install
   # set PAYVY_URL in wrangler.toml to your server's public URL
   npx wrangler secret put PAYVY_INBOUND_SECRET
   npx wrangler deploy
   ```
4. **Turn on Email Routing** for the domain in the Cloudflare dashboard, add `in.example.com` as a subdomain, and let Cloudflare create its MX and SPF records.
5. **Route everything to the worker**: in Email Routing's routing rules, set the catch-all address for `in.example.com` to *Send to a Worker* → `payvy-email-inbound`.
6. **Create your address**: a person runs `email.new_address` (REST or MCP). `email.address` shows it afterwards.

Send yourself a test bill and check `email.list` and `document.list`.

### What the worker does

- Retries a failed delivery twice, a few seconds apart.
- If Payvy is still unreachable, forwards the message to `FALLBACK_TO` when you've set one (it must be a verified Email Routing destination), so nothing is lost. Otherwise it fails and Cloudflare reports the failure to the sending server.
- Bounces mail to addresses Payvy didn't issue, and messages over 25 MB.

## Without Cloudflare

Anything that can run a script per message (Postfix with a pipe transport, a Mailgun or SES inbound hook, a small IMAP poller) can deliver instead. Post the raw message:

```text
POST /webhooks/email
Content-Type: message/rfc822
X-Payvy-Timestamp: <unix seconds, within 5 minutes of the server's clock>
X-Payvy-Recipient: <the envelope recipient>
X-Payvy-Signature: hex(HMAC-SHA256(secret, "{timestamp}.{recipient}.{hex(sha256(body))}"))

<the raw RFC 5322 message>
```