Skip to content

Open source. Agent-native. Yours.

Your forms have somewhere to go.

Your agent can build, wire and test your form before you even sign up. Claim it whenever you want.

Self-host Postbag

One submit URL

Your form already knows where to go.

Point your form at one URL, done — whatever your site speaks. We never say "got it" until we have.

POST postbag.dev/s/fm_73c74vjq6z24 live

_test: true means we save your hello without emailing anyone.

This is a real Postbag form, living its normal life. Go ahead, say hello.

Use what your site already uses.

Ready-to-paste snippets, generated for your own form too.

Submit reference
index.html
<form action="https://postbag.dev/s/fm_8f3kq2" method="POST">
  <label>Email<input type="email" name="email" required /></label>
  <label>Message<textarea name="message" required></textarea></label>
  <button type="submit">Send</button>
</form>
submit.js
await fetch("https://postbag.dev/s/fm_8f3kq2", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(data),
})
// → { ok: true, submission_id: "sb_4d2k91", status: "received" }
ContactForm.jsx
export function ContactForm() {
  async function submit(event) {
    event.preventDefault()
    await fetch("https://postbag.dev/s/fm_8f3kq2", { method: "POST", body: new FormData(event.currentTarget) })
  }
  return <form onSubmit={submit}>{/* email, message, send */}</form>
}
actions.ts
"use server"

export async function submitForm(formData: FormData) {
  await fetch("https://postbag.dev/s/fm_8f3kq2", { method: "POST", body: formData })
}
// works as a form action — no client JavaScript needed
terminal
curl -X POST https://postbag.dev/s/fm_8f3kq2 \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","message":"Hello","_test":true}'

# {"ok":true,"submission_id":"sb_4d2k91","status":"received"}
HTML
Plain forms work exactly as they are.
JSON
fetch gets a receipt back, with an id to prove it.
Redirect
No JavaScript? Visitors land right back on your page.

It lands before it travels.

Someone fills in your form at 2am on a Saturday. Your email is mid-outage. On Monday the message is still safely in Postbag. If the retries ran out, the Delivery is marked and ready to send again once email is back.

Message sb_4d2k91 incoming
  1. HTTP 200

    Received

    The 2am message is kept the instant it lands. Even spam gets a label, not a trapdoor.

  2. committed

    Saved first

    Saved means saved — before we so much as think about emailing anyone.

  3. worker awake

    Routed second

    Sending comes second. A bad email day just means waiting, never vanishing.

  4. attempt 2 sent

    Kept on record

    Failed sends retry a set number of times. If they run out, the Delivery stays clearly marked and ready to retry when the other side is back.

The agent can finish before you sign up.

It builds the form, wires it in, and proves messages arrive. You show up at the end to say "keep it."

  1. Create

    Your agent asks Postbag for a form and gets one. No account, no API key, no waiting on you.

  2. Wire

    The form has its permanent address from day one. The agent wires it into your site.

  3. Prove

    It sends a few test messages and reads them back. You get proof, not promises.

  4. Claim

    Like what you see? Sign in and claim it. Same form, same address — now it's yours.

  5. Route

    Tell it where messages should go — your inbox, Telegram, wherever — and you're live.

create, then prove receipt
$ curl -X POST https://postbag.dev/v1/public/sandboxes \
  -H "content-type: application/json" \
  -H "Idempotency-Key: <uuidv4>" \
  -d '{"name":"Contact","origin":"https://example.com"}'

{ "sandbox": { "id": "fm_…", "status": "active",
    "submit_url": "https://postbag.dev/s/fm_…",
    "expires_at": "2026-08-25T09:00:00Z",
    "accepted_count": 0, "remaining": 5 },
  "sandbox_token": "pbs_…", "claim_url": "https://postbag.dev/app/claim#token=…" }

$ curl https://postbag.dev/v1/public/sandboxes/fm_… \
  -H "Authorization: Sandbox pbs_…"

{ "id": "fm_…", "status": "active", "remaining": 4,
  "submissions": [{ "id": "sb_…", "received_at": "…" }] }

This is the whole setup, exactly as your agent sees it. The nice part: you never have to read it.

One form is simple. Fifteen can stay that way.

Fifteen sites, each naming its fields a little differently. Postbag lines them up, so everything arrives in one tidy place.

Form · office-contact

fullName, company, tel

Form · machine-enquiry

name, org, phone

Form · partner-lead

contact, business, mobile

and twelve more by tag

Stream · enquiries

Schema v3

name
string
company
string
phone
string
site
constant

If a form doesn't fit, Postbag says so up front — not three weeks later, in production, at night.

01

Route

partner webhook

02

Route

daily ops email

03

Route

CRM webhook

