/ Article — Gravity Forms

gform_after_submission Webhook:
Gravity Forms Integration

gform_after_submission is the correct hook for sending Gravity Forms entries to a webhook. This article covers what the hook provides, how to build a raw PHP implementation, where that approach breaks in production, and how Webhook Actions handles reliable delivery with field mapping, file support, and automatic retry.

TL;DR

  • gform_after_submission fires after each successful Gravity Forms submission and provides both the $entry object and the $form array
  • Raw PHP wp_remote_post works in dev but loses events in production — any timeout or endpoint failure silently drops the submission
  • Webhook Actions enqueues the payload at hook time into its own database queue, dispatches it asynchronously via a background worker (triggered by Action Scheduler when available), and retries on failure with field mapping UI and per-attempt logs
/ The Hook

What does gform_after_submission do?

gform_after_submission is the action hook Gravity Forms fires after an entry has been saved to the database and all built-in processing — notifications, confirmations, payment actions — has completed. It is the correct place to trigger integrations that depend on the complete, saved entry record.

Gravity Forms processes a submission in a predictable sequence. The form validates, the entry is written to wp_gf_entry and wp_gf_entry_meta, built-in notifications are dispatched, then gform_after_submission fires. By the time your callback runs, you have a fully resolved $entry array with every field value, an entry ID, a timestamp, and the source URL. Nothing is provisional.

There is also a form-specific variant: gform_after_submission_{form_id}. If you want your callback to run only for form ID 3, hooking gform_after_submission_3 removes the need for an if ( $form['id'] === 3 ) conditional inside the callback. Both variants receive identical parameters.

The hook is documented in the official Gravity Forms developer documentation and has been available since Gravity Forms 1.8.

/ Parameters

What parameters does the hook pass?

The hook passes two arguments: $entry and $form. The add_action call must specify the priority (10 is the default) and the accepted argument count (2) for both to arrive in your callback.

