Create the Form before you have an account
No signup first — one command returns a real Form id and submit URL, valid straight into production, inside a 24-hour sandbox that stores five test messages and sends nothing. The token it prints appears once; put it somewhere safe.
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"
Write the single-file component
This is Vue at its most pleasant: v-model does the bookkeeping, @submit.prevent spares you the event plumbing, and the whole thing fits in one honest file. While a send is in flight the button says so; success swaps the form for a thank-you; failure says so and keeps the visitor's words in the boxes.
<script setup>
import { ref } from "vue";
const email = ref("");
const message = ref("");
const status = ref("idle");
async function handleSubmit() {
status.value = "sending";
try {
const res = await fetch("YOUR_POSTBAG_SUBMIT_URL", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: email.value, message: message.value }),
});
status.value = res.ok ? "sent" : "error";
} catch {
status.value = "error";
}
}
</script>
<template>
<p v-if="status === 'sent'" role="status">Thanks! Your message has been sent.</p>
<form v-else @submit.prevent="handleSubmit">
<label>
Email
<input type="email" v-model="email" required />
</label>
<label>
Message
<textarea v-model="message" required></textarea>
</label>
<button type="submit" :disabled="status === 'sending'">
{{ status === 'sending' ? 'Sending…' : 'Send' }}
</button>
<p v-if="status === 'error'" role="status">Something went wrong. Please try again.</p>
</form>
</template> <script setup>
import { ref } from "vue";
const email = ref("");
const message = ref("");
const status = ref("idle");
async function handleSubmit() {
status.value = "sending";
try {
const res = await fetch("YOUR_POSTBAG_SUBMIT_URL", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: email.value, message: message.value }),
});
status.value = res.ok ? "sent" : "error";
} catch {
status.value = "error";
}
}
</script>
<template>
<p v-if="status === 'sent'" role="status">Thanks! Your message has been sent.</p>
<form v-else @submit.prevent="handleSubmit">
<label>
Email
<input type="email" v-model="email" required />
</label>
<label>
Message
<textarea v-model="message" required></textarea>
</label>
<button type="submit" :disabled="status === 'sending'">
{{ status === 'sending' ? 'Sending…' : 'Send' }}
</button>
<p v-if="status === 'error'" role="status">Something went wrong. Please try again.</p>
</form>
</template> Send one and watch it land
Mount it, send yourself a line, or test from the terminal. Then ask the sandbox what it holds — your message is there, stored and timestamped, before anything else happened to it.
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 the claim, connect your inbox and route the Form to it. From then on, saved first, sent second — an outage on the receiving end delays a message rather than losing it.
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"