Create the Form before you have an account
Start with one command and no signup. You get a real Form — an id and a submit URL that will survive into production — plus 24 hours and five test messages to prove it works. Keep the token it prints; it appears once.
sandbox_json="$(npx postbag --json sandbox create \
--name "Contact" \
--origin "https://example.com")"
submit_url="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox.submit_url')"
form_id="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox.id')"
sandbox_token="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox_token')"
claim_url="$(printf '%s\n' "$sandbox_json" | jq -er '.claim_url')"
printf 'Submit URL: %s\nForm ID: %s\nClaim URL: %s\nSandbox token: %s\nReplace YOUR_POSTBAG_SUBMIT_URL in the next snippet with the Submit URL above.\n' \
"$submit_url" "$form_id" "$claim_url" "$sandbox_token" sandbox_json="$(npx postbag --json sandbox create \ --name "Contact" \ --origin "https://example.com")" submit_url="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox.submit_url')" form_id="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox.id')" sandbox_token="$(printf '%s\n' "$sandbox_json" | jq -er '.sandbox_token')" claim_url="$(printf '%s\n' "$sandbox_json" | jq -er '.claim_url')" printf 'Submit URL: %s\nForm ID: %s\nClaim URL: %s\nSandbox token: %s\nReplace YOUR_POSTBAG_SUBMIT_URL in the next snippet with the Submit URL above.\n' \ "$submit_url" "$form_id" "$claim_url" "$sandbox_token"
Make it a component
No frontmatter, no client directive, no island. Because there's nothing to hydrate, Astro renders this to pure HTML and ships not one byte of JavaScript for it — a form that would make a Lighthouse audit smile.
Drop it into any page with an import and a tag. It keeps working if you later turn on view transitions, because a native form post doesn't care about client routing.
<form action="YOUR_POSTBAG_SUBMIT_URL" method="POST">
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-10000px" aria-hidden="true" />
<button type="submit">Send</button>
</form> <form action="YOUR_POSTBAG_SUBMIT_URL" method="POST">
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-10000px" aria-hidden="true" />
<button type="submit">Send</button>
</form> ---
import ContactForm from "../components/ContactForm.astro"
---
<ContactForm /> --- import ContactForm from "../components/ContactForm.astro" --- <ContactForm />
Send one and watch it land
Run the dev server, fill the form in, press Send. Or test from the terminal — either way, ask the sandbox what it holds and you'll find your message stored, timestamped, waiting.
curl --fail --silent --show-error -X POST "$submit_url" \
-H "content-type: application/json" \
-d '{ "email": "[email protected]", "message": "hello from the terminal" }' curl --fail --silent --show-error -X POST "$submit_url" \
-H "content-type: application/json" \
-d '{ "email": "[email protected]", "message": "hello from the terminal" }' POSTBAG_SANDBOX_TOKEN="$sandbox_token" npx postbag sandbox status POSTBAG_SANDBOX_TOKEN="$sandbox_token" npx postbag sandbox status
Claim it when you're ready
The creation response included a claim link. Open it, sign in — Google, GitHub, or an emailed code — and the sandbox becomes a real Form in your own workspace. Same id, same submit URL: the page you just wired needs no edit. Your test messages come along, still marked as tests.
Turn on email
After claiming, connect your inbox and route the Form to it. Every new message is saved first, then sent — this site you're reading is an Astro site, its contact form is a Postbag form, and this is exactly how it's wired. Of course it is.
destination_json="$(curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d '{ "type": "email", "config": { "to": ["[email protected]"] } }')"
destination_id="$(printf '%s\n' "$destination_json" | jq -er '.id')"
route_body="$(jq -n --arg form_id "$form_id" --arg destination_id "$destination_id" \
'{ form_id: $form_id, destination_id: $destination_id }')"
curl --fail --silent --show-error -X POST https://postbag.dev/v1/routes \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d "$route_body" destination_json="$(curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d '{ "type": "email", "config": { "to": ["[email protected]"] } }')"
destination_id="$(printf '%s\n' "$destination_json" | jq -er '.id')"
route_body="$(jq -n --arg form_id "$form_id" --arg destination_id "$destination_id" \
'{ form_id: $form_id, destination_id: $destination_id }')"
curl --fail --silent --show-error -X POST https://postbag.dev/v1/routes \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d "$route_body"