txtbag form
Collect form submissions without maintaining your own backend.
txtbag form gives every form a Formspree + webhook delivery + submission inbox + viewer sharing + export pipeline + API-first security model.
Limited-time launch offer(Jul 16 - Aug 22, 2026): use code VERYRICH5000 at checkout to get non-expiring 5,000 submission credits free while the quota is available.
One endpoint for every form workflow
Every product needs forms: contact, waitlists, feedback, support, internal ops. The hard part is everything after submit: delivery, privacy, notifications, webhooks, sharing, and exports.
txtbag form brings proper submission workflow.
- Endpoint for each form.
- API-key-required mode for trusted backend submissions.
- Optional browser embeds for no-backend forms.
- Spam-resistant browser submission flow.
- Email notifications with burst suppression.
- Signed outgoing webhooks.
- Read-only viewer links with optional passwords.
- XLSX-compatible exports.
How it works
- Create a form endpoint.
- Choose the submission mode.
- Send submissions from your backend, worker, static site, or browser embed.
New forms start in secure server-side mode:
Require API key: on
Use this mode for Next.js route handlers, Flask apps, PHP scripts, Node services, Vercel Serverless Functions, internal tools, and automations.
If you want a pure browser form with no backend, turn off Require API key in form Preferences and use the provided HTML or JavaScript snippets.
Why teams use it
Launch forms without owning the backend
Use a hosted endpoint instead of maintaining custom capture, notification, webhook, and export workflows.
Protect backend submissions by default
Account API keys are designed for trusted server-side code only. Use them for route handlers, workers, automations, and internal tools that need reliable submission access without depending on a browser.
Browser embeds when needed
For marketing sites, static pages, Webflow, WordPress, or simple landing pages, disable Require API key and paste the browser snippet.
Monitoring and observability
Submissions are stored, paginated, exportable, and visible from the dashboard. Delivery records help you see whether notifications and webhooks were sent.
Best effort anti-spam and security set
API-key locking, browser anti-spam checks, rate limits, payload limits, webhook safety checks, and formula-safe XLSX exports are built in.
Features
| Feature | Included |
|---|---|
| Hosted form endpoints | Yes |
| API-key-required mode | Yes |
| Server-side API keys | Yes |
| Browser HTML form support | Yes |
| JavaScript SDK | Yes |
| Authorized browser domains | Yes |
| Spam honeypot and timestamp checks | Yes |
| Email notifications | Yes |
| Burst notification suppression | Yes |
| Signed outgoing webhooks | Yes |
| Webhook delivery tracking | Yes |
| Manual webhook resend | Yes |
| Read-only viewer links | Yes |
| Optional viewer password | Yes |
| XLSX export | Yes |
| Formula-safe spreadsheet cells | Yes |
Common use cases
Contact forms
Collect contact requests from any website. Use the default API-key lock from your backend, or disable it when you want a browser-only embed.
Landing pages
Capture product interest, early access requests, demo requests, and campaign leads without creating a new backend service.
Waitlists
Collect signups over time, review them in the dashboard, and export them when you are ready.
Support requests
Forward submissions to a helpdesk, CRM, Slack bridge, or automation platform through signed webhooks.
Internal tools
Let internal apps submit operational records into a central inbox without provisioning a full data capture service.
Backend automations
Use API keys from Next.js route handlers, Node services, Rails apps, cron jobs, or Cloudflare Workers.
Browser HTML form example
Browser-only HTML forms work when Require API key is disabled for the form. Never place API keys in browser HTML or JavaScript.
Use form-embed.js for the simplest browser integration. It adds bot-check fields, refreshes _ts, captures common tracking parameters, adds an idempotency key, and exposes a small browser API for dynamic apps.
New snippets use the shorter /f/... endpoint. Older /public/f/... endpoints remain available as legacy aliases.
<form action="https://form.txtbag.com/f/form_xxxxxxxxx" method="post" data-formbag>
<label>
Email
<input type="email" name="email" required>
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<button type="submit">Send</button>
</form>
<script src="https://form.txtbag.com/form-embed.js" defer></script>
Native browser submissions redirect after success. If your form has no custom redirect URL, the browser lands on https://txtbag.com/form/success.
You can also use data-formbag-id and let the helper set the endpoint:
<form data-formbag data-formbag-id="form_xxxxxxxxx">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
<script src="https://form.txtbag.com/form-embed.js" defer></script>
Dynamic apps and AJAX forms
form-embed.js exposes a small browser API for forms rendered after page load:
window.formbag.scan(root = document)
window.formbag.prepare(form)
window.formbag.submit(form)
Use scan(root) after rendering a form dynamically, such as after a React route transition, markdown preview render, modal open, or HTMX/Turbo replacement:
useEffect(() => {
window.formbag?.scan(containerRef.current)
}, [html])
For no-code AJAX, add data-formbag-ajax:
<form action="https://form.txtbag.com/f/form_xxxxxxxxx" method="post" data-formbag data-formbag-ajax>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
<script src="https://form.txtbag.com/form-embed.js" defer></script>
AJAX submissions stay on the current page. The helper prepares the form with a fresh _ts and idempotency_key, appends ?format=json, sends FormData with Accept: application/json, and dispatches formbag:success or formbag:error.
For custom UI, call submit(form) manually:
const form = document.querySelector("form")
try {
const result = await window.formbag.submit(form)
form.reset()
console.log("sent", result)
} catch (error) {
console.error("failed", error)
}
Browser JavaScript SDK example
Browser-side SDK submissions work when Require API key is disabled for the form.
<script src="https://form.txtbag.com/formbag.js"></script>
<script>
const formbag = new Formbag({
formId: "form_xxxxxxxxx"
})
const result = await formbag.submit({
email: "[email protected]",
message: "Hello from my site"
})
if (!result.success) {
console.error(result.message)
}
</script>
The browser SDK automatically adds:
- Honeypot field.
- Timestamp field.
- Idempotency key.
- Common tracking parameters.
Server-side API example
Use account API keys only from trusted server-side code. This is the default mode for newly created forms.
Fetch, XHR, and SDK submissions receive JSON. Add ?format=json when you want to explicitly opt out of browser redirects.
await fetch("https://form.txtbag.com/f/form_xxxxxxxxx?format=json", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.FORMBAG_API_KEY}`
},
body: JSON.stringify({
email: "[email protected]",
message: "Hello from server-side code"
})
})
Valid API-key submissions can be sent directly from your backend without browser-only anti-spam fields.
You can also submit form-encoded data:
curl -i -X POST "https://form.txtbag.com/f/form_xxxxxxxxx?format=json" \
-H "Authorization: Bearer $FORMBAG_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "[email protected]" \
-d "message=Hello from curl"
File attachments
txtbag form stores text fields only. For files, upload to your own storage first, then submit the resulting file URL.
For browser forms, disable Require API key and submit the URL field from the browser. For locked forms, perform both the upload and txtbag form submission from trusted backend code.
Example submitted field:
image_url=https://cdn.example.com/uploads/product-photo.jpg
Your backend can create upload URLs for your storage provider, then send the final file URL to txtbag form.
Example browser-side upload flow:
<script src="https://form.txtbag.com/formbag.js"></script>
<script>
async function uploadFileWithFetch(file) {
const presignResponse = await fetch("/presign-upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
content_type: file.type
})
})
const presign = await presignResponse.json()
await fetch(presign.upload_url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file
})
return presign.file_url
}
const selectedFile = document.querySelector("input[type='file']")?.files[0]
const imageUrl = selectedFile ? await uploadFileWithFetch(selectedFile) : null
const formbag = new Formbag({
formId: "form_xxxxxxxxx"
})
await formbag.submit({
email: "[email protected]",
message: "Here is the image URL",
image_url: imageUrl
})
</script>
Webhooks
Send every accepted submission to your own endpoint.
Webhook requests include signed headers:
X-TxtbagForm-Event
X-TxtbagForm-Delivery
X-TxtbagForm-Timestamp
X-TxtbagForm-Signature
Example payload:
{
"event": "submission.created",
"form": {
"id": "form_xxxxxxxxx",
"name": "Contact"
},
"submission": {
"id": "sub_xxxxxxxxx",
"created_at": "2026-07-09T12:00:00Z",
"spam": false
},
"fields": {
"email": "[email protected]",
"message": "Hello"
}
}
Webhook deliveries are queued, retried, and tracked. You can manually resend a webhook from the submission detail page.
Viewer links
Share a read-only view of submissions without giving dashboard access.
Viewer links can be:
- Enabled or disabled.
- Rotated.
- Protected with a password.
- Invalidated by changing the password.
- Logged out manually from the viewer page.
- Used for external monitoring.
Password-protected viewer sessions expire automatically after 12 hours.
Exports
Download submissions as .xlsx.
Exports include:
- Submission ID.
- Submitted timestamp in UTC.
- Submitted local timestamp.
- Submitted timezone.
- Submitted fields.
- Formula-injection-safe cell values.
Exports are generated asynchronously and downloaded through authenticated, short-lived links.
Stored submissions are retained only for 90 days so ensure you export them in a timely manner because we don't store them indefinitely. Export files expire after 24 hours.
Pricing
| Pack | Best for | Included | Price |
|---|---|---|---|
| Free | Small forms and early projects | 500 free submissions per month, dashboard, viewer links, webhooks, exports | Free |
| 5,000 credits | Growing sites | One-time/ overage submission credits used after free quota | $5 |
| 10,000 credits | Higher-volume forms | One-time/ overage submission credits used after free quota | $10 |
*Contact us for usage beyond 100,000 credits.
No subscription is required.
Referral credits are promotional, non-transferable, and have no cash value.
Security and privacy
- Public form IDs are random and prefixed.
- New forms require API keys by default.
- Submitted payloads are encrypted at rest.
- Stored submissions are retained for 90 days.
- Export files expire after 24 hours.
- Logs and error reporting scrub submitted fields and sensitive details.
- API keys, viewer passwords, and webhook secrets are handled as sensitive credentials.
- Webhook destinations are checked before delivery.
- Export downloads use short-lived signed links.
- Public pages and private dashboard pages are
noindex.
Start accepting submissions
Create a form endpoint, choose how submissions arrive, and start collecting data without maintaining your own form backend.
Invite a teammate or friend and both accounts get 200 submission credits after the referred account qualifies. Referral credits are promotional, non-transferable, subject to abuse checks, and have no cash value.