WP Webhooks / Blog / Architecture
Article · Architecture

The Complete ivyforms/form/after_submission Reference

ivyforms/form/after_submission documentation: hook parameters, numeric field IDs, a wp_remote_post code example, and reliable async webhook dispatch.

7 min 2026-07-17
#ivyforms#wordpress#hooks

TL;DR

  • ivyforms/form/after_submission fires after each successful IvyForms submission with four arguments: $formId, $submissionData, $formFields, $entryId
  • Submitted values are keyed by numeric field ID — join them against the $formFields field objects (getId() + label getter) to get human-readable keys
  • Inline wp_remote_post works in dev but loses events in production — any timeout or endpoint failure silently drops the submission; dispatch from a queue instead

/ The Hook

What does ivyforms/form/after_submission do?

ivyforms/form/after_submission is the action hook IvyForms fires after a form submission has passed validation and been processed. It is the correct place to trigger outbound integrations — webhooks, CRM syncs, notifications — that depend on the complete submitted data set.

IvyForms has no built-in webhook feature, so this hook is the extension point for sending submissions anywhere outside WordPress. By the time your callback runs you have the form ID, every submitted value, the full field definitions, and — when entry storage is enabled — the stored entry ID.

A companion hook, ivyforms/form/before_submission, fires earlier in the same request, before the submission is processed. For webhook dispatch, after_submission is the safer choice: the data is final and the entry (if stored) already exists.

/ Parameters

What parameters does the hook pass?

The hook passes four arguments. The add_action call must specify the priority (10 is the default) and the accepted argument count (4) for all of them to arrive in your callback.

Hook signature

add_action( 'ivyforms/form/after_submission', function( $form_id, $submission_data, $form_fields, $entry_id ) {
    // All four arguments are available here
}, 10, 4 );
  • $form_id — the numeric ID of the submitted form
  • $submission_data — an associative array of submitted values keyed by numeric field ID as string keys ('1', '2', '3'…), plus three metadata keys: formId, postId (the page the form was embedded on), and referer
  • $form_fields — an array of IvyForms\Entity\Field\Field entity objects exposing each field definition through getters: getId(), getType(), and — via getFieldGeneralSettings()getLabel() and isRequired(). The submitted value is not on the object; you join it from $submission_data by ID
  • $entry_id — the ID of the stored entry, or null when entry storage is disabled in IvyForms

The numeric keying is the detail that surprises most developers: $submission_data does not contain 'name' or 'email' keys. Field labels live only in $form_fields. IDs stay stable when labels are edited, which is why IvyForms keys by ID — but it means any downstream consumer either works with numeric keys or joins the two arrays first.

/ Basic Implementation

How do you send submissions to a webhook with raw PHP?

The minimal implementation hooks ivyforms/form/after_submission, builds a payload from the submitted values, and calls wp_remote_post(). This works in local development and for low-stakes forms where a dropped delivery is acceptable.

functions.php — minimal ivyforms webhook

