TL;DR
ivyforms/form/after_submissionfires after each successful IvyForms submission with four arguments:$formId,$submissionData,$formFields,$entryId- Submitted values are keyed by numeric field ID — join them against the
$formFieldsfield objects (getId()+ label getter) to get human-readable keys - Inline
wp_remote_postworks 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), andreferer$form_fields— an array ofIvyForms\Entity\Field\Fieldentity objects exposing each field definition through getters:getId(),getType(), and — viagetFieldGeneralSettings()—getLabel()andisRequired(). The submitted value is not on the object; you join it from$submission_databy ID$entry_id— the ID of the stored entry, ornullwhen 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.
| Scenario | Inline wp_remote_post | Queued async dispatch |
|---|---|---|
| Endpoint timeout (5s) | Submission lost permanently | Retried with exponential backoff |
| Endpoint 503 (server error) | Submission lost permanently | Retried up to 5 times (1m, 2m, 4m, 8m) |
| Endpoint down for 2 hours | All submissions during window lost | Queue drains when endpoint recovers |
| Delivery failure visibility | None | Per-attempt log with status codes |
| Visitor form experience | Blocks until endpoint responds | Instant — 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.