/ Tutorial — Gravity Forms × n8n

Send Gravity Forms Submissions to n8n via Webhook

Connect every Gravity Forms entry to an n8n workflow — with HMAC signing, automatic retries when n8n is offline, and a delivery log for every attempt. Six configuration steps, end-to-end.

TL;DR

  • Trigger: gform_after_submission (or _{id} for one form). Pair with n8n's Webhook trigger node.
  • Webhook Actions auto-attaches X-Event-Id, X-Event-Timestamp, X-Webhook-Id for n8n-side deduplication.
  • Always route through the queue — n8n downtime triggers automatic retry (1m, 2m, 4m, 8m…) instead of silent data loss.
/ Overview

What is the simplest way to send Gravity Forms data to n8n?

Add an HTTP webhook to your form that POSTs the entry as JSON to an n8n Webhook trigger node. The Gravity Forms official Webhooks add-on can do this, and so can Webhook Actions. The choice between them comes down to one question: what happens when n8n is unreachable?

The Gravity Forms Webhooks add-on fires the request inline during form submission. If n8n returns a 5xx, times out, or is down for maintenance, the submission still completes on the WordPress side but the entry never reaches n8n — and there is no record of the failure beyond a single line in the form's feed log. Production deployments need a queue with automatic retry; that is the gap Webhook Actions fills.

In both cases the n8n side is identical: a Webhook trigger node with Header Auth turned on, then the workflow does the actual work (write to a CRM, push to Slack, append to a sheet).

  Gravity Forms submission
       │  (gform_after_submission hook)
       ▼
  Webhook Actions (queue + retry)
       │  POST application/json
       │  + Authorization: Bearer 
       │  + X-Event-Id: 
       │  + X-Webhook-Id: 
       ▼
  n8n Webhook trigger node
       │
       ├─ Header Auth: verify bearer token
       ├─ Branch on form_id
       ├─ Map fields → CRM / Slack / Sheets
       └─ Respond 200 OK ──► WP marks delivery complete

  If n8n is down or returns 5xx:
       │
       ▼
  Webhook Actions retries with exponential backoff
       └─ 1m → 2m → 4m → 8m → cap at 1h → permanently_failed
/ n8n Side

Which n8n trigger node do you use?

The Webhook node, configured with HTTP method POST and Response Mode set to "Immediately" with response code 200. Switch the workflow to active, copy the Production URL (not the Test URL — the test URL only works while the editor is open), and use that as the destination in your WordPress configuration.

Authentication tab: pick Header Auth, header name Authorization, value Bearer <long-random-secret>. On the WordPress side, add the same Authorization: Bearer ... as a Custom Header on the webhook in Webhook Actions. Two clicks each side, no code, no shared signing secrets to rotate. Without header auth, anyone who guesses your workflow URL can post arbitrary payloads.

Response data: return {"received": true} only after every required field is present in the payload. Returning 200 to a malformed payload tells the WordPress side the delivery succeeded — when really the workflow exited at step two without doing anything. Validate the payload shape in n8n and return a 400 explicitly when it fails.

/ Payload Mapping

How do you build the Gravity Forms webhook payload?

Two paths. The no-code path: in Webhook Actions → Add Webhook, paste the n8n Production URL, pick trigger gform_after_submission (or gform_after_submission_{form_id} for a single form), submit the form once to capture an example payload, then use the Payload Mapping editor to rename Gravity Forms field IDs (1.3, 1.6, 2) to semantic keys (first_name, last_name, email). Save. The next submission dispatches with the mapped shape.

The code path is for advanced enrichment — looking up a CRM record id from post meta, signing the body, injecting computed fields. Use the fswa_webhook_payload filter, which runs just before dispatch and receives the mapped payload, webhook ID, trigger name, and the original pre-mapping payload (see the plugin README on WordPress.org for the full filter list). The plugin's admin UI exposes a Field Selector with live preview so most rename / restructure work never needs PHP.

Optional enrichment via fswa_webhook_payload filter
// Inject a computed field into the outgoing payload for one webhook. add_filter( 'fswa_webhook_payload', function( $payload, $webhook_id, $trigger, $original ) { if ( $trigger !== 'gform_after_submission' ) { return $payload; } // Pull a CRM contact id stored on the entry, fall back to lookup by email. $payload['crm_contact_id'] = my_crm_lookup_contact( $payload['email'] ); return $payload; }, 10, 4 );

Scope the filter on $webhook_id when multiple webhooks listen to the same trigger but need different enrichment. See the gform_after_submission deep dive for hook argument details.

/ Security

How do you secure the n8n webhook (bearer token + event IDs)?

Two layers, both no code. Layer one: bearer token via the plugin's built-in Custom Headers. Open the webhook in Webhook Actions → Edit, add a Custom Header Authorization with value Bearer <long-random-secret>, save. In the n8n Webhook node, set Authentication to Header Auth with name Authorization and value Bearer <same-secret>. Anyone who guesses your workflow URL still cannot post.

Layer two: dedup with the headers Webhook Actions attaches automatically. X-Event-Id is a UUID stable across retries — store the last N values in n8n (a Set node + workflow static data, or a small Redis lookup) and exit early on a repeat. This matters because the queue retries on 5xx, and the same event can arrive twice if n8n returned 200 just after a transient error.

