# Form.io Webhooks

The Form.io **Webhook action** calls an external URL when a form submission is created, updated
or deleted. Requests are **not signed**. If you fill in **Authorize User** and **Authorize
Password** on the action, Form.io sends them as `Authorization: Basic base64(user:pass)`, and your
endpoint authenticates the request by checking that header.

A static shared credential authenticates the **sender**, not the **body**, and gives **no replay
protection** (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.

## When to Use This Skill

* How do I receive Form.io webhooks?
* How do I authenticate a Form.io Webhook action (Authorize User / Authorize Password)?
* Does Form.io sign webhook payloads? (No: use HTTP Basic auth)
* How do I tell a submission create from an update or delete? (By HTTP method)
* Why does my Form.io webhook endpoint return 401 or 405?

## Verification (core)

```javascript
const crypto = require('crypto');

const sha = (b) => crypto.createHash('sha256').update(b).digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(sha(a), sha(b));

function verifyFormioRequest(headers, user = process.env.FORMIO_WEBHOOK_USERNAME,
                             pass = process.env.FORMIO_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                       // never accept when unconfigured
  const m = /^basic +([A-Za-z0-9+/]+={0,2}) *$/i.exec(headers.authorization || '');
  if (!m) return false;
  const decoded = Buffer.from(m[1], 'base64');
  const i = decoded.indexOf(0x3a);                        // split on the FIRST colon
  if (i < 0) return false;
  const userOk = safeEqual(decoded.subarray(0, i), Buffer.from(user));
  const passOk = safeEqual(decoded.subarray(i + 1), Buffer.from(pass));
  return userOk && passOk;                                // both evaluated, no short-circuit
}

```

* Header: `Authorization: Basic base64(username:password)`. Scheme is case-insensitive; split on the
  first colon because passwords may contain colons.
* Compare username and password each in constant time, and evaluate both before deciding. In
  Python use `hmac.compare_digest` on **bytes** (encode both sides; `str` raises on non-ASCII).
* Return `401` for a missing, malformed or mismatched header, `400` for a non-empty body that isn't
  a JSON object, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **Accept more than POST.** The request method is set in the action. With the default **Match**
  method Form.io sends POST for creates, PUT for updates and DELETE for deletes (URL params go in
  the query string). The HTTP verb is the only hint of the operation: there is no event field.
  The examples accept POST, PUT, PATCH, GET and DELETE and handle an empty body.
* **Hookdeck:** the Form.io source pre-selects POST only. Allow PUT and DELETE on the source (and
  any other methods your action uses), or those requests are rejected.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/formio-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/formio-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/formio-webhooks/examples/fastapi/).

## Operations

| Method | Operation (Match) | Body |
| --- | --- | --- |
| `POST` | Submission create | `{ request, response, submission, params }` |
| `PUT` / `PATCH` | Submission update | `{ request, response, submission, params }` |
| `DELETE` | Submission delete | May be empty; `params` arrive as a query string |
| `GET` | Only if chosen in the action | Empty |

## Environment Variables

```bash
FORMIO_WEBHOOK_USERNAME=your-webhook-username   # "Authorize User" on the Webhook action
FORMIO_WEBHOOK_PASSWORD=your-webhook-password   # "Authorize Password" on the Webhook action

```

## Local Development

Form.io must reach your endpoint over the network. Tunnel with the Hookdeck CLI (no account
required):

```bash
npx hookdeck-cli listen 3000 formio --path /webhooks/formio

```

Use the printed URL as the **Request URL** of the Webhook action.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/formio-webhooks/references/overview.md) - What the Webhook action sends, methods and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/formio-webhooks/references/setup.md) - Add the Webhook action and set the credentials
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/formio-webhooks/references/verification.md) - Basic auth check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/formio-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests