Docs

Point any form at your Sendform endpoint URL. No JavaScript required, no backend of your own. Replace YOUR_ENDPOINT_ID below with the URL shown on your dashboard.

Plain HTML

The whole product is one form action attribute.

<form action="https://sendform.app/api/public/f/YOUR_ENDPOINT_ID" method="POST">
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>

  <!-- honeypot: keep it hidden, bots fill it in -->
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" />

  <!-- optional: where the browser lands after a successful post -->
  <input type="hidden" name="_redirect" value="https://yoursite.com/thanks" />

  <button type="submit">Send</button>
</form>

Browser posts get a thank-you page, or a 302 to your _redirect URL (or the redirect set in dashboard settings). Requests sending accept: application/json always get JSON back.

Carrd, Framer and Webflow

  1. Select your form element, open the settings panel and choose Type: External.
  2. Paste your endpoint URL into the Action field and set Method to POST.
  3. Name each field exactly as you want it stored — e.g. email, message.
  4. Add a hidden field named _redirect with your thank-you page URL, or set the redirect once in dashboard settings.
  5. Carrd Pro users can add a hidden _gotcha field as the honeypot.

Email template variables

The notification subject, autoresponse subject and autoresponse message accept {{fieldname}} (any submitted field, e.g. {{name}}), {{form_name}} and {{submitted_at}} (ISO timestamp). Values are inserted as plain text, capped at 200 characters, with line breaks removed in subjects. Unknown variables become empty. A _subject field always overrides the notification subject. Emails are sent as HTML plus plain text, and the per-form “From name” is shown as the sender name.

Redirect rules (_redirect / _next)

A hidden _redirect field (alias _next) is only honoured when it is an absolute http(s) URL whose host matches one of: your form’s allowed domains, the host of the redirect URL in settings, or the Origin/Referer host of the submitting page. Anything else is ignored and Sendform falls back to your saved redirect URL, then the built-in thank-you page (customisable title, message, button and accent colour in the dashboard). This prevents your endpoint from being abused as an open redirect.

Special email fields

Add these optional fields to control the notification email. They are used for the email only and are never stored with the submission.

  • _subject — custom subject line for the notification (default: “New submission — form name”).
  • _replyto — the address your reply goes to. Falls back to the submitter’s email field.
<form action="https://sendform.app/api/public/f/YOUR_ENDPOINT_ID" method="POST">
  <input type="email" name="email" required />
  <input type="hidden" name="_subject" value="New lead from the pricing page" />
  <input type="hidden" name="_replyto" value="sales@yoursite.com" />
  <button type="submit">Send</button>
</form>

Notifications go to up to 5 recipients you set per form in the dashboard. Emails are sent only when an email provider is configured; otherwise they are reported as skipped.

File uploads

Add enctype="multipart/form-data" and a file input. Files are stored privately and appear as download links in your inbox. Files over the per-form limit (default 5 MB, max 10 MB) are rejected with a 413 and a clear message.

<form action="https://sendform.app/api/public/f/YOUR_ENDPOINT_ID" method="POST" enctype="multipart/form-data">
  <input type="email" name="email" required />
  <input type="file" name="attachment" />
  <button type="submit">Send</button>
</form>

JSON and curl

const res = await fetch("https://sendform.app/api/public/f/YOUR_ENDPOINT_ID", {
  method: "POST",
  headers: { "content-type": "application/json", accept: "application/json" },
  body: JSON.stringify({ email: "jane@example.com", message: "Hello!" }),
});

const result = await res.json();
// { ok: true, id, spam_score, spam_status, files, email, webhook, slack, discord, usage }
curl -X POST https://sendform.app/api/public/f/YOUR_ENDPOINT_ID \
  -H "accept: application/json" \
  -F "email=jane@example.com" \
  -F "message=Hello from curl" \
  -F "attachment=@/path/to/file.pdf"

Signed webhooks

Set a webhook URL in settings and every clean submission is POSTed to it, signed with your form’s signing secret and retried once on a non-2xx response. Delivery status shows in your inbox.

import crypto from "node:crypto";

