TL;DR: Elementor can send form submissions to a webhook two ways, and both need Elementor Pro — the Form widget is a Pro feature.
- No code: add Webhook under Actions After Submit in the Form widget and paste your URL.
- With code: hook
elementor_pro/forms/new_record, which fires after the form actions have run and hands you the record. - The code path is what you need when the endpoint expects a specific payload shape or an authentication header.
- Either way, a submission sent inline blocks the visitor's request and is lost if the endpoint is down.
/ Overview
Can Elementor send form submissions to a webhook?
Yes — a Webhook action is built into the Form widget, no code required. It is one of the default form actions Elementor Pro ships, alongside Email, Redirect, Collect Submissions, Slack, Discord and a set of CRM integrations.
The prerequisite catches people out: the Form widget itself is part of Elementor Pro and is not available in the free plugin. If you are on free Elementor there is no Webhook action to configure, because there is no Elementor form to attach it to. Everything below assumes Pro.
/ Built-in action
Where is the built-in Webhook action?
In the Form widget's Content tab, under Actions After Submit. Add Webhook to the action list and a Webhook panel appears below, where you paste the destination URL. On submission Elementor posts the form data to that URL.
By default each field is sent using its label as the field name, which is convenient in a no-code tool and fragile everywhere else — renaming a label in the editor renames the key in your payload and quietly breaks the consumer. Give each field an explicit Custom ID in the field's Advanced settings and use that as the stable key instead.
That is the whole configuration surface, and its limits are the reason the code path exists. You get one URL and Elementor's payload shape. There is no place to add an Authorization header, no way to rename or nest fields to match an API contract, no signature, and no visibility into whether the request succeeded.
/ The hook
How does elementor_pro/forms/new_record work?
It is an action that fires after the configured form actions have run, and it receives the submitted record plus the AJAX handler. Elementor's developer documentation gives the signature as two parameters:
| Parameter | Type | What it holds |
|---|---|---|
$record | Form_Record | The submitted form: fields, settings, uploaded files and meta. |
$ajax_handler | Ajax_Handler | The response handler — used to add success or error messages back to the visitor. |
Because it runs after the actions, anything the built-in Webhook action was going to send has already gone out by the time your callback runs. Use new_record when you want to send your own request; use it alongside the built-in action only if you genuinely want two deliveries.
Two related hooks are worth knowing. elementor_pro/forms/validation runs before processing and is where you reject a submission. elementor_pro/forms/process fires after fields are validated and processed. Both take the same $record and $ajax_handler pair, and both have per-field-type variants — elementor_pro/forms/validation/{$field_type} and elementor_pro/forms/process/{$field_type} — for targeting a single kind of field.
elementor_pro/forms/new_record fires after the form actions have run — so it is a delivery hook, not a validation hook. — Elementor developer documentation
/ Implementation
How do you post a submission to your own endpoint?
Read the form name to make sure you are handling the right form, pull the fields off the record, and send them. The form-name guard is not optional — without it your callback fires for every Elementor form on the site.
PHP — send one Elementor form to a custom endpoint
add_action( 'elementor_pro/forms/new_record', function( $record, $ajax_handler ) { // Only handle the form named "Contact" in the editor. if ( 'Contact' !== $record->get_form_settings( 'form_name' ) ) { return; } // Raw fields, keyed by each field's Custom ID. $raw = $record->get( 'fields' ); $payload = []; foreach ( $raw as $id => $field ) { $payload[ $id ] = $field['value']; } wp_remote_post( 'https://example.com/hooks/elementor', [ 'timeout' => 5, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . get_option( 'my_endpoint_token' ), ], 'body' => wp_json_encode( [ 'source' => 'elementor', 'form' => $record->get_form_settings( 'form_name' ), 'submitted' => gmdate( 'c' ), 'fields' => $payload, ] ), ] ); }, 10, 2 );
Each entry in $record->get('fields') is an array, not a bare string — value holds what the visitor typed, and title holds the field label. Reading ['value'] explicitly is what keeps the payload clean. Note the header block: this is the capability the built-in action does not have, and usually the reason people move to code in the first place.
Uploaded files arrive as URLs rather than binary content, so a file field's value is a link into the uploads directory. If your endpoint needs the file itself, fetch it from that URL — and remember the URL is public unless you have restricted upload access.
/ Reliability
Why does a synchronous request hurt your form?
Because it runs inside the visitor's submission request. wp_remote_post() is blocking: with the five-second timeout above, a slow endpoint adds five seconds to every submission before the visitor sees a confirmation. Drop the timeout and you cut off legitimate slow responses; raise it and a degraded endpoint stalls your form.
Worse is what happens on failure. The visitor's submission succeeded, Elementor showed a success message, and your delivery threw a connection error into the void. Nothing retries it. Nobody is told. This is the standard failure mode for form webhooks and it is almost always silent.
| Concern | wp_remote_post inside the hook | Queued delivery |
|---|---|---|
| Submission speed | Visitor waits for your endpoint | Returns immediately — job runs after |
| Endpoint down | Delivery lost, no record | Job stays queued and retries |
| Slow endpoint | Adds latency to every submission | No effect on the form |
| Visibility | Failure is invisible unless you log it | Per-attempt status is inspectable |
| Traffic spikes | Each submission opens its own request | Work is batched by the queue runner |
The fix is to enqueue rather than send. Capture the payload in the hook, hand it to a background job, and let a queue runner deliver it with retries and exponential backoff. Action Scheduler is the usual choice on WordPress because it gives each delivery its own database row and retry state — the same pattern described in the WordPress job queue guide.
/ Choosing
Which approach should you use?
Start with the built-in action, and move to the hook when you hit one of its walls. Concretely:
- Use the built-in Webhook action when the destination accepts whatever you send it — an automation platform's catch-hook node, an internal script, a test endpoint. Set Custom IDs on your fields first.
- Use
elementor_pro/forms/new_recordwhen the endpoint needs an authentication header, a specific JSON shape, nested objects, or fields renamed to match an API contract. - Queue the delivery as soon as the submission matters commercially. A lost contact form entry is an annoyance; a lost order or lead is revenue.
The same trade-off shows up in every form plugin — the built-in option is fastest to set up, the hook gives control, and neither gives you retries on its own. For the equivalent hooks in other form plugins, see the gform_after_submission reference and the wpcf7_mail_sent guide.