Skip to content

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.

Updated Read as Markdown
On this page
  1. Orient before changing anything
  2. Create a sandbox without credentials
  3. Wire and prove receipt
  4. Authenticate and claim
  5. Submit an attachment after claim
  6. Configure and verify Delivery
  7. Use the authenticated shortcut
  8. Leave a durable handoff
  9. Rules of the road

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:

  1. A Destination that defines where the Submission should go.
  2. 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-Key on POST requests you may retry. Use if_exists: "return" on supported creates.
  • Every error is { code, message, hint, docs }. Read hint before improvising a recovery.
  • Id prefixes are part of the contract: fm_, sb_, fl_, st_, ds_, rt_, dl_, and prj_.
  • Spam and quarantine are stored statuses. A 200 with "status": "quarantined" means Postbag kept the Submission but did not queue Delivery.
  • Include the configured site Origin header 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 _test Submission. Do not poll a Submission and infer that Delivery worked.

Give the form job to your agent.

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