Change the shape whenever you need. Older records keep their old shape — nothing breaks quietly behind your back.

Get things the moment they arrive, or as one tidy daily digest. Spam never tags along.

Explore routing

No crossed fingers

Promises you don't have to take our word for.

Servers crash. Ours too — that's computers. Your messages are already safe when it happens.

Never lose a message

Spam, rate limits, odd traffic — everything that arrives is kept, with a label, until you delete it or its retention window ends.

No side doors

The dashboard, the CLI and your agent all go through the same front door. Anything you can click, an agent can do.

Yours means yours

Every record belongs to your workspace, fenced twice over. No one else's account can so much as glance at it.

  1. unique (form_id, idempotency_key)

    Reusing an idempotency key cannot create a duplicate Submission.

  2. unique (submission_id, route_id)

    Each Route gets one Delivery record per Submission.

  3. unique (route_id, period_key)

    Each Route gets one digest record per period.

  4. unique (form_id, version)

    Old records are never quietly rewritten.

  5. organization_id not null

    Your data belongs to you, and the rules know it.

Each line pairs a real database constraint with the practical promise it can actually keep.

Send it somewhere useful.

Your inbox, Telegram, your CRM — set up once, used by every form. Always testable before it matters.

Email

Lands in your inbox, and hitting Reply goes straight to the person who wrote to you.

Telegram

A message in your chat the moment something arrives. Lovely for the phone-first life.

Webhook

For everything else: a retried nudge to any URL you own. Add a secret when the receiver should verify it.

Webhooks reach everything else.

Add a secret and Postbag signs every attempt, so your endpoint can verify it. If your server naps, we try up to 10 times, then keep the Delivery clearly surfaced for you to retry when it wakes up.

a webhook signed with your secret
POST https://crm.example.com/postbag
Content-Type: application/json
Postbag-Signature: t=1724200000,v1=<hmac-sha256>
Postbag-Delivery: dl_a91x02
Postbag-Event: submission.received

{ "id": "dl_a91x02", "type": "submission.received",
  "schema_version": 3,
  "form": { "id": "fm_8f3kq2", "slug": "office-contact" },
  "data": { "name": "Maja Lind", "company": "Kontorsautomat AB" },
  "meta": {} }

Run it here. Or run it yourself.

Two doors into the same product — truly the same, not a generous demo. Cloud just means the server chores are ours.

postbag.dev

We keep it running.

Hosting, upgrades, backups, email plumbing — handled. Every plan gets the whole product; you pay for room, not features.

your-server.example

You keep the keys.

One container, one database, everything included. There is no smaller "community edition" — this is the edition.

One container
Run the whole thing, or split API and sender.
One database
Postgres is the only other moving part.
Open source
AGPL service. MIT SDK, CLI and MCP server.

Questions, answered

What is Postbag?
The open-source form backend built for agents. Point any form at your Postbag address; every message is saved the moment it arrives, then sent where you want it — inbox, Telegram, or a webhook. Your agent can run the whole setup.
What is a form backend?
The part of a contact form you'd rather not build: the place messages actually go. Your site's <form action> points at Postbag; Postbag keeps the message safe and tells you. No server code on your end.
Do I need JavaScript or an SDK?
No. A plain HTML form works as it is, and visitors land back on your page. Prefer fetch or a framework? Postbag hands you ready-made snippets for HTML, fetch, React, Astro and Next.js.
Does Postbag ever lose submissions?
No — this is the hill Postbag lives on. Everything that arrives is saved, spam included, each with a label. Things are deleted only when you delete them, or when your retention window says so.
How is Postbag different from Formspree, Formspark or Getform?
Three things. Every message is saved before it's sent anywhere, and each Delivery keeps its retry count and latest outcome. A coding agent can do the entire setup before you sign up. And you can run the whole product yourself, free. See the comparison pages.
Where can Postbag send messages?
Email (replying goes straight to the sender), Telegram, and webhooks for everything else — your CRM, your Slack bot, anything with a URL. Add a secret for HMAC signing. Native Slack and Discord are next.
Can coding agents like Claude Code or Cursor use Postbag?
Yes — that's the heart of it. An agent can create a form with no account, wire it into your site, and prove it works with test messages. You sign in only to keep the form and turn on real sending.
Can I self-host Postbag?
Yes. One Docker container plus one Postgres database is the entire product — the hosted version is the same code with billing on. There's a compose file to start from and a proper guide.
How much does it cost?
Free gets you 5 forms, 1,000 messages a month and 90 days of history. Pro is $15 a month, Team is $49 — bigger rooms, same product. Self-hosting is free, because it's open source. See pricing.

Give the form job to your agent.

It can build, wire and test the form before you even have an account. Claim it when it's worth keeping.

Read the quickstart