> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onlyform.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive completed-response events and verify Onlyform signatures.

Webhooks let your server react when a form receives a completed response.

## Event type

The public API currently supports one event:

| Event                     | Trigger                                    |
| ------------------------- | ------------------------------------------ |
| `form.response.completed` | A respondent completes and submits a form. |

## Create a webhook

```bash theme={null}
curl --request POST https://api.onlyform.com/v1/webhooks \
  --header "X-API-Key: $ONLYFORM_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "form_id": "507f1f77bcf86cd799439011",
    "url": "https://example.com/webhooks/onlyform",
    "enabled": true,
    "signing_secret": "replace-with-a-long-random-secret"
  }'
```

Signing secrets must contain at least 16 characters. Onlyform never returns the secret after creation. The `has_secret` response field tells you whether a webhook has one.

<Warning>
  Use a long, randomly generated signing secret. Store it independently from the API key.
</Warning>

## Destination requirements

Onlyform accepts public `http` and `https` URLs, but production destinations should use HTTPS.

For request safety, webhook destinations:

* Must resolve through public DNS.
* Cannot resolve to private, loopback, link-local, reserved, or multicast addresses.
* Cannot use protocols other than HTTP or HTTPS.
* Should return a 2xx response promptly.

Redirects are not followed during delivery.

## Delivery request

Onlyform sends an HTTP `POST` request with a JSON body:

```json theme={null}
{
  "id": "a1b2c3d4",
  "response": {
    "formId": "507f1f77bcf86cd799439011",
    "answers": {
      "customer-email": "alex@example.com"
    }
  }
}
```

The exact response object may contain additional submission metadata. Ignore fields your integration does not use.

Delivery headers include:

```text theme={null}
Content-Type: application/json
User-Agent: Onlyform Webhook
Onlyform-Version: 1.0.0
Onlyform-Signature: sha256=<hex digest>
```

`Onlyform-Signature` is present when the webhook has a signing secret.

## Verify signatures

The signature is a SHA-256 HMAC of the exact raw request body.

```javascript theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyOnlyformWebhook(rawBody, signature, secret) {
  if (!signature?.startsWith('sha256=')) return false

  const expected = `sha256=${createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')}`

  const actualBuffer = Buffer.from(signature)
  const expectedBuffer = Buffer.from(expected)

  return actualBuffer.length === expectedBuffer.length
    && timingSafeEqual(actualBuffer, expectedBuffer)
}
```

Verify the signature before parsing or processing the payload. Your framework must give you access to the raw request bytes.

## Delivery history

`GET /v1/webhooks/{webhookId}/deliveries` returns up to the 100 most recent redacted delivery summaries. A network-level failure has status `0`.

Do not rely on automatic retries for business-critical processing. Make your handler idempotent and use the delivery history for monitoring and reconciliation.

## Rotate a signing secret

Update `signing_secret` with `PATCH /v1/webhooks/{webhookId}`, deploy the new secret to your receiver, and verify the next delivery. Updating the secret replaces the previous value.

<CardGroup cols={2}>
  <Card title="Create a webhook" icon="plus" href="/api-reference/webhooks/create-webhook">
    See the complete request and response schema.
  </Card>

  <Card title="List deliveries" icon="list" href="/api-reference/webhooks/list-webhook-deliveries">
    Inspect recent delivery outcomes.
  </Card>
</CardGroup>