// Sendform sends:
//   x-sendform-timestamp: 1730000000
//   x-sendform-signature: sha256=<hex>
export function verify(rawBody, headers, secret) {
  const ts = headers["x-sendform-timestamp"];
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(headers["x-sendform-signature"] ?? "");
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Telegram

  1. Open Telegram, message @BotFather, send /newbot and follow the prompts. Copy the bot token it gives you.
  2. Send any message to your new bot (or add it to a group / channel as admin).
  3. Open https://api.telegram.org/bot<TOKEN>/getUpdates in a browser and copy chat.id (groups and channels start with -100).
  4. Paste both into the form’s Telegram settings, save, and click Send test. The token is stored server-side; only its last 4 characters are shown.

Clean submissions (and rows rescued with “Not spam”) send the same short summary as Slack/Discord. The outcome appears as telegram in the JSON response.

Slack and Discord

Paste a Slack incoming-webhook URL (https://hooks.slack.com/…) or a Discord channel webhook URL (https://discord.com/api/webhooks/…) into a form’s settings and use Send test to check it. Every clean submission — and any row you rescue with “Not spam” — posts a short message with the form name, up to 15 fields (each cut at 300 characters), file names and a link to your dashboard. A failed post never blocks the submission; the outcome is returned as slack / discord in the JSON response.

Spam, rate limits and allowed domains

  • Honeypot: any value in the hidden _gotcha field scores 80.
  • Heuristics: spam keywords, link stuffing, shouting, very short messages, plus disposable inbox domains and abusive + aliases.
  • Rate limit: per visitor and per form, default 10 posts per minute. Excess posts get a 429 with retry-after: 60.
  • Allowed domains: optional list of hostnames. Posts whose Origin or Referer is elsewhere get a 403. Empty means accept everywhere.
  • Monthly cap: Free stores 250 clean submissions per month (Pro 5,000, Agency 25,000), counted on the server; over the cap the endpoint answers 429.

Cloudflare Turnstile

Turn the bot check on in settings, paste your site key, and drop the widget into your form. Verification runs server-side on every post.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form action="https://sendform.app/api/public/f/YOUR_ENDPOINT_ID" method="POST">
  <input type="email" name="email" required />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Send</button>
</form>

Autoresponse

Enable the autoresponse in settings to email the submitter a confirmation whenever a clean submission comes in. Sendform uses the email field (or the first email-looking value) as the recipient.

REST API

Create an API key under API keys on the dashboard (up to 5; the key is shown once). Send it as Authorization: Bearer <key>. Each key allows 60 requests per minute.

# List your forms
curl https://sendform.app/api/public/v1/forms -H "Authorization: Bearer frk_YOUR_KEY"

# Latest clean submissions (max limit 100)
curl "https://sendform.app/api/public/v1/forms/YOUR_ENDPOINT_ID/submissions?limit=50&status=clean&since=2026-01-01T00:00:00Z" \
  -H "Authorization: Bearer frk_YOUR_KEY"

# Next page: pass next_cursor from the previous response
curl "https://sendform.app/api/public/v1/forms/YOUR_ENDPOINT_ID/submissions?cursor=NEXT_CURSOR" \
  -H "Authorization: Bearer frk_YOUR_KEY"

# Delete a submission (and its files)
curl -X DELETE https://sendform.app/api/public/v1/submissions/SUBMISSION_ID -H "Authorization: Bearer frk_YOUR_KEY"

Query parameters: limit (1–100, default 50), status (clean, spam or all), since (ISO date) and cursor. Newest first; next_cursor is null on the last page.

{
  "data": [
    {
      "id": "…",
      "created_at": "2026-10-10T09:00:00Z",
      "spam_status": "clean",
      "spam_score": 4,
      "data": { "email": "jane@example.com", "message": "Hello!" },
      "files": [{ "name": "cv.pdf", "size": 120334, "type": "application/pdf" }]
    }
  ],
  "next_cursor": "MjAyNi0xMC0xMF…"
}

Errors: 401 bad or missing key, 404 form or submission not yours, 429 rate limit. The dashboard also offers CSV and JSON export.

Privacy & data retention

Full details: Privacy policy.

  • Retention: each form keeps submissions for 30, 90 or 365 days (the default), or forever. On the Free plan, retention is capped at 90 days; 365 days and "Keep forever" are available on Pro and Agency. Expired submissions without files, with their webhook logs, are deleted by a daily cleanup. Expired submissions with files are deleted, files included, the next time that form receives a submission.
  • Spam auto-cleanup: submissions marked as spam (and their files) are deleted after 30 days on every plan, whatever the form’s retention setting — including “keep forever”.
  • Delete on demand: select rows in the inbox to delete them, or clear a whole form with “Delete all submissions”.
  • Erasure requests: “Erase data for an email address” deletes every submission across your forms that contains that address, including files.
  • Visitor IP addresses are never stored in plain form — only a one-way hash used for rate limiting. Uploaded files are private and only reachable via short-lived links.

Response codes

  • 200 — stored (HTML thank-you, 302 redirect, or JSON).
  • 403 — domain not in the allowlist.
  • 404 — unknown endpoint ID.
  • 413 — file over the size limit.
  • 429 — rate limit or monthly cap reached.