WP Webhooks / Blog / Architecture
Article · Architecture

Send Gravity Forms Submissions to n8n via Webhook

Forward Gravity Forms entries to n8n with a reliable, retry-capable webhook — payload mapping, HMAC signing, error handling, and end-to-end test recipe.

8 min 2026-05-17
#gravity#gform#n8n

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 <token>
       │  + X-Event-Id: <uuid>
       │  + X-Webhook-Id: <uuid>
       ▼
  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
DispatchInline (blocks submission)Persistent queue (Action Scheduler when available)
Retry on n8n 5xxSingle attempt5 attempts, exponential backoff up to 1h
Failure logSingle line per feedPer-attempt history with request & response
Event identityNoneX-Event-Id, X-Event-Timestamp, X-Webhook-Id automatic
ReplayManual re-submitReplay any logged event from admin or REST
Conditional dispatchPer-feed conditionsFree: 1 condition; Pro: unlimited groups w/ AND/OR
Bearer authManual workaroundBuilt-in Custom Headers — zero code
HMAC body signingNot built inAdd via fswa_headers filter (5 lines) when needed
REST API controlNoneToken-auth REST (logs, retry, replay, queue)
CostBundled 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 always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Can I connect Gravity Forms to n8n without writing code? +
Yes — both the Gravity Forms Webhooks add-on (bundled with GF Elite) and Webhook Actions support a no-code configuration. The add-on fires inline during submission with one attempt and no retry. 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.
Which n8n trigger node receives Gravity Forms submissions? +
The Webhook node. Set HTTP method to POST, copy the Production URL (not the Test URL), 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.
How do I authenticate the request so n8n knows it came from my WordPress site? +
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, and verify in an n8n Function node.
What happens to entries when n8n is offline? +
With the Gravity Forms Webhooks add-on they are lost — the dispatch fires inline, sees the failure, logs one line in the feed, and moves on. With Webhook Actions they enter a retry queue with exponential backoff (1m, 2m, 4m, 8m, capped at 1h between attempts; 5 attempts by default). Failed entries land in the permanently_failed list, which is queryable and replayable from the admin UI or REST API.
How do I send a specific Gravity Forms form to n8n while leaving others alone? +
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.
Ready

Stop losing webhooks.
Start logging them.

$ wp plugin install flowsystems-webhook-actions --activate