# Gravity Forms Webhooks

[Gravity Forms](https://www.gravityforms.com/) is a WordPress forms plugin. Its
[Webhooks Add-On](https://docs.gravityforms.com/triggering-webhooks-form-submissions/)
makes an HTTP request to your URL when a form is submitted. You configure it as a
**feed** under **Form Settings → Webhooks**.

## When to Use This Skill

* How do I receive Gravity Forms form submissions with a webhook?
* How do I verify a Gravity Forms webhook signature? (You can't. Gravity Forms doesn't
  sign webhooks. Authenticate with a custom Request Header instead.)
* How do I protect my Gravity Forms webhook endpoint with an `X-Webhook-Secret` header?
* How do I read field values (`"1"`, `"3.3"`) from a Gravity Forms webhook payload?
* Why does my Gravity Forms webhook arrive as form data instead of JSON?
* Why is my Gravity Forms webhook missing the entry ID? (Select Fields mode sends only
  the keys you map.)

## There Is No Signature

The Gravity Forms docs describe no signature header, no signing secret, no timestamp and
no HMAC. Don't invent an `X-GF-Signature` verifier: no real delivery would pass it.

The mechanism available is the feed's **Request Headers** setting, which sends one or
more HTTP headers of your choosing with every request. The docs don't prescribe a header
name; this skill uses `X-Webhook-Secret` by convention. You:

1. Generate a random secret yourself (`openssl rand -hex 32`).
2. In the feed, add a Request Header named `X-Webhook-Secret` with the secret as value.
3. On your endpoint, compare the header value with your secret in constant time. Return
  `401` if it's missing or wrong.
4. Fail closed: if the secret isn't configured on your server, reject the request.

This proves the caller knows the shared secret. It does not protect the body from
tampering, since nothing is signed over it, so you don't need the raw body for
verification: parsing before the check is safe. Checking first still avoids parsing
untrusted input.

## Verification (core)

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

// Gravity Forms sends the Request Header you configured on the feed:
//   X-Webhook-Secret: <secret>
function verifyGravityFormsSecret(headerValue, secret) {
  if (!secret) return false; // fail closed when GRAVITY_FORMS_WEBHOOK_SECRET is unset
  if (typeof headerValue !== 'string') return false;
  // Hash both sides so timingSafeEqual always gets equal-length buffers
  const a = crypto.createHash('sha256').update(headerValue).digest();
  const b = crypto.createHash('sha256').update(secret).digest();
  return crypto.timingSafeEqual(a, b);
}

// verifyGravityFormsSecret(req.get('x-webhook-secret'), process.env.GRAVITY_FORMS_WEBHOOK_SECRET)

```

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

## Events: Form Submission Only

Gravity Forms webhooks have **no event type field**. A feed runs when the form is
submitted, optionally only when its **Webhook Condition** matches. Route on `form_id`
instead, which is part of the entry.

The body is the **entry**. With **Request Body → All Fields** Gravity Forms sends the
entire unformatted entry. Keys are field IDs (`"1"`, and `"3.3"` for a sub-input such as
a name's first name) plus entry properties (`id`, `form_id`, `date_created`,
`source_url`, ...):

```json
{
  "id": "101",
  "form_id": "3",
  "date_created": "2026-10-21 10:15:30",
  "source_url": "https://example.com/contact/",
  "1": "Ada Lovelace",
  "2": "ada@example.com",
  "3.3": "Ada",
  "3.6": "Lovelace"
}

```

With **Select Fields** you map your own keys to form fields, entry meta or custom values
(merge tags allowed) and only those are sent. Map `id` and `form_id` to be able to
deduplicate and route.

The **Request Format** decides the encoding: `JSON` (with `Content-Type: application/json` for `POST` and `PUT`) or `FORM` (urlencoded). The examples accept both.

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gravity-forms-webhooks/references/overview.md) for the payload and feed settings.

## Environment Variables

```bash
# The secret YOU generated. The feed must send:  X-Webhook-Secret: <this value>
GRAVITY_FORMS_WEBHOOK_SECRET=generate_with_openssl_rand_hex_32

# Optional: header name, if you used something other than X-Webhook-Secret
# GRAVITY_FORMS_WEBHOOK_HEADER=X-Webhook-Secret

```

## Local Development

```bash
npx hookdeck-cli listen 3000 gravity-forms --path /webhooks/gravity-forms

```

No account required. The CLI creates a guest account on first run and gives you a public
URL and a web UI for inspecting requests. Use port `8000` for the FastAPI example.

Gravity Forms validates the Request URL with WordPress core checks
(`wp_http_validate_url`), so a custom port or a redirect to a URL that fails validation
aborts the feed. Use the public HTTPS URL the CLI prints.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gravity-forms-webhooks/references/overview.md) - How Gravity Forms webhooks work, feed settings, entry payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gravity-forms-webhooks/references/setup.md) - Adding the feed and the secret header in WordPress
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/gravity-forms-webhooks/references/verification.md) - Header authentication per framework, gotchas, debugging