Submissions
Submission shape, spam handling, and retention windows.
Submission shape
Endpoints accept application/x-www-form-urlencoded or application/json. Field names are free-form — whatever you post is what shows up in the recipient email and the dashboard.
curl -X POST https://api.gopigeon.dev/f/f_abc123def456xyz0 \ -H 'Content-Type: application/json' \ -d '{"name":"Jane","email":"jane@somewhere.com","message":"hello"}'
Honeypot: _gotcha
_gotcha is a reserved field name. Any non-empty value marks the submission as spam and drops it before it counts toward the unclaimed cap. Leave it out of real forms; include it as a hidden input for bot detection.
<!-- Honeypot: leave _gotcha empty; bots often fill every field. --> <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
Idempotency
POST /f/:id accepts an optional Idempotency-Key header. A repeat carrying a key already seen on that endpoint does not store a second row — it returns the original submission_id with idempotent_replay: true, and consumes no quota.
# Send the same key twice: the second call stores nothing. curl -X POST https://api.gopigeon.dev/f/f_abc123def456xyz0 \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 9f2c1e64-7a30-4b8e-9d51-0f6c2b7a8e13' \ -d '{"event":"direct_booking_click"}' # 1st => {"ok":true,"submission_id":"sub_abc123def456xyz0"} # 2nd => {"ok":true,"submission_id":"sub_abc123def456xyz0","idempotent_replay":true}
This matters most for fire-and-forget sends with keepalive: true, where the browser can retry a request on its own and inflate your counts invisibly. Generate a fresh value — a UUID — per logical event, and reuse it verbatim on any retry of that event.
- Opt-in. Omit the header and nothing changes; two identical posts store two rows.
- Scoped per endpoint. The same key on a different endpoint is a different event.
- Max 255 printable ASCII characters, no spaces. A malformed key returns 400 rather than being ignored, so you are never left believing retries are deduplicated when they are not.
- Browser callers need no extra setup — the header is already in
Access-Control-Allow-Headers.
How long a key stays claimed is simply how long its submission exists: for telemetry endpoints that is the plan retention window, after which the key becomes reusable.
Redirects
If the endpoint has a configured redirect_url, successful submissions return an HTTP 303 See Other to that URL instead of the JSON response body. Useful for classic server-rendered form flows that want a confirmation page. Idempotent replays redirect identically.
Reading submissions back
GET /api/forms/:id/submissions returns rows in this shape:
{ "id": "sub_abc123def456xyz0", "form_id": "f_abc123def456xyz0", "data": "{\"email\":\"jane@somewhere.com\"}", "data_parsed": { "email": "jane@somewhere.com" }, "metadata": "{}", "is_spam": 0, "created_at": "2026-08-02 18:43:00", "created_at_iso": "2026-08-02T18:43:00Z", "updated_at": "2026-08-02 18:43:00", "updated_at_iso": "2026-08-02T18:43:00Z" }
datais a JSON string, kept verbatim for backwards compatibility.data_parsedis the same payload already decoded — prefer it and skip the second parse.created_at/updated_atare returned in the database’s native format.created_at_iso/updated_at_isoare the same instants as ISO-8601 UTC; parse those.updated_atis a monotonic mutation watermark. It moves whenever a row changes, not just when it is created.
Incremental pulls
Pass since — an ISO-8601 UTC timestamp — to fetch only what changed after that point, oldest first.
# First pull: everything. Then pass back the next_since you were given. curl -s -H "Authorization: Bearer $GOPIGEON_API_KEY" \ 'https://api.gopigeon.dev/api/forms/f_abc123def456xyz0/submissions?since=2026-08-01T00:00:00Z'
The response includes next_since: store it and hand it back on the next pull. The cursor runs over updated_at rather than created_at, so it catches edits to older rows as well as new submissions — which a created_at filter would miss. Without it, a nightly sync re-walks your entire history every night and gets slower forever.
Retention
Submissions are retained for the owner’s plan retention window (free tier: 30 days; pro tier: 90 days). Unclaimed endpoints retain submissions until expiry or cap, whichever comes first. Spam submissions (_gotcha triggered) are dropped immediately and never stored.
Telemetry endpoints are retained on a much shorter window — 7 days on Free, 30 on Pro — and are actively swept.