Menu

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"
}
  • data is a JSON string, kept verbatim for backwards compatibility. data_parsed is the same payload already decoded — prefer it and skip the second parse.
  • created_at / updated_at are returned in the database’s native format. created_at_iso / updated_at_iso are the same instants as ISO-8601 UTC; parse those.
  • updated_at is 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.

Not what you're looking for? See the full surface at /llms.txt.