Agent guide: start a Postbag Form before signup
Create, wire, and verify a sandbox Form without credentials, then claim it and configure Delivery without changing its submit URL.
On this page
This page is written as an execution guide for coding agents. The dashboard is available when a person wants it, but the complete workflow also works through the API, CLI, MCP server, and Postbag skill.
Install the skill when your environment supports agent skills:
npx skills add faahim/postbag --skill postbag
The CLI, SDK, and MCP server are published as postbag, @postbag/sdk, and @postbag/mcp on npm.
Orient before changing anything
GET {API}/llms.txt # short Markdown onboarding
GET {API}/openapi.json # complete contract generated from live routes
GET {API}/v1/me # organization, scopes, limits, and counts after auth
Use the fixed vocabulary in user-facing text and implementation: Organization, Project, Form, Submission, Stream, Schema, Mapping, Destination, Route, Delivery, and Drift. Do not invent substitutes.
If postbag.json already exists, reuse its form_id and submit_url. Do not create a second Form for the same job.
Create a sandbox without credentials
Use a canonical UUIDv4 as the idempotency key. Generate it with a cryptographically secure random generator.
curl -X POST {API}/v1/public/sandboxes \
-H "content-type: application/json" \
-H "Idempotency-Key: <uuidv4>" \
-d '{ "name": "Contact", "origin": "https://example.com" }'
The response contains sandbox.submit_url, embed snippets, sandbox_token, claim_url, verification calls, and next[].
The capability is shown only in this creation response. Keep it secret. It can be reused to read and claim this sandbox until the sandbox is claimed or expires. The sandbox lasts 24 hours, accepts at most five 16 KiB test Submissions, never accepts attachments, and cannot create Destinations, Routes, Deliveries, Events, or outbound traffic.
Set claim_email only when the user explicitly supplied the email they will use for Postbag. Never infer it from Git metadata or another account.
Wire and prove receipt
Use the embed returned by Postbag. Keep the honeypot input and the exact submit URL.
POST {submit_url}
Origin: https://example.com
{ "email": "[email protected]", "message": "test" }
GET {API}/v1/public/sandboxes/{id}
Authorization: Sandbox <sandbox_token>
The read response is the proof: Postbag committed the Submission. Do not look for a Delivery before claim because anonymous receipt is deliberately inert.
Authenticate and claim
The email-code flow works without a browser:
postbag login --email [email protected]
postbag login --email [email protected] --code 123456
postbag sandbox claim --token "pbs_…"
The HTTP flow is POST /v1/auth/request-code, POST /v1/auth/verify-code, then POST /v1/sandboxes/{id}/claim with Bearer authentication and the Postbag-Sandbox-Token header. API key names are 1-32 characters.
Google and GitHub OAuth are optional. They are browser alternatives only when the instance operator configured both credentials for a provider. The email-code and API-key path remains the portable agent path.
Claiming preserves the Form id and submit URL. It copies anonymous rows as test Submissions, but those tests never deliver retroactively.
Submit an attachment after claim
Use multipart only after the Form belongs to an Organization. Keep the field name
meaningful; the resulting Submission replaces it with an fl_… reference and exposes
attachment metadata alongside data.
curl -X POST "$SUBMIT_URL" \
-H "Idempotency-Key: <uuidv4>" \
-F 'message=Feedback from the settings screen' \
-F 'screenshot=@./settings.png;type=image/png'
Free allows 2 MiB per attachment, 3 per Submission and 100 MiB retained; Pro allows 10 MiB, 10 and 10 GiB; Team allows 15 MiB, 20 and 100 GiB. The multipart request as a whole is capped at 16 MiB. Do not put a storage credential, public object URL or binary content in a Route configuration: authenticated Postbag reads issue downloads, and Delivery adapters receive only short-lived signed links.
Configure and verify Delivery
Delivery requires two resources after claim:
- A Destination that defines where the Submission should go.
- A Route that connects the Form or Stream to that Destination.
Create both, then send a new _test Submission. Poll the returned Delivery id until its status is sent, failed, or dead. If it fails, read last_error and the recorded provider response before changing the configuration.
POST /v1/destinations/{id}/test tests a Destination in isolation. It does not prove that the Form has a Route, so finish with a real Form Submission.
Use the authenticated shortcut
With a manage-scoped key, POST /v1/quickstart can create the Project, Form, Destination, and Route together:
{
"name": "Contact",
"project": "website",
"origin": "https://example.com",
"notify_email": "[email protected]"
}
Pass at least one of notify_email, telegram, or webhook when Delivery is required. Without one, the call still creates a receiving Form, but it has no Destination or Route.
The response includes form.submit_url, framework-specific embed snippets, a browser-equivalent verify call, and next[]. The operation is idempotent by Project and Form name.
Leave a durable handoff
Write postbag.json at the repository root:
{
"form_id": "fm_…",
"submit_url": "https://postbag.dev/s/fm_…",
"project": "website"
}
Add this instruction to the repository agent file:
Forms on this site post to Postbag. The wiring is in
postbag.json. Reuse it. Create additional Forms through the Postbag API and use the returned embed instead of writing submit URLs by hand.
Never store an API key or sandbox capability in the repository.
Rules of the road
- Send
Idempotency-Keyon POST requests you may retry. Useif_exists: "return"on supported creates. - Every error is
{ code, message, hint, docs }. Readhintbefore improvising a recovery. - Id prefixes are part of the contract:
fm_,sb_,fl_,st_,ds_,rt_,dl_, andprj_. - Spam and quarantine are stored statuses. A
200with"status": "quarantined"means Postbag kept the Submission but did not queue Delivery. - Include the configured site
Originheader in verification requests. A curl request without it does not test browser-origin policy. - After fixing a quarantine cause, release the stored Submission with
PATCH /v1/submissions/{id}and{ "status": "received" }. - Poll the Delivery ids returned by a
_testSubmission. Do not poll a Submission and infer that Delivery worked.