# PostTo
> Handle HTML form submissions without a backend. PostTo routes form submissions to email — with AI spam filtering, rate limiting, CORS handling, and a full management dashboard. No SPF, DKIM, or email infrastructure required. Hosted in Europe, GDPR-native.
## What PostTo Does
PostTo is a form backend service for developers. Instead of building email delivery, spam filtering, and submission management from scratch, developers point their HTML form's `action=""` attribute at a PostTo endpoint URL. PostTo handles everything else.
Integration requires no SDK, no JavaScript library, and no changes to how the form looks. Any HTML form, any framework, any static site can use PostTo.
Full public documentation is available with no account required — start at [https://postto.dev/docs](https://postto.dev/docs).
## Common Use Cases
- Contact forms on static sites (Hugo, Astro, Next.js, plain HTML)
- Feedback forms and bug-report widgets
- Lead capture and newsletter sign-up forms
- Job application forms
- Client inquiry forms on agency or freelancer sites
- Any form that should deliver to an inbox without a custom backend
## How PostTo Compares
PostTo is a direct alternative to Formspree, Basin, Netlify Forms, and similar form backend services. Key differences:
- **Hosted in Europe** — GDPR-native; data stays within European borders
- **Full submission dashboard** — every submission stored, searchable, exportable; spam-flagged submissions are held (never silently dropped) and can be reviewed and released
- **Signed API mode** — server-side integrations can HMAC-SHA256-sign each request for replay protection; no other service in this space offers this natively
- **Encryption at rest** — submission fields (PII) encrypted with AES-256
- **Webhook fan-out** — forward submissions to your app, CRM, or Slack via signed outbound webhooks
- **Tiered AI spam filtering** — first-party baseline filtering on all plans; optional AI-powered classification (off by default, opt-in per endpoint) included on Pro/Scale or as a $5/mo add-on on Starter
Detailed comparison pages:
- [Formspree alternative](https://postto.dev/alternatives/formspree)
- [Netlify Forms alternative](https://postto.dev/alternatives/netlify-forms)
- [Basin alternative](https://postto.dev/alternatives/basin)
- [Forminit alternative](https://postto.dev/alternatives/forminit)
- [Web3Forms alternative](https://postto.dev/alternatives/web3forms)
AI app builder guides:
- [Lovable contact form](https://postto.dev/ai-builders/lovable)
- [Bolt.new contact form](https://postto.dev/ai-builders/bolt-new)
- [v0 contact form](https://postto.dev/ai-builders/v0)
- [Replit Agent contact form](https://postto.dev/ai-builders/replit)
- [Base44 contact form](https://postto.dev/ai-builders/base44)
Framework integration guides:
- [Astro contact form](https://postto.dev/guides/astro)
- [Hugo contact form](https://postto.dev/guides/hugo)
- [Next.js form to email](https://postto.dev/guides/nextjs)
- [WordPress contact form](https://postto.dev/guides/wordpress)
- [Laravel contact form](https://postto.dev/guides/laravel)
- [SvelteKit contact form](https://postto.dev/guides/sveltekit)
- [Nuxt contact form](https://postto.dev/guides/nuxt)
Problem / solution pages:
- [GDPR form backend](https://postto.dev/solutions/gdpr-form-backend)
- [HTML form to email](https://postto.dev/solutions/html-form-to-email)
- [Form spam filtering](https://postto.dev/solutions/form-spam-filtering)
- [Client website contact forms](https://postto.dev/solutions/client-website-contact-forms)
Documentation pages (technical reference, no account required):
- [Getting started](https://postto.dev/docs)
- [Sending submissions](https://postto.dev/docs/sending-submissions)
- [Response format & error codes](https://postto.dev/docs/response-format)
- [Field mapping](https://postto.dev/docs/field-mapping)
- [Notification language](https://postto.dev/docs/notification-language)
- [Reserved fields](https://postto.dev/docs/reserved-fields)
- [Signed mode (HMAC-SHA256)](https://postto.dev/docs/signed-mode)
- [Webhooks](https://postto.dev/docs/webhooks)
- [CORS & domain allowlist](https://postto.dev/docs/cors)
- [Spam protection](https://postto.dev/docs/spam-protection)
- [Rate limits & quotas](https://postto.dev/docs/rate-limits-quotas)
## Integration Examples
### HTML form (browser)
```html
```
### Signed API (HMAC-SHA256, server-side only)
The signed mode adds a timestamp and HMAC-SHA256 signature to each request, preventing replay attacks and forged submissions. The secret key must stay server-side — never expose it in browser code.
```javascript
const crypto = require('crypto');
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
subject: 'Contact form',
message: 'Hello, I would like to...',
email: 'alice@example.com',
name: 'Alice',
});
const sig = crypto.createHmac('sha256', SECRET_KEY)
.update(`${ts}.${body}`).digest('hex');
await fetch('https://postto.dev/api/v1/send/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-PostTo-Timestamp': ts,
'X-PostTo-Signature': sig,
},
body, // exact bytes that were signed
});
```
PHP, Python, and Laravel examples are available in the endpoint dashboard.
## Request Fields & Response Format
Submit any field names — every field you send is stored verbatim in the submission's `fields` object. Four field names are also read as **canonical roles** used to build the outgoing email; they are not reserved or stripped, just given extra meaning if present:
| Field name (default) | Canonical role |
|---|---|
| `subject` | Email subject line |
| `message` | Email body |
| `email` (or `_replyto`) | Reply-To address — discarded (not rejected) if not a valid email |
| `name` | Sender name |
If a form's fields are already named `subject`/`message`/`email`/`name`, they resolve automatically — nothing is overwritten or ignored. If a form uses different names (e.g. `topic`, `body`, `from`), map them once under the endpoint's **Settings → Field map** in the dashboard so PostTo knows which field plays which role; every field is still stored under its original name in `fields` regardless of mapping. Fields with no role just ride along in `fields` and appear in the dashboard and webhook payload.
Reserved field names with special (non-role) handling:
| Field | Purpose |
|---|---|
| `_hp` | Honeypot — must stay empty; a filled value is a silent discard, not stored, not delivered |
| `cf-turnstile-response` | Cloudflare Turnstile token, read only when Turnstile is enabled on the endpoint |
| `_next` | Redirect URL after a successful non-JSON (browser) submission; overrides the endpoint's configured success redirect |
### Response format
PostTo negotiates on the request's `Accept` header. Send `Accept: application/json` to get a JSON response back instead of a browser-style redirect:
Success (200):
```json
{ "id": "sub_01JXF...", "status": "pending" }
```
Error:
```json
{ "error": "rate_limited", "message": "Too many submissions." }
```
`error` is always a stable machine code, never free text:
| Code | HTTP | Meaning |
|---|---|---|
| `endpoint_paused` | 403 | Endpoint is paused |
| `endpoint_unverified` | 403 | Destination not yet verified |
| `invalid_signature` | 401 | HMAC signature missing, invalid, or expired (signed mode) |
| `turnstile_failed` | 403 | Cloudflare Turnstile human verification failed |
| `rate_limited` | 429 | Per-endpoint or per-IP limit exceeded |
| `quota_exceeded` | 402 | Monthly quota reached |
| `empty_submission` | 422 | No fields in the request body |
| `origin_not_allowed` | 403 | Origin not in the allowlist |
A plain `