All docs
4 min read

Webhooks

Webhooks let you POST every submission to your own endpoint. Each form has its own list of webhooks. All endpoints require a bearer token.

Base URL: https://formspring.io/api/v1

Method Path Ability
GET /forms/{form}/webhooks webhooks:read
POST /forms/{form}/webhooks webhooks:write
GET /forms/{form}/webhooks/{webhook} webhooks:read
PUT /forms/{form}/webhooks/{webhook} webhooks:write
DELETE /forms/{form}/webhooks/{webhook} webhooks:write
GET /forms/{form}/webhooks/{webhook}/deliveries webhooks:read
POST /forms/{form}/webhooks/{webhook}/deliveries/{delivery}/replay webhooks:write

List webhooks

GET /forms/{form}/webhooks

Returns every webhook attached to the form, including its signing secret hash (the actual secret was returned only at creation time — see below).

Response 200:

{
  "data": [
    {
      "id": "wh_01H...",
      "label": "Production Slack",
      "provider": "slack",
      "url": "https://hooks.slack.com/services/...",
      "active": true,
      "config": { "channel": "#submissions" },
      "secret_last4": "a1f3",
      "created_at": "2026-01-12T09:00:00Z"
    }
  ]
}

Create a webhook

POST /forms/{form}/webhooks
Content-Type: application/json
{
  "label": "Production Slack",
  "provider": "slack",
  "url": "https://hooks.slack.com/services/...",
  "config": { "channel": "#submissions" }
}

provider is optional. Set it to one of slack, discord, teams, zapier, make, webhook (default) to format the payload for that target. Plain webhook posts the raw submission JSON.

Response 201:

{
  "data": { "id": "wh_01H...", "label": "Production Slack", ... },
  "secret": "whsec_abc123def456..."
}

The plaintext secret is shown once and never again. Save it somewhere your endpoint can access it. The secret is used to compute the X-Formspring-Signature header on every delivery so you can verify authenticity.

Show a webhook

GET /forms/{form}/webhooks/{webhook}

Returns the same shape as the list endpoint. The plaintext secret is never returned again — only secret_last4.

Update a webhook

PUT /forms/{form}/webhooks/{webhook}
Content-Type: application/json

Send only the fields you want to change. To rotate the signing secret, set "rotate_secret": true and the response includes a fresh secret field.

Response 200: updated WebhookResource. If rotate_secret was true, the response includes the new plaintext secret.

Delete a webhook

DELETE /forms/{form}/webhooks/{webhook}

Removes the webhook. In-flight deliveries continue but no new submissions trigger the webhook.

Response 200: {"ok": true}

List deliveries

GET /forms/{form}/webhooks/{webhook}/deliveries?per_page=50

Every delivery attempt is recorded. Each entry has the request body, response status, response body (truncated to 4KB), latency, and retry count.

Response 200:

{
  "data": [
    {
      "id": "whd_01H...",
      "status": "succeeded",
      "http_status": 200,
      "attempts": 1,
      "duration_ms": 142,
      "submission_id": "subm_01H...",
      "request_body": { ... },
      "response_body": "ok",
      "delivered_at": "2026-05-07T12:34:56Z"
    }
  ]
}

status is one of pending, succeeded, failed, dead. A delivery becomes dead after retries are exhausted.

Replay a delivery

POST /forms/{form}/webhooks/{webhook}/deliveries/{delivery}/replay

Re-fires the same delivery against the webhook's current URL and config. The original delivery row is preserved; a new delivery row is created for the replay.

Use cases:

  • Your endpoint was down when the original fired.
  • You changed the webhook URL and want to send historical events to the new endpoint.
  • Debugging — you can replay the same payload against a tunnel like ngrok.

Response 200: {"ok": true, "delivery_id": "whd_02H..."}

Common errors

Status Meaning
400 Invalid URL (must be HTTPS), invalid provider, or config validation failed
401 Token missing or invalid
403 Token lacks webhooks:read or webhooks:write
404 Form, webhook, or delivery not found
429 API rate limit hit

What's next