Docs
Quick start
Point a form at your endpoint:
POST https://api.submissionbuddy.io/f/{form_slug}
No API key, no SDK, no schema to register first. Send fields, they land in your dashboard.
Plain HTML:
<form action="https://api.submissionbuddy.io/f/your-form" method="POST">
<input name="name" type="text" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
JSON via fetch:
await fetch('https://api.submissionbuddy.io/f/your-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Ada', email: 'ada@example.com', message: 'Hi' }),
})
Both work against the same endpoint — pick whichever fits your form.
Using an AI assistant?
SubmissionBuddy publishes /llms.txt — a machine-readable summary of the endpoint, control fields, and limits that agents and AI coding tools can read directly.
If you're using an AI coding tool (Cursor, Copilot, Claude, etc.), you can hand it this prompt as-is:
Add a contact form to my site posting to my SubmissionBuddy endpoint
https://api.submissionbuddy.io/f/YOUR-FORM— plain HTML with a_honeypotfield and a_redirectto/thanks/.
Swap YOUR-FORM for your form's slug and it should produce working markup.
Endpoint reference
POST /f/{form_slug}
Accepted content types
application/jsonapplication/x-www-form-urlencodedmultipart/form-data— text fields only. File uploads are not supported today; a file input in a multipart form is silently skipped, not stored.
Any other content type, an empty body, or a multipart request with no boundary returns 400.
Limits
| Limit | Value |
|---|---|
| Raw request body | 1 MB |
| Fields per submission | 100 |
| Characters per field value | 10,000 |
There is no field schema beyond these limits — send whatever fields your form has, add or remove fields without reconfiguring anything.
Success responses
202 Accepted— the default. Body:{ "ok": true, "submission_id": "9be2c1a4-..." }303 See Other— returned instead of202when the request includes a_redirectfield. The response has aLocationheader pointing at your redirect URL and no body.
Error responses
All error bodies look like {"ok": false, "error": "<message>"}.
| Status | When it fires |
|---|---|
| 400 | Body exceeds 1 MB |
| 400 | Unsupported or missing content type, empty body, or missing multipart boundary |
| 400 | Malformed request body (bad base64 or invalid JSON) |
| 404 | Form slug doesn't exist |
| 422 | Payload fails validation — too large after parsing, too many fields, or a field value too long |
| 422 | CAPTCHA is in Enforce mode and the token is missing — "CAPTCHA token required." |
| 422 | CAPTCHA is in Enforce mode and the token is invalid — "CAPTCHA validation failed." |
| 429 | Per-form rate limit exceeded — "Rate limit exceeded." |
| 429 | Global per-IP rate limit exceeded |
| 429 | Monthly plan quota exceeded — "Monthly submission limit reached for this form." |
A mismatched request origin never produces an error — it only feeds the risk score (see Spam protection below).
Control fields
These field names are reserved. If present in your request, they're read as instructions and stripped out before the submission is stored — they never show up in your dashboard data.
| Field | Purpose |
|---|---|
_redirect |
Send a browser here after a successful submission instead of returning JSON. Triggers a 303 with a Location header. Works even when a honeypot silently fakes success. |
_honeypot |
Your form's honeypot field, named whatever your form is configured to use. See Spam protection. |
_captcha_token (or altcha, the widget's default field name) |
The CAPTCHA proof-of-work token, if CAPTCHA is enabled on the form. |
Rate limits
Two limits apply, both on a fixed 1-minute window:
- Per form, per IP — 10 requests/minute by default. A form's rate limit can be overridden per-form.
- Global, per IP — 60 requests/minute across all forms.
Exceeding either returns:
{ "ok": false, "error": "Rate limit exceeded." }
with status 429.
Spam protection
Honeypot. Add a hidden field to your form (the field name is configurable per form). Real visitors never fill it in; bots that auto-fill every field do. If it's filled, the submission is discarded — nothing is written to storage, nothing is queued — but the response looks like a normal success (202 with a submission ID, or a 303 redirect if _redirect is set). The bot has no way to tell it failed.
CAPTCHA. SubmissionBuddy uses a self-hosted, invisible proof-of-work CAPTCHA (ALTCHA) — not a third-party service. The browser solves a small cryptographic puzzle and submits a token; verification happens entirely on our servers with no calls out to a third party and no cookies set. It has three modes, set per form:
| Mode | Behavior |
|---|---|
| Off | No CAPTCHA logic runs. |
| Monitor | Token is verified if present and recorded (verified / invalid / missing), but never blocks a submission. A failed or missing token adds a risk-score flag. |
| Enforce | A missing or invalid token is rejected with 422. |
CAPTCHA is available on Pro and Business plans.
Risk scoring. Every submission gets an informational risk score, visible in the dashboard, built from:
- Honeypot triggered
- CAPTCHA failure
- Request origin not matching the form's configured domain
- Rate-limit pressure (usage near the limit)
The score never blocks a submission by itself — it's a signal for you to triage borderline cases, not an enforcement mechanism.
What happens to a submission
- Your request is validated and passes spam checks.
- The raw payload is written to durable storage before you get a response. A
202(or303) is only returned after this write succeeds. - A message referencing that stored payload — not the payload itself — is placed on a processing queue.
- A worker picks up the message and writes the submission into the database that backs your dashboard. Processing is idempotent: a redelivered message doesn't create a duplicate record.
- If a stage fails, it retries automatically rather than dropping the submission.
- A notification email is sent to the form's configured recipients (if any). On the Business plan, an auto-response to the submitter can also be sent.
- The processed record in your dashboard is kept indefinitely — it doesn't expire on its own. The raw payload copy in object storage is retained for 30 days, then deleted.
Plans & quotas
| Plan | Price | Submissions/mo | Forms | CAPTCHA | Auto-response | Team |
|---|---|---|---|---|---|---|
| Free | $0 | 100 | 2 | no | no | no |
| Pro | $8/mo | 1,000 | unlimited | yes | no | no |
| Business | $24/mo | 20,000 | unlimited | yes | yes | yes |
Going over your monthly submission quota returns 429 with "Monthly submission limit reached for this form." at the ingestion endpoint. Dashboard actions gated by plan (adding a form past your cap, inviting a team member, enabling auto-response or CAPTCHA) return 402 if your plan doesn't include them.
Dashboard
- Search — case-insensitive substring search across submission field values.
- Filter — by date range.
- Sort — by received date, status, or any payload field.
- Export — download submissions as an Excel (
.xlsx) file, up to 50,000 rows per export. - Team (Business plan) — invite project members with a role of owner, editor, or read-only. The invitee needs an existing SubmissionBuddy account.
CORS
The ingestion endpoint accepts cross-origin POST requests from any origin — there's no allowlist to configure. Browser preflight (OPTIONS) requests are answered automatically, so a plain fetch() from any site works without extra setup.
Ready to try it? Start free — 100 submissions a month, no credit card.