Delivery log
Every dispatch — success or failure, first try or seventh — is recorded. The log is your source of truth when something didn't show up downstream.
What we store per attempt
| Field | Notes |
|---|---|
id |
ULID for the delivery attempt. |
webhook_id |
Which webhook ran. |
submission_id |
Which submission triggered it. |
event |
submission.created or submission.flagged. |
attempt |
1–7 for normal flow; resets per replay. |
http_status |
The status code we got back, or null if no response. |
response_body |
First 4 KB of the response, UTF-8. Anything larger is truncated. |
response_headers |
A small subset (Content-Type, Server, Retry-After). |
error |
Short error code if the request never completed (timeout, dns_error, tls_error, connection_refused). |
latency_ms |
Time from request start to response end. |
dispatched_at |
When we fired this attempt. |
next_retry_at |
When we'll try again, or null if we're done. |
We do not store the request body in the delivery log — that's already on the submission. The log focuses on what happened on the wire.
Retention
| Plan | Retention |
|---|---|
| Free | 7 days |
| Pro | 30 days |
| Team | 90 days |
After retention, the delivery rows vacate. Replays are still possible — replay reads from the submission, not the log — but you lose the historical attempt detail.
Inspect from the dashboard
Form → Webhooks → click a webhook → Deliveries.
You see a reverse-chronological list. Each row shows status, attempt count, latency, and a quick excerpt of the response body. Click in for the full detail view: full response, full headers, raw payload that was sent, and a Replay button.
The list filters by:
- Outcome (success, retrying, failed)
- Event type
- Date range
- Submission ID
Inspect via the API
GET /api/v1/webhooks/{webhook_id}/deliveries
Authorization: Bearer <token>
Returns a paginated list of deliveries. Add ?status=failed to see only failures, or ?submission_id=... to drill into one submission.
GET /api/v1/deliveries/{delivery_id}
Returns the full record including the truncated response body and the request payload that was sent.
Inspect via MCP
If you've connected Formspring's MCP server to your editor or assistant:
formspring.list_deliveries(webhook_id, status="failed", limit=20)
formspring.get_delivery(delivery_id)
formspring.replay_delivery(delivery_id)
Useful for debugging from inside a chat session — pull the failing response, eyeball it, fix the handler, replay.
What good looks like
A healthy webhook log is mostly attempt 1, mostly 200, latency under 500ms. Patterns to watch:
- Climbing latency: your handler is doing too much synchronously. Move work to a queue and respond fast.
- Repeated 5xx: downstream is broken. Check the response body in the log — we keep it for a reason.
- Sporadic timeouts: cold starts. Warm the function, raise the timeout, or accept the retry will paper over it.
tls_error: cert expired or chain broken. Hit your URL withcurl -v.