That covers 95% of real deployments. You only need HMAC body signing if you do not trust the network path (running n8n on the public internet without TLS pinning, or piping through a third-party proxy that could mutate the body). In that case hook the fswa_headers filter, compute hash_hmac('sha256', $body, $secret), inject as X-Signature, and verify in an n8n Function node. Bearer + TLS is enough for almost everyone else.

/ Reliability

How do you handle retries when n8n is offline?

Queue the payload in WordPress; do not POST inline. Webhook Actions does this by default — every dispatch goes through a queue table with status, attempt count, and per-attempt request/response history. When n8n is unreachable, retries fire with exponential backoff (1m → 2m → 4m → 8m, capped at 1h between attempts; 5 attempts before permanently_failed — see the plugin README "Smart Retry" section). An inline wp_remote_post in gform_after_submission gets one chance, no log, and the entry is gone.

On the n8n side, return a 5xx (not a 4xx) when the workflow fails for a transient reason — a downstream API is rate-limiting, a CRM is briefly down. Webhook Actions treats 4xx as permanent and stops retrying; 5xx and 429 trigger the next backoff. Use 5xx for "try again," 4xx for "the payload is wrong, do not retry." See retry vs replay for the data model behind both.

/ Field Mapping

How do you map Gravity Forms fields to n8n nodes?

Inside the n8n Webhook node, the payload arrives under $json.body (or directly under $json depending on response settings). Reference fields with expressions like {{ $json.body.email }} in downstream nodes. The semantic-key approach from the previous section means n8n expressions read like English instead of {{ $json.body["2"] }}.

For repeating fields (multiple checkboxes, dynamic field rows), the value is a comma-separated string in the Gravity Forms $entry array. Split it in the PHP mapper before sending — explode( ',', $entry['5'] ) — so n8n receives a real array and can Split In Batches over it cleanly.

File uploads: Gravity Forms stores the URL in the entry array as a serialized JSON string for multi-file fields, or a plain URL for single-file. Forward the URL (n8n can fetch it with an HTTP Request node) rather than the file bytes — keeps the webhook payload small and avoids n8n timeouts on large attachments.

/ Testing

How do you test the flow end-to-end?

1. In n8n, click Listen for test event on the Webhook node and submit a test entry from Gravity Forms' preview. Confirm the payload arrives and any HMAC verification passes.
2. Switch the n8n workflow to Active, change the WordPress webhook to the Production URL, and submit a real entry. Watch the Webhook Actions delivery log for status 200.
3. Simulate failure: change the WordPress webhook URL to a typo and submit again. Confirm the log shows the retry schedule and the entry lands in permanently_failed after the final attempt — that is your dead-letter path working.

Skip step 3 at your peril. The first time most n8n integrations fail in production is on a Saturday night when no one is watching the logs.

/ Tooling Choice

When should you skip the Gravity Forms Webhooks add-on and use Webhook Actions instead?

ConcernGravity Forms Webhooks add-onWebhook Actions
Inline (blocks submission)Persistent queue (Action Scheduler when available)
Single attempt5 attempts, exponential backoff up to 1h
Single line per feedPer-attempt history with request & response
NoneX-Event-Id, X-Event-Timestamp, X-Webhook-Id automatic
Manual re-submitReplay any logged event from admin or REST
Per-feed conditionsFree: 1 condition; Pro: unlimited groups w/ AND/OR
Manual workaroundBuilt-in Custom Headers — zero code
Not built inAdd via fswa_headers filter (5 lines) when needed
NoneToken-auth REST (logs, retry, replay, queue)
Bundled with GF EliteFree; Pro for unlimited conditions, per-webhook retry

The add-on is fine for low-volume forms where occasional data loss is tolerable. For lead capture, e-commerce checkouts, and anything that costs money when an entry is missed, the queue-backed approach pays for itself the first time n8n has a five-minute outage.

/ FAQ

Common questions

Yes — both the Gravity Forms Webhooks add-on (bundled with GF Elite) and Webhook Actions support no-code configuration.

The add-on fires inline with one attempt. Webhook Actions queues the dispatch, retries on n8n outages with exponential backoff up to one hour, and logs every attempt with the full request and response.
The Webhook node. Set HTTP method to POST, copy the Production URL (not the Test URL — that only works while the editor is open), and paste it into your Gravity Forms webhook destination.

Enable Header Auth with an X-API-Key for a first layer of access control, then verify HMAC in a Function node.
Easiest: bearer token via the plugin's built-in Custom Headers. Add a Custom Header named Authorization with value Bearer <long-random-secret> on the webhook in Webhook Actions, then enable Header Auth on the n8n Webhook node with the same name and value. No code on either side.

Use HMAC body signing only when you do not trust the transport — hook the fswa_headers filter, compute hash_hmac('sha256', $body, $secret), and verify in an n8n Function node.
Gravity Forms Webhooks add-on: lost. The dispatch fires inline, logs one line in the feed, and moves on.

Webhook Actions: queued and retried with exponential backoff (1m → 2m → 4m → 8m, capped at 1 hour between attempts; 5 attempts by default). Failed entries land in the permanently_failed list, replayable from the admin UI or REST API.
Hook gform_after_submission_FORMID instead of gform_after_submission. The suffix is the form ID — for example add_action( 'gform_after_submission_3', ... ) fires only on form 3.

Inside the callback, build the payload and enqueue the webhook for that one form. See the gform_after_submission hook reference.