Skip to content

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 with the small worker in the source repo (workers/email-inbound). It is free, and it is what hosted Payvy uses.

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:
    Terminal window
    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.

  • 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.

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:

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>