add_action( 'ivyforms/form/after_submission', function( $form_id, $submission_data, $form_fields, $entry_id ) {

    $payload = [
        'form_id'  => $form_id,
        'entry_id' => $entry_id,
        'name'     => $submission_data['1'] ?? '',
        'email'    => $submission_data['2'] ?? '',
        'message'  => $submission_data['3'] ?? '',
    ];

    wp_remote_post( 'https://your-n8n-url/webhook/test', [
        '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, 4 );

The hard-coded field IDs ('1', '2', '3') are the fragile part — they map to whatever fields hold those IDs in the form builder. The next section shows how to avoid hard-coding them.

/ Field IDs

How do you map numeric field IDs to labels?

$form_fields exists precisely for this join. Each Field object describes one field — read its ID with getId() and its label through the general-settings accessor. Instead of hard-coding ID keys, iterate the definitions and build a payload keyed by label:

functions.php — payload keyed by field labels

add_action( 'ivyforms/form/after_submission', function( $form_id, $submission_data, $form_fields, $entry_id ) {

    $payload = [
        'form_id'  => $form_id,
        'entry_id' => $entry_id,
    ];

    foreach ( $form_fields as $field ) {
        // "Full Name" → "full_name"
        $label = $field->getFieldGeneralSettings()->getLabel();
        $key   = str_replace( '-', '_', sanitize_title( $label ) );

        // Submitted value lives in $submission_data, keyed by field ID
        $payload[ $key ] = $submission_data[ (string) $field->getId() ] ?? null;
    }

    wp_remote_post( 'https://your-n8n-url/webhook/test', [
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( $payload ),
        'timeout' => 10,
    ] );

}, 10, 4 );

For a contact form with Name (ID 1), Email (ID 2), and Message (ID 3), the join produces {"name": "…", "email": "…", "message": "…"} regardless of which IDs the form builder assigned. If a form is later edited and a field gets a new ID, the label-based payload stays stable — the numeric-key payload silently shifts meaning.

Two details worth noting. The submitted values are keyed by field ID as string keys, so cast getId() to string for the lookup. And sanitize_title() produces dash-separated slugs (full-name), so the str_replace turns them into the underscore keys most receivers expect.

The same numeric-ID structure appears when the submission leaves WordPress as a webhook: the raw payload carries $submission_data under args[1] and the field definitions under args[2] — already normalized to plain {id, label, type, required, value} objects with the submitted value merged in, so receivers like n8n can do the identical join on the other side. The IvyForms webhook example shows that payload end-to-end and a no-code way to flatten it with field mapping.

/ Production Reality

Why does the raw PHP approach break in production?

The raw implementation has three structural problems that only surface under production conditions.

1. Inline execution blocks the visitor. The hook runs synchronously inside the request that processed the form submission. wp_remote_post() holds the connection open until the endpoint responds or the timeout expires — a slow endpoint means a visibly slow form.

2. Timeouts and downtime permanently lose the submission. wp_remote_post() defaults to a 5-second timeout. An endpoint restart, a 5xx during a deployment, a rate limit during a campaign burst — each one drops the event with no retry and no recovery path.

3. The failure is invisible. WordPress does not log outbound HTTP calls by default. A failed delivery produces no admin error and no alert; you find out when someone asks why their message was never answered.

ScenarioInline wp_remote_postQueued async dispatch
Endpoint timeout (5s)Submission lost permanentlyRetried with exponential backoff
Endpoint 503 (server error)Submission lost permanentlyRetried up to 5 times (1m, 2m, 4m, 8m)
Endpoint down for 2 hoursAll submissions during window lostQueue drains when endpoint recovers
Delivery failure visibilityNonePer-attempt log with status codes
Visitor form experienceBlocks until endpoint respondsInstant — dispatch happens in background

The fix is the same as for every WordPress form plugin: record the event first, deliver it second. Enqueue the payload at hook time into a persistent queue, return the form response immediately, and let a background worker attempt delivery with retries and per-attempt logging. Why inline delivery fails silently — and what a production-grade queue looks like — is covered in Why WordPress Webhooks Silently Fail in Production and Why WP-Cron Is Not Enough for Production Webhook Delivery.

For the no-code version of this integration — trigger selection, field mapping, and an event log with replay — see the IvyForms webhook example.

FAQ

Common questions always ask.

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

What parameters does ivyforms/form/after_submission pass? +
The hook passes four arguments: $formId (the numeric form ID), $submissionData (an array of submitted values keyed by numeric field ID, plus formId, postId, and referer), $formFields (an array of IvyForms Field entity objects exposing getId(), getType(), and the label via getFieldGeneralSettings()->getLabel()), and $entryId (the stored entry ID, or null when entry storage is disabled). Register your callback with add_action('ivyforms/form/after_submission', $callback, 10, 4) so all four arrive.
How do I send IvyForms submissions to n8n? +
Hook into the ivyforms/form/after_submission action (4 args: $formId, $submissionData, $formFields, $entryId), then post the data to your n8n webhook URL using wp_remote_post(). Note that $submissionData keys are numeric field IDs — use $formFields to map them to labels. For reliable delivery with retries, dispatch from a persistent queue instead of inline, so failures are retried with exponential backoff rather than lost.
Why are IvyForms submission values keyed by numbers instead of field names? +
IvyForms stores submitted values under each field's numeric ID — $submissionData['1'], $submissionData['2'], and so on — because labels can change while IDs stay stable. The $formFields argument carries the matching Field objects — read the ID with getId() and the label with getFieldGeneralSettings()->getLabel() — so you can join by ID to produce a payload with human-readable keys.
Does ivyforms/form/after_submission fire when entry storage is disabled? +
Yes. The hook fires on every successful submission regardless of the entry storage setting. When entry storage is disabled in IvyForms, the fourth argument ($entryId) is null instead of a numeric entry ID — treat it as optional in your callback.
Does a webhook inside ivyforms/form/after_submission block the form submission? +
Yes, if you call wp_remote_post() directly. The hook runs synchronously inside the request that processed the submission, so PHP waits for the remote endpoint to respond (default timeout: 5 seconds) before the visitor sees the confirmation. Moving dispatch into a queued background job returns the response instantly and makes failures retryable.
Ready

Your next automation is
one sentence away.

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