Payload
Every webhook delivery is a POST with a JSON body and a small set of headers. Same envelope for every event — the type field tells you what happened.
Headers
X-Formspring-Signature: <hex hmac-sha256 of raw body>
X-Formspring-Event: submission.created
X-Formspring-Timestamp: 1746651750
Content-Type: application/json
User-Agent: Formspring-Webhook/1.0
X-Formspring-Timestamp is unix seconds at the moment we built the request. Use it to reject stale replays in addition to the HMAC check (drop anything older than ~5 minutes).
Envelope
{
"type": "submission.created",
"submission_id": "01HFXX0X9R7KZJVN9VS6TG2C5T",
"form_id": "r2EdO-orF-3S",
"form_name": "Contact",
"received_at": "2026-05-07T16:09:10Z",
"payload": { },
"meta": { }
}
| Field | Type | Notes |
|---|---|---|
type |
string | submission.created or submission.flagged. |
submission_id |
ULID | Stable, sortable, unique. Use as your idempotency key. |
form_id |
string | The form's public ID. |
form_name |
string | Display name at time of submission. |
received_at |
ISO 8601 | When we accepted the submission. |
payload |
object | The raw form fields, key/value. |
meta |
object | IP-derived data, user agent, referrer, captcha verdict. |
submission.created
{
"type": "submission.created",
"submission_id": "01HFXX0X9R7KZJVN9VS6TG2C5T",
"form_id": "r2EdO-orF-3S",
"form_name": "Contact",
"received_at": "2026-05-07T16:09:10Z",
"payload": {
"email": "ada@example.com",
"name": "Ada Lovelace",
"message": "Loved the docs."
},
"meta": {
"ip_country": "GB",
"user_agent": "Mozilla/5.0 ...",
"referrer": "https://example.com/contact",
"spam_score": 0.04
}
}
submission.flagged
Same envelope, but with a reason and the spam verdict in meta.
{
"type": "submission.flagged",
"submission_id": "01HFXX1HEK7Q3MNTW5YH8RBZF2",
"form_id": "r2EdO-orF-3S",
"form_name": "Contact",
"received_at": "2026-05-07T16:11:42Z",
"reason": "captcha_failed",
"payload": {
"email": "spammer@example.com",
"message": "buy followers"
},
"meta": {
"ip_country": "??",
"spam_score": 0.91,
"captcha_verdict": "fail",
"honeypot_tripped": false
}
}
Possible reason values: spam_score, captcha_failed, honeypot_tripped, rate_limited, blocked_email, blocked_country.
File uploads
Files don't ride in the JSON body — webhooks would balloon and time out. Instead payload carries signed download URLs:
{
"payload": {
"email": "ada@example.com",
"attachment": {
"type": "file",
"filename": "design.pdf",
"size_bytes": 184213,
"content_type": "application/pdf",
"url": "https://files.formspring.io/u/abc.../design.pdf?sig=...",
"url_expires_at": "2026-05-07T17:09:10Z"
}
}
}
The signed URL is good for one hour. Fetch the bytes inside that window or store them yourself.
Size limits
| Limit | Value |
|---|---|
| Total request body | 1 MB |
| Single field value | 64 KB |
| Number of fields | 200 |
Submissions exceeding these are truncated — meta.truncated = true flags it. You'll still get a delivery; the data is just trimmed.