Hook signature
add_action( 'gform_after_submission', function( $entry, $form ) { // $entry and $form are both available here }, 10, 2 );

$entry is an associative array containing everything about the submitted entry:

  • $entry['id'] — the numeric entry ID assigned by Gravity Forms
  • $entry['form_id'] — the ID of the form that was submitted
  • $entry['date_created'] — UTC timestamp of the submission
  • $entry['source_url'] — the page URL where the form was embedded
  • $entry['ip'] — submitter IP address
  • $entry['1'], $entry['2'], … — field values keyed by numeric field ID
  • $entry['1.3'], $entry['1.6'], … — sub-field values for multi-input fields (Name, Address, etc.)

$form is the full form configuration array. It contains the form title, fields definition, notification settings, confirmation rules, and all Gravity Forms metadata. You rarely need the full $form object for webhook dispatch — the entry array contains everything you need to build a payload — but $form['title'] and $form['id'] are useful for routing logic.

Use rgar( $entry, $field_id ) instead of $entry[ $field_id ] directly. The rgar() helper function returns an empty string for missing keys rather than triggering a PHP notice, which matters when optional fields are not submitted.

Accessing field values safely
add_action( 'gform_after_submission', function( $entry, $form ) { // Simple text field (field ID 1) $name = rgar( $entry, '1' ); // Email field (field ID 2) $email = rgar( $entry, '2' ); // First name sub-field of a Name field (field ID 3) $first = rgar( $entry, '3.3' ); $last = rgar( $entry, '3.6' ); // Entry metadata always present $entry_id = rgar( $entry, 'id' ); $created_at = rgar( $entry, 'date_created' ); $form_title = rgar( $form, 'title' ); }, 10, 2 );
/ Basic Implementation

How do you send form data to a webhook with raw PHP?

The minimal implementation hooks gform_after_submission, builds a payload array from the entry fields you need, and calls wp_remote_post() to deliver it. This pattern works in local development and for low-stakes, low-traffic forms where a dropped delivery is acceptable.

functions.php — minimal gform_after_submission webhook
add_action( 'gform_after_submission_3', function( $entry, $form ) { $payload = [ 'entry_id' => rgar( $entry, 'id' ), 'form_title' => rgar( $form, 'title' ), 'name' => rgar( $entry, '1' ), 'email' => rgar( $entry, '2' ), 'message' => rgar( $entry, '3' ), 'submitted' => rgar( $entry, 'date_created' ), ]; $response = wp_remote_post( 'https://your-n8n-instance.example.com/webhook/gf-contact', [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( $payload ), 'timeout' => 10, ] ); // No retry. No queue. No log. Event is gone if this fails. }, 10, 2 );

This pattern produces a working webhook for simple cases. It is also the pattern that fails silently in production for reasons we cover in the next section.

/ Production Reality

Why does the raw PHP approach break in production?

The raw PHP implementation has four structural problems that only surface under production conditions. None of them are visible during local development.

1. Inline execution blocks the user's form response. gform_after_submission runs synchronously inside the PHP request that processed the form submission. The wp_remote_post() call holds the connection open until the endpoint responds or the timeout expires. On a slow n8n instance or a distant Zapier endpoint, the person who submitted the form waits for that HTTP call to complete before seeing the confirmation page.

2. Timeout failures permanently lose the entry. wp_remote_post() defaults to a 5-second timeout. Any endpoint that takes longer — including healthy endpoints under momentary load — results in a WP_Error with error code http_request_failed. There is no retry, no queue, and no log. The form entry was saved to Gravity Forms, but the downstream system never received it.

3. Endpoint downtime causes permanent data loss. If your n8n instance restarts, your Zapier webhook URL changes, or the receiving server returns a 5xx during a deployment, every form submission during that window is permanently lost. The raw PHP pattern has no mechanism to detect or recover from this.

4. No visibility into failures. WordPress does not log outbound wp_remote_post() calls by default. A failed webhook delivery produces no error in the WordPress admin, no PHP log entry (unless WP_DEBUG_LOG is enabled and the error is caught), and no alert. The failure is structurally silent.

Scenario Raw PHP (wp_remote_post) Webhook Actions (queued)
Endpoint timeout (5s) Entry lost permanently Retried with backoff
Endpoint 503 (server error) Entry lost permanently Retried up to 5 times
PHP crash mid-request Entry lost permanently Queue row survives crash
Endpoint down for 2 hours All entries during window lost Queue drains when endpoint recovers
Delivery failure visibility None Per-attempt log with status codes
User form experience Blocks confirmation page Instant — async dispatch

For a full analysis of how inline WordPress webhook delivery fails in production and the infrastructure required to fix it, see Why WordPress Webhooks Silently Fail in Production.

/ Webhook Actions

How does Webhook Actions use gform_after_submission?

Webhook Actions registers its own gform_after_submission listener internally. When a Gravity Forms submission triggers the hook, Webhook Actions does not attempt delivery immediately. Instead, it serializes the entry payload and creates a job in its own database queue. The form's confirmation page renders without waiting for any HTTP call.

Webhook Actions' queue runner picks up the job on the next cycle — typically within a few seconds on a properly configured site — and dispatches the HTTP request from a background process completely isolated from the original form submission request. When Action Scheduler is available (e.g. WooCommerce is active), WA uses it to trigger its queue runner reliably; otherwise it falls back to WP-Cron. If the endpoint is unavailable, the job is rescheduled with exponential backoff and retried up to five times before marking the delivery as failed.

  Gravity Forms submission
       │
       ▼
  gform_after_submission fires
       │
       ▼
  Webhook Actions: serialize payload → enqueue into WA queue
       │
       ▼
  Form confirmation rendered to user  ◄── request ends here
       │
       (background, next WA runner cycle)
       ▼
  WA queue runner (triggered by AS when available, else WP-Cron)
       │
       ├─ 2xx response     → delivery logged as complete
       ├─ 5xx / timeout    → reschedule (exponential backoff, up to 5 retries)
       └─ 4xx response     → log permanent failure, alert admin

Every delivery attempt — success or failure — is written to the Webhook Actions delivery log. From the WordPress admin you can see the HTTP status code, response body, attempt number, and timestamp for every job. Permanently failed deliveries can be manually retried from the log view without touching the codebase.

The plugin configuration is entirely UI-based. You select the Gravity Forms trigger, choose the forms to listen on (all forms or a specific form by ID), configure the endpoint URL, and optionally define field mappings. No add_action code is required. See the Gravity Forms Webhooks example for the full step-by-step setup walkthrough.

/ Field Mapping

How do you map Gravity Forms fields to webhook payload keys?

By default, Webhook Actions sends the full Gravity Forms entry array as the webhook payload — every field keyed by its numeric Gravity Forms field ID. This is useful for development but produces a payload like {"1": "John", "2": "[email protected]", "3": "Hello"} that depends on specific field IDs remaining stable.

Field mapping lets you define a stable, semantic payload structure that does not change when you rearrange fields in the Gravity Forms editor. In the Webhook Actions field mapping UI, you define key-value pairs where each value is a merge tag referencing a Gravity Forms field.

Equivalent PHP mapping for reference
// This is what Webhook Actions does internally when field mapping is configured. // You do not write this code — the plugin generates it from the UI mapping. $payload = [ 'name' => rgar( $entry, '1.3' ) . ' ' . rgar( $entry, '1.6' ), 'email' => rgar( $entry, '2' ), 'phone' => rgar( $entry, '3' ), 'message' => rgar( $entry, '4' ), 'source_url' => rgar( $entry, 'source_url' ), 'entry_id' => rgar( $entry, 'id' ), 'submitted_at' => rgar( $entry, 'date_created' ), ];

Multi-input fields require attention. Gravity Forms splits Name, Address, and Time fields into sub-inputs, each with a decimal field ID (1.3 for first name, 1.6 for last name in a Name field). Webhook Actions exposes these as individual merge tags in the mapping UI. For a Name field with ID 1, the merge tags are {Name (First):1.3} and {Name (Last):1.6}.

Checkboxes and multi-select fields store their values differently. Each checkbox option is a separate sub-input: checkbox field ID 5 with three options stores them at 5.1, 5.2, and 5.3. The value is the option label if checked, or empty string if not. Webhook Actions provides a choice merge tag that concatenates all checked values as a comma-separated string for easier downstream handling.

/ File Uploads

How do you handle file upload fields?

Gravity Forms file upload fields store the uploaded file URL in the entry — not the file binary. The value at $entry['5'] for a single-file upload field with ID 5 is the full public URL of the uploaded file, something like https://yoursite.com/wp-content/uploads/gravity_forms/123-abc/file.pdf.

For multiple-file upload fields (Gravity Forms 2.6+), the entry value is a JSON-encoded array of URLs. You need to json_decode() it to work with individual URLs:

Handling multi-file upload field values
// Single file upload — value is a URL string $file_url = rgar( $entry, '5' ); // Multi-file upload — value is a JSON array of URLs $raw_value = rgar( $entry, '6' ); $file_urls = json_decode( $raw_value, true ); // $file_urls is now [ 'https://...file1.pdf', 'https://...file2.pdf' ]

In a webhook payload, send file URLs rather than binaries. Downstream systems (n8n, Zapier, Make) can fetch the file contents from the URL if needed. Embedding binary data in a webhook payload body is impractical — it dramatically increases payload size, complicates JSON encoding, and most receiving endpoints do not expect it.

One constraint to be aware of: Gravity Forms file uploads are stored in a publicly-accessible directory by default. If your forms contain sensitive files and you use the signed URL option, the URL in the entry may expire. Pass the URL immediately in the webhook payload rather than storing it for later use.

Webhook Actions handles file upload fields by including the URL string (or JSON array string for multi-file) in the payload. The field mapping UI treats file upload fields like any other field. If you need to transform the multi-file JSON array into a clean array structure, configure a custom field mapping that uses the merge tag for the field — the plugin normalizes the output automatically.

/ Reliability

What happens when the endpoint is down?

This is the critical reliability question for production Gravity Forms webhook integrations, and the answer depends entirely on how delivery is implemented.

With raw PHP wp_remote_post(): the delivery attempt is made inline during the form submission request. If the endpoint returns a 5xx, times out, or is unreachable, the error is silently discarded. There is no retry, no queue, and no recovery path. Every submission during the outage window is permanently lost.

With Webhook Actions: the payload is serialized into WA's own database queue at hook time, before any HTTP attempt is made. The queue row survives PHP crashes, server restarts, and WordPress cache flushes. When the endpoint comes back online, WA's queue runner resumes delivery from where it stopped.

The retry schedule uses exponential backoff. With a base delay of 60 seconds and a maximum of five attempts, a delivery that fails on attempt 1 is retried at 1 minute, 2 minutes, 4 minutes, 8 minutes, and 16 minutes — a total retry window of 31 minutes. For endpoints with longer outages, you can configure the maximum attempt count and backoff multiplier in the Webhook Actions settings.

After all retry attempts are exhausted, the job is marked as failed in the Webhook Actions delivery log. You can view the full attempt history — including the HTTP status code and response body for each attempt — and manually re-trigger delivery from the WordPress admin without modifying any code. See the Action Scheduler deep dive for a full explanation of how the queue runner, concurrency settings, and failure states work.

For critical integrations — CRM updates, payment notifications, or any downstream system where a missed event causes business impact — the difference between raw PHP and a queued delivery system is the difference between "sometimes works" and "reliably works."

/ FAQ

Common questions

Yes. gform_after_submission fires on every successful Gravity Forms submission, regardless of which form triggered it. You can narrow to a specific form using an if ( $form['id'] === 3 ) check inside the callback, or use the form-specific variant gform_after_submission_3 to avoid the conditional entirely.
No. gform_after_submission fires after the entry has already been saved to the database. To conditionally block a submission before it is recorded, use gform_pre_submission or gform_validation instead. gform_after_submission is strictly for post-processing — sending notifications, triggering integrations, dispatching webhooks.
gform_entry_created fires immediately after the entry row is inserted into the database, before Gravity Forms processes notifications and confirmations. gform_after_submission fires later in the same request, after all built-in processing is complete. For webhook dispatch, gform_after_submission is the safer choice because you have access to the fully processed $entry object with all field values resolved.
Use rgar($entry, $field_id) where $field_id is the numeric Gravity Forms field ID as a string. For example, rgar($entry, '1') returns the value of field 1. For multi-input fields like Name or Address, use dot notation: rgar($entry, '1.3') returns the first name component of a Name field with ID 1. The rgar() helper returns an empty string if the key does not exist, avoiding PHP notices.
The hook fires correctly in both environments — the delivery failure is almost always in the wp_remote_post call that follows. In production, the target endpoint may be slow, temporarily unavailable, or returning errors. Without a persistent queue and retry logic, any failure during the synchronous HTTP call is permanently lost.

The solution is to enqueue the payload at hook time and dispatch it asynchronously via a background job with retry — which is what Webhook Actions does with its own database queue, using Action Scheduler to trigger the runner when available. See Why WordPress Webhooks Silently Fail in Production for the full breakdown.