Quickstart
From signup to your first delivered submission in under 5 minutes.
1. Sign up and create a form
Sign in with a magic link. From the dashboard, click New form, give it a name like contact, and copy the endpoint URL.
2. Drop the endpoint into your HTML
Replace YOUR-FORM-ID with the ID from the dashboard. Any field names you use here will appear in the destination delivery.
<form
action="https://routepost.dev/api/f/YOUR-FORM-ID"
method="POST"
>
<input name="name" placeholder="Your name" required />
<input name="email" placeholder="you@example.com" type="email" required />
<textarea name="message" placeholder="What's up?" required></textarea>
<!-- Honeypot — must stay empty. Bots fill it; humans don't. -->
<input type="text" name="website" style="display:none" tabindex="-1" autocomplete="off" />
<button type="submit">Send</button>
</form>3. Add a destination
Open your form in the dashboard, scroll to Destinations, and add at least one. Email + Slack + webhook can all coexist — every destination receives every submission in parallel.
4. Test it
Send a request from the terminal to verify everything before publishing the form:
curl -X POST https://routepost.dev/api/f/YOUR-FORM-ID \
-d "name=Carlos&email=carlos@example.com&message=Hello"You should get {"success":true,"delivered":1,"failed":0} and the submission appears in the dashboard within a second.
5. (Optional) Redirect after submit
Set a redirect_url on the form to send users to a thank-you page after submission. Without it, the API returns JSON.
Destinations
Where each submission gets sent. Three types are supported; add as many of each as you need.
Each submission delivered as a formatted email. Best for low-volume forms — contact, support, sales leads.
What you see in the dashboard:
No JSON, no API — just a dropdown for the type and a text field for the address.
Sample email body the recipient sees:
New submission: contact
| name | Jane Doe |
| jane@acme.com | |
| message | Tell me more about Pro |
Slack
Post each submission to a Slack channel via incoming webhook. Best for team notifications and high-touch workflows.
First, get a Slack webhook URL:
- In Slack: your-workspace → Apps → search Incoming Webhooks → Add to Slack
- Pick a channel, click Add Incoming Webhooks integration
- Copy the Webhook URL (starts with https://hooks.slack.com/services/…)
Then paste it in the dashboard:
What you see in the dashboard:
No JSON, no API — just a dropdown for the type and a text field for the address.
Custom webhook
POSTs the submission as JSON to any URL. Use for Zapier, Make.com, your own backend, or anywhere a webhook can land.
What you see in the dashboard:
No JSON, no API — just a dropdown for the type and a text field for the address.
Payload your endpoint will receive:
{
"form": "contact",
"received_at": "2026-06-18T18:00:00.000Z",
"data": {
"name": "Jane Doe",
"email": "jane@acme.com",
"message": "Tell me more about Pro"
}
}Your endpoint should respond with a 2xx within a reasonable timeout. Non-2xx responses are recorded as failures in the submission delivery report.
Delivery modes
Different destinations deliver on different schedules. Pick the right destination type for the latency you need.
Real-time — Slack and webhook
Slack and webhook destinations fire immediately on every submission, on every plan. The submission endpoint posts to them inline; you'll see the message in Slack (or the request hit your webhook) within a couple of seconds of the form submit.
Use these when you need low-latency notification or when you're feeding submissions into a downstream system (Inngest, Zapier, n8n, EventBridge, your own queue).
Digest — email
Email destinations are always digest-mode. Submissions accumulate, and the digest email goes out when either cadence clause fires — whichever happens first:
- Submissions cadence — N new submissions since the last digest.
- Time cadence — M minutes since the last digest.
Each digest is capped at 100 submissions per email; backlogs roll into the next run. The cron evaluates due destinations every 5 minutes.
Per-plan cadence floors
Cadence values are configurable per destination, but no plan can go below its floor. These floors exist so email stays a summary medium — for real-time delivery use a Slack or webhook destination.
| Plan | Submissions floor | Time floor |
|---|---|---|
| Free | every 50 submissions | daily (1440 min) |
| Starter | every 25 submissions | hourly (60 min) |
| Pro | every 10 submissions | every 10 min |
Need real-time? Add a Slack or webhook destination — they fire on every submission, no cadence floor applies.
Destination verification
Why RoutePost requires you to confirm a destination before submissions start flowing — and what happens if you don't.
Without verification, anyone could sign up for RoutePost, point a destination at someone-elses-address@example.com, and use the public form endpoint to spam them under our domain. To shut that vector down, every destination has to prove it belongs to the form-owner before it can deliver.
By destination type
- Email. Adding an email destination sends a one-click verification link to that address. Submissions to the form are rejected until the recipient clicks Confirm destination. The link expires 24 hours after sending. You can resend from the dashboard up to 5× per 24h, no more than once per minute.
- Slack. Adding a Slack webhook fires a test message to the channel naming the RoutePost account that connected it, plus a link the channel members can click to detach. The destination is treated as verified on add (Slack webhook URLs are workspace-controlled secrets — you couldn't have one unless someone in your workspace created it).
- Webhook. Same model as Slack: adding the destination POSTs a one-time payload of type routepost.verification to the URL with an owner_email and a disconnect_url. The endpoint can either ignore it or surface the disconnect link to a human.
What submitters see when a form has no verified destinations
If every destination on the form is pending verification or disabled by its recipient, the public endpoint rejects the submission outright with HTTP 422:
{
"error": "no_verified_destinations",
"message": "This form has no verified destinations. The form owner must complete destination verification before submissions are accepted."
}The submission is not stored — it isn't queued for later either. Your form's frontend should surface the message so the form-filler doesn't assume their data was accepted.
Editing a destination after the fact
- Editing only the label or routing rules keeps the destination verified.
- Editing an email destination to point at a different address resets verification and sends a fresh confirmation link to the new address. As a courtesy, the old address gets a notification that the destination was moved.
- Editing a Slack or webhook URL re-fires the test-ping with a fresh disconnect link. The destination stays in the verified state (same verified-on-add reasoning as creation).
If a recipient disconnects
When someone clicks the disconnect link in a Slack/webhook test-ping (or in the post-confirmation page for an email destination), the destination is marked Disabled by recipient in the dashboard and stops receiving submissions immediately. The form-owner can delete it and create a new one if needed. Disconnect links remain valid for 30 days from when they were issued.
Routing rules
Conditionally deliver a submission to a destination only when fields in the submission match a rule. Useful for routing high-value leads to sales-Slack and everything else to a support email.
On every destination you can add one or more rules. All rules must match (logical AND) for that destination to receive the submission. Empty rules = unconditional (the default).
Example: route enterprise leads to sales-Slack
Suppose your HTML form has a dropdown field interest with values starter, team, and enterprise. You can:
- Add a slack destination pointing at #sales, with a rule:
interest equals enterprise - Add a second email destination pointing at support@yourdomain.com, with no rules (everything else lands there)
Now every form submission is automatically routed — sales gets enterprise leads in Slack instantly; support handles the rest by email. No Zapier in the middle.
Supported operators
| Operator | Matches when… |
|---|---|
| equals | field value matches the configured string exactly |
| not_equals | field value is NOT the configured string |
| contains | field value contains the configured substring |
| not_contains | field value does not contain the configured substring |
| starts_with | field value starts with the configured string |
| is_empty | field is missing or empty |
| is_not_empty | field exists with any non-empty value |
| regex | field value matches the configured regular expression |
Rules evaluate on the server immediately after the submission lands. If a destination's rules don't match, that destination is simply skipped (no delivery, no error). The submission still appears in your dashboard inbox.
Autoresponder
Send an automatic thank-you reply to the submitter as soon as their form lands. Off by default — toggle per form.
Enable it on a form
Open the form in your dashboard → Form settings → toggle Auto-reply to submitter. Configure which field holds the email, the subject, body, and optional Reply-To.
Template variables
Use {{ field_name }}in either subject or body — it's replaced with the value of that submission field. {{ form_name }}substitutes the form's display name.
Subject: Hi {{ name }}, we got your message
Body:
Hi {{ name }},
Thanks for reaching out via {{ form_name }}. We received your message:
"{{ message }}"
Someone on our team will reply within 24 hours.
— The {{ form_name }} teamDeliverability — read this
- Auto-replies go out via your configured Resend account. The
Fromaddress is whatever you set asRESEND_FROM_EMAILon the server. - For replies to land in real inboxes (not spam), the sender domain must be verified in Resend — same as transactional email.
- Set a Reply-Toto a real address you monitor; otherwise submitter replies go to the auto-reply's From address (often a noreply mailbox).
- Honeypot-flagged submissions never trigger the autoresponder.
If the autoresponder skips silently
A few reasons it might not fire:
- The configured field is missing or empty in the submission
- The recipient value isn't a valid email address
- Resend isn't configured on the server (missing API key)
- Your plan isn't in
FEATURE_AUTORESPONDER_PLANS - The instance has
FEATURE_AUTORESPONDER=false
File uploads
Accept file attachments alongside form fields. Files are stored, URLs flow through to every destination.
File uploads are off by default. To enable them on a form, open it in the dashboard → Form settings → toggle Accept file uploads.
HTML form with file inputs
<form
action="https://routepost.dev/api/f/YOUR-FORM-ID"
method="POST"
enctype="multipart/form-data"
>
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<!-- Single file -->
<input name="attachment" type="file" />
<!-- Multiple files -->
<input name="screenshots" type="file" multiple />
<button type="submit">Send</button>
</form>Critical: include enctype="multipart/form-data" on the <form> tag. Without it, browsers send only the filenames, not the actual bytes.
Limits
- Max 25 MB per file
- Max 50 MB total per submission
- Any file type is accepted (no MIME filter at the server)
- Files are stored in Supabase Storage with public URLs
- If file uploads are disabled on the form, files are silently dropped (fields still process)
What destinations see
Each destination receives URLs to the uploaded files alongside the form data.
Email destination: file links appear below the data table in the email body.
Slack destination: an Attachments section with clickable links is appended to the message.
Webhook payload now includes:
{
"form": "contact",
"received_at": "2026-06-18T18:00:00.000Z",
"data": { "email": "jane@acme.com", "message": "..." },
"attachments": [
{
"name": "screenshot.png",
"size": 12384,
"type": "image/png",
"url": "https://nansd…supabase.co/storage/v1/object/public/submission-files/…"
}
]
}Dashboard
Where you manage forms, destinations, and inspect submissions.
Forms list
Landing screen after signing in. One card per form, with a quick submission count.
Contact form
42 submissionsNewsletter signup
318 submissionsBug report
11 submissionsPartner inquiry
3 submissionsForm detail
Endpoint URL (with copy button), destinations management, and recent submissions — all on one page.
Endpoint
https://routepost.dev/api/f/abc-123Destinations
- emailteam@acme.comRemove
- slackhooks.slack.com/…Remove
Recent submissions
Last 100 submissions per form, with IP and timestamp. Click a row to see the full payload.
| Received | Data |
|---|---|
| 2026-06-18 12:34 | name: Jane · email: jane@acme.com · plan: pro |
| 2026-06-18 12:30 | name: Bob · email: bob@example.com |
| 2026-06-18 12:18 | name: Carlos · ref: launch-week |
| 2026-06-18 11:55 | email: alice@beta.io · question: pricing |
Spam protection
A small amount goes a long way. Bots are dumb, your honeypot is free.
Every form has a hidden honeypot_field (defaults to website). Bots fill every input they find; humans don't see hidden fields. When the honeypot arrives non-empty, RoutePost returns 200 and silently discards the submission — no notification, no inbox row, no quota hit.
<form action="https://routepost.dev/api/f/YOUR-FORM-ID" method="POST">
<input name="email" type="email" required />
<!-- HONEYPOT — keep empty -->
<input type="text" name="website"
style="display:none" tabindex="-1" autocomplete="off" />
<button>Send</button>
</form>You can rename the honeypot field per form (e.g. company or phone2) so bots that learn one site's convention still miss yours.
CORS
The submission endpoint accepts cross-origin requests from any domain — no whitelist required.
All POST /api/f/[form_id] responses include Access-Control-Allow-Origin: *, so a form hosted anywhere — your marketing site, a static SPA, a customer's portal — can submit without any proxy or server-side relay.
If you're submitting via fetch() from JavaScript, the response is JSON:
const res = await fetch(
'https://routepost.dev/api/f/YOUR-FORM-ID',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, message }),
}
);
const { success, delivered, failed } = await res.json();