Built for the agents that build websites.
Everything a human can do in the dashboard, an agent can do with an API key. The exit test for this property: a coding agent in a fresh site repo, given only a key, ships a working contact form with email and Telegram in one conversation.
Set up your agent in seconds.
Three ways in, none of them a dashboard.
Paste to your agent
One line. The agent reads llms.txt, mints its own key by email code, and builds the form.
Set up a contact form on this site with Postbag. Read https://postbag.dev/llms.txt first. Set up a contact form on this site with Postbag. Read https://postbag.dev/llms.txt first.
Install the skill
An Agent Skill with the decision tree, the quickstart call and the embed snippet already in it.
npx skills add faahim/postbag --skill postbag
# works once the repo is public — or fetch the file directly:
curl https://postbag.dev/.well-known/skills/postbag/SKILL.md npx skills add faahim/postbag --skill postbag # works once the repo is public — or fetch the file directly: curl https://postbag.dev/.well-known/skills/postbag/SKILL.md
MCP or CLI
Tools that mirror the API one to one, for agents and terminals that prefer not to shell out to curl.
npx -y @postbag/mcp # env: POSTBAG_API_KEY=pb_live_…
npx postbag login # in progress — CLI/MCP not yet published to npm npx -y @postbag/mcp # env: POSTBAG_API_KEY=pb_live_… npx postbag login # in progress — CLI/MCP not yet published to npm
How does the agent get a key?
Two calls, one human step in between. A new email provisions an organization first; an existing email reuses its own. The key is shown once.
$ curl -X POST https://postbag.dev/v1/auth/request-code -d '{"email":"[email protected]"}'
{ "ok": true, "expires_in": 600, "next": "POST /v1/auth/verify-code with { email, code, key_name }" }
# — a human reads the 6-digit code out of their inbox and gives it to the agent —
$ curl -X POST https://postbag.dev/v1/auth/verify-code -d '{"email":"[email protected]","code":"482913"}'
{ "api_key": "pb_live_…", "organization": { "id": "org_…", "slug": "…", "name": "…" },
"user": { "email": "[email protected]", "created": true },
"next": [ "POST /v1/quickstart to create a routed form in one call", "GET /v1/me to see limits and what exists" ] } $ curl -X POST https://postbag.dev/v1/auth/request-code -d '{"email":"[email protected]"}'
{ "ok": true, "expires_in": 600, "next": "POST /v1/auth/verify-code with { email, code, key_name }" }
# — a human reads the 6-digit code out of their inbox and gives it to the agent —
$ curl -X POST https://postbag.dev/v1/auth/verify-code -d '{"email":"[email protected]","code":"482913"}'
{ "api_key": "pb_live_…", "organization": { "id": "org_…", "slug": "…", "name": "…" },
"user": { "email": "[email protected]", "created": true },
"next": [ "POST /v1/quickstart to create a routed form in one call", "GET /v1/me to see limits and what exists" ] } Four calls from key to verified.
-
1 Discover
Read GET /llms.txt (Markdown, written for agents) or GET /openapi.json (the full contract, generated from the live routes). Then GET /v1/me: organization, scopes, plan limits and counts of what already exists.
curl https://postbag.dev/llms.txt curl https://postbag.dev/v1/me -H "Authorization: Bearer pb_live_…"curl https://postbag.dev/llms.txt curl https://postbag.dev/v1/me -H "Authorization: Bearer pb_live_…"
-
2 Create
POST /v1/quickstart creates the project if missing, the form, an email destination and a direct route, idempotently by (project, name). It returns the submit URL, embed snippets for HTML, fetch, React, Astro and Next.js, a verification recipe, and next[] calls.
curl -X POST https://postbag.dev/v1/quickstart -H "Authorization: Bearer pb_live_…" -d '{ "name": "Portfolio contact", "notify_email": "[email protected]", "origin": "https://example.com", "project": "portfolio" }'curl -X POST https://postbag.dev/v1/quickstart -H "Authorization: Bearer pb_live_…" -d '{ "name": "Portfolio contact", "notify_email": "[email protected]", "origin": "https://example.com", "project": "portfolio" }' -
3 Verify
Post a test submission. The response carries the submission id and delivery ids; poll a delivery until it is sent. No inbox to check, no human in the loop.
curl -X POST https://postbag.dev/s/fm_8f3kq2 -H "content-type: application/json" \ -d '{"email":"[email protected]","message":"test","_test":true}' # → { "ok": true, "submission_id": "sb_4d2k91", "status": "received", "deliveries": ["dl_a91x02"] } curl https://postbag.dev/v1/deliveries/dl_a91x02 -H "Authorization: Bearer pb_live_…" # → { "status": "sent", "attempts": 1, "response": { "status": 200, "latency_ms": 412 } }curl -X POST https://postbag.dev/s/fm_8f3kq2 -H "content-type: application/json" \ -d '{"email":"[email protected]","message":"test","_test":true}' # → { "ok": true, "submission_id": "sb_4d2k91", "status": "received", "deliveries": ["dl_a91x02"] } curl https://postbag.dev/v1/deliveries/dl_a91x02 -H "Authorization: Bearer pb_live_…" # → { "status": "sent", "attempts": 1, "response": { "status": 200, "latency_ms": 412 } } -
4 Wire
Add destinations and routes with individual calls, test any destination with POST /v1/destinations/{id}/test, and write postbag.json into the repo so the next session finds it.
curl -X POST https://postbag.dev/v1/destinations -H "Authorization: Bearer pb_live_…" -d '{ "type": "webhook", "name": "CRM", "config": { "url": "https://crm.example.com/hooks/postbag", "secret": "…" } }' curl -X POST https://postbag.dev/v1/destinations/ds_7hm2q0/test -H "Authorization: Bearer pb_live_…"curl -X POST https://postbag.dev/v1/destinations -H "Authorization: Bearer pb_live_…" -d '{ "type": "webhook", "name": "CRM", "config": { "url": "https://crm.example.com/hooks/postbag", "secret": "…" } }' curl -X POST https://postbag.dev/v1/destinations/ds_7hm2q0/test -H "Authorization: Bearer pb_live_…"
What agent-native means here
It is a property the whole surface has, checked on every change, not a page in the docs.
- Discoverable
- llms.txt, openapi.json, and /v1/me. Accept: text/markdown on the site root returns the agent onboarding page.
- One call to a working form
- POST /v1/quickstart. Everything it does is also available as individual calls; it is a convenience, not a special path.
- Verifiable
- _test submissions return delivery ids to poll. Destinations have a /test endpoint that returns the provider's response.
- Schema-aware, fleet-aware
- GET /v1/streams/{id} returns the stream schema and a form template; POST /v1/forms with from_template creates a form that is pre-attached with a valid mapping. An agent cannot produce a form the stream does not understand.
- Idempotent and safe to retry
- Idempotency-Key on every POST. if_exists: "return" on creates. Re-running setup is a no-op, not a mess.
- Errors that teach
- { code, message, hint, docs } on every error; next[] on every create. Ids are prefixed so they are self-describing in logs and conversation.
Leave the wiring where the next agent will look.
Site factories and agent scaffolds should write postbag.json into the repo and a line into CLAUDE.md or AGENTS.md. The next session, human or agent, finds the form id without guessing. Never hand-write a submit URL.
Coming next as thin clients over the same API: npx postbag init (writes postbag.json) and an MCP server whose tools mirror the API one to one plus postbag_quickstart and postbag_explain.
# CLAUDE.md / AGENTS.md (site repo)
Forms on this site post to Postbag. Config lives in postbag.json (form_id, submit_url, project).
To add a form: POST https://postbag.dev/v1/forms with the project slug, then use the embed from the response.
Never hand-write a submit URL. Verify with a _test submission and poll the delivery. # CLAUDE.md / AGENTS.md (site repo) Forms on this site post to Postbag. Config lives in postbag.json (form_id, submit_url, project). To add a form: POST https://postbag.dev/v1/forms with the project slug, then use the embed from the response. Never hand-write a submit URL. Verify with a _test submission and poll the delivery.
Agent questions
What does an AI agent need to use Postbag?
POST /v1/auth/request-code), a human reads the code out of their inbox, and POST /v1/auth/verify-code returns a manage-scoped key (and provisions an organization for a new email). From there it can read /llms.txt and /openapi.json, call GET /v1/me, create a routed form with POST /v1/quickstart, verify with a _test submission, and add destinations or streams, all without a browser.How does an agent get an API key without a browser?
POST /v1/auth/request-code {"email":"…"} emails a 6-digit code good for 10 minutes; the human reads it out and gives it to the agent; POST /v1/auth/verify-code {"email":"…","code":"…"} returns a pb_live_… key. postbag login (once published) or npx skills add faahim/postbag --skill postbag drives this same flow.How does an agent verify that a form actually works?
_test: true. The response includes the submission id and delivery ids; polling GET /v1/deliveries/{id} shows sent along with the provider's response. Test submissions are excluded from quotas.Is there an MCP server or CLI?
npx postbag login, npx postbag init, and npx -y @postbag/mcp. Today the recommended path is the HTTP API directly; it is small and fully described by /openapi.json. Nothing the CLI or MCP server does is impossible over HTTP.What happens when an agent makes a mistake?
{ code, message, hint, docs }. The hint says what to do next; docs is a deep link into the error reference. Validation errors list the offending fields. Incomplete stream mappings fail at attach time with the missing fields listed.Is it safe for an agent to re-run a setup script?
Idempotency-Key is honoured on every POST under /v1, and creates accept if_exists: "return" so re-running returns the same objects instead of duplicates or errors.How should a site repo remember its Postbag wiring?
postbag.json at the repo root with form_id, submit_url and project, and a line in CLAUDE.md or AGENTS.md saying forms on this site post to Postbag. Later sessions, human or agent, find the wiring there.Give your agent a key.
Create an account, mint an API key with the manage scope, and hand it to Claude Code, Cursor or Codex with one instruction: read /llms.txt and set up a contact form.