This page covers what an endpoint receives from the PRO Webhook action, the runtime rules the request runs under, and the filters that adjust them. For configuring the action itself, see Webhook Action.
Automations are available in Elzo Forms PRO.
What your endpoint receives
For POST, PUT, and PATCH, the payload is a JSON object in the request body, sent with Content-Type: application/json unless the action supplies its own. For GET there is no body — the same data is appended as query parameters, with array values JSON-encoded into a single parameter.
In All fields payload mode, keys are the submitted fields’ Field Keys. In Custom mapping mode, keys are the target names configured on the action. Both modes append three keys:
{
"email": "jane@example.com",
"customer_type": "Business",
"elzo_forms_form_id": 42,
"elzo_forms_form_name": "Contact Form",
"elzo_forms_form_slug": "contact-form"
}
Field Keys are editable per form, so treat them as configuration rather than a stable contract. Validate the payload shape on arrival and fail loudly when a key you require is missing.
There is no request signature
Elzo Forms does not sign webhook requests. There is no HMAC header and no shared secret negotiated with the endpoint. Authentication is whatever the action’s configured headers carry, so an endpoint must authenticate the caller itself:
- require a bearer token or API key sent as a header from the action;
- reject requests without it, rather than treating an unknown caller as trusted;
- treat the payload as untrusted input and validate every value before use.
Header values are stored with the form or in site settings, so any user who can edit forms can read them. Issue a token scoped to this integration alone, and rotate it independently of anything else.
Delivery guarantees
There are none worth relying on. Concretely:
- Each matching action sends exactly one request. A failed request is not retried.
- The request runs synchronously during the visitor’s submission, not from a queue.
- The response body is discarded. Only the status code and method reach later actions.
- A request that times out may still have been processed by the endpoint — Elzo Forms records that it could not confirm the outcome, and does not retry.
Design the endpoint accordingly: make writes idempotent where a duplicate would cause harm, and do slow work in your own background job rather than holding the connection open. An endpoint that takes longer than 10 seconds to answer will always be recorded as failed, whatever it eventually does.
Runtime limits
Requests go through a guarded HTTP client, not straight to wp_remote_request(). These limits apply per submission across every automation:
| Limit key | Default | Hard ceiling |
|---|---|---|
max_http_requests |
10 | 50 |
max_http_timeout_seconds |
10 | 20 |
max_http_request_body_bytes |
512 KB | 4 MB |
max_http_response_bytes |
1 MB | 4 MB |
max_http_redirects |
3 | 10 |
max_http_headers |
50 | 100 |
max_runtime_seconds |
30 | 60 |
The client also enforces rules that are not configurable: TLS verification is always on, only http and https are allowed, redirects that leave the original origin are blocked, and hosts resolving to loopback or private addresses are refused. A small set of transport headers — Host, Content-Length, Transfer-Encoding, Connection, Proxy-Authorization, Proxy-Connection, Upgrade, Trailer, TE, and Expect — cannot be set.
elzo_forms_pro_automation_runtime_limits
Arguments: $defaults, $form, $source, where $source is runtime or test_runner.
<?php
add_filter( 'elzo_forms_pro_automation_runtime_limits', function ( array $limits, $form, string $source ): array {
if ( 42 === (int) $form->get_id() ) {
$limits['max_http_timeout_seconds'] = 20;
$limits['max_http_requests'] = 25;
}
return $limits;
}, 10, 3 );
Values above the hard ceiling are clamped, and a value of zero or below falls back to the default. Raising the timeout raises how long a visitor waits for their own submission, so raise it only for the form that needs it.
Other security filters
| Filter | Arguments | Default |
|---|---|---|
elzo_forms_pro_automation_http_allowed_ports |
$ports |
[80, 443] |
elzo_forms_pro_automation_http_allow_cross_origin_redirect |
$allowed, $source_origin, $target_origin, $source |
false |
elzo_forms_pro_automation_url_guard_resolved_ips |
$ips, $host |
null — normal DNS resolution |
Warning: these filters relax protections against server-side request forgery. Allowing an extra port, a cross-origin redirect, or a fixed IP resolution widens what a form editor can make the server reach. Scope each one to a specific known host:
<?php
add_filter(
'elzo_forms_pro_automation_http_allow_cross_origin_redirect',
function ( $allowed, $source_origin, $target_origin, $source ) {
return 'https://api.example.com' === $target_origin ? true : $allowed;
},
10,
4
);
Diagnostics
Failures are recorded under Health & History with a stable code:
| Code | Cause |
|---|---|
webhook_request_failed |
The transport could not complete the request, including a timeout. |
webhook_unexpected_status |
The endpoint replied outside the configured success range. |
invalid_webhook_status_range |
The configured success range is not a valid 100–599 range. |
automation_http_unsafe_url |
The host, port, or resolved address was blocked. |
The action also returns invalid_webhook_url, automation_http_redirect_blocked, automation_http_request_too_large, automation_http_response_too_large, automation_http_timeout, and automation_http_limit_exceeded. These are not entries in the diagnostic catalog, so Health & History displays them as Unknown Automation failure. The specific code is still available to a PHP callback through the action result, which is the reliable way to distinguish them.
Execution history stores codes, statuses, and timing — never payloads or header values. Add your own redaction keys with elzo_forms_pro_automation_trace_sensitive_keys, and register titles and recommendations for custom codes with elzo_forms_pro_automation_diagnostic_catalog_entries.
To observe deliveries from PHP, use the lifecycle actions:
<?php
add_action( 'elzo_forms_pro_automation_after_action', function ( $automation, $action_payload, $context, $action_result ) {
if ( 'webhook' !== ( $action_payload['type'] ?? '' ) || 'failed' !== $action_result->get_status() ) {
return;
}
my_plugin_record_webhook_failure(
$action_result->get_error()['code'] ?? '',
$action_result->get_outputs()['status_code'] ?? 0
);
}, 10, 4 );
An exception thrown from an after_ hook is caught and recorded rather than surfaced, so failures inside your callback will not be obvious. Keep the work small and log your own errors.
Calling HTTP from a custom action
A custom automation action must use the context’s HTTP client so that the same limits, URL checks, and budget accounting apply:
$response = $context->get_http_client()->request( $url, [
'method' => 'POST',
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( $payload ),
'timeout' => 10,
] );
Only method, headers, body, and timeout are accepted; any other argument fails the request. The client throws AutomationRuntimeLimitExceeded when a limit is crossed, which your action should catch and convert into a failed ActionResult. Check $context->is_test() first so a configuration test does not make a live call.
Next steps
- Webhook Action — the settings this page’s limits apply to.
- PHP Hooks and Filters Reference — the automation extension API.
- Automation Health and Troubleshooting — where these codes are shown.