---
title: "WPForms Webhook: Send Form Entries to Any Endpoint"
description: "WPForms webhook setup: the Elite-only Webhooks addon, and the free wpforms_process_complete hook for when you need your own payload and headers."
url: "https://wpwebhooks.org/blog/wpforms-webhook/"
date: "2026-08-06"
---

# WPForms Webhook: Send Form Entries to Any Endpoint

**TL;DR:** WPForms can send an entry to a webhook two ways, and they sit on opposite sides of a paywall.

-   **No code:** the _Webhooks_ addon adds a request panel to the form builder — but it needs an **Elite** licence.
-   **With code:** hook `wpforms_process_complete`, which fires at the very end of a successful submission and works on any tier, including Lite.
-   The hook hands you `$fields`, `$entry`, `$form_data` and `$entry_id` — everything needed to build your own payload.
-   `$entry_id` is `0` on Lite or with entry storage off, so never use it as your idempotency key.
-   Either route sends the request inline unless you queue it, so a slow endpoint becomes slow form submissions.

/ Overview

## Does WPForms have a **webhook**?

Yes, and there are two entirely separate routes to one. The first is the Webhooks addon, which puts a request builder in the form editor and needs no PHP. The second is `wpforms_process_complete`, an action WPForms fires at the end of every successful submission, which you handle in a snippet.

Which one you can use is mostly a licensing question. WPForms documents the Webhooks addon as requiring an [Elite licence level](https://wpforms.com/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/), the top tier — Basic, Plus and Pro do not include it. The action hook is part of the plugin's own processing code, so it is available on every tier including the free WPForms Lite.

That makes the decision unusually clear compared with other form plugins. If you are already on Elite and your endpoint is happy with the payload WPForms produces, use the addon. In every other case the hook is the route, and it is not much work.

FIG 01 — Two routes from a WPForms entry to your endpoint

/ Built-in addon

## What does the **Webhooks addon** actually send?

Whatever you tell it to, within a fixed set of options. The addon exposes five request methods — GET, POST, PUT, PATCH and DELETE — and two body formats: JSON, sent as `application/json`, or form-encoded, sent as `application/x-www-form-urlencoded`. You map form fields to request keys in the builder rather than accepting a fixed payload shape, which is the main thing it does better than the built-in webhook actions in most competing form plugins.

Two options matter for talking to a real API. Custom request headers let you pass an API key, which is the difference between a webhook you can point at a service and one you can only point at a catch-all URL. A Secret field generates a per-request hash sent as a header, so the receiver can confirm the request came from your site rather than anyone who learned the URL.

The limits are the shape of the payload and the shape of the failure. You get a flat mapping of fields to keys, so an endpoint expecting nested objects or a computed value needs a translation layer somewhere. And a request that fails is a request that failed — the addon is a sender, not a delivery system with its own retry schedule.

/ The hook

## What is **wpforms\_process\_complete**?

It is the action that runs at the very end of successful entry processing. WPForms documents the [signature](https://wpforms.com/developers/wpforms_process_complete/) as four parameters:

| Parameter | Type | What it holds |
| --- | --- | --- |
| `$fields` | `array` | Sanitised entry field values and properties, keyed by field ID. |
| `$entry` | `array` | The original $\_POST payload for the submission. |
| `$form_data` | `array` | The processed form settings — including the form ID and title. |
| `$entry_id` | `int` | The saved entry ID, or 0 when entry storage is off or the site runs Lite. |

The timing is the important part. It fires only when the submission passed validation, and only after the entry has been written and notification emails have gone out. So by the time your callback runs, the entry is real and the user is already being redirected or shown the confirmation. Nothing you do here can reject the submission — for that you want the earlier [wpforms\_process](https://wpforms.com/developers/wpforms_process/) action, which is where validation errors belong.

> wpforms\_process\_complete runs at the very end of successful form entry processing — after the entry is saved and the notifications are sent. — WPForms developer documentation

/ Implementation

## How do you post a WPForms entry to your own **API**?

Bind a callback, guard it to one form, build the body you want, and send it with [wp\_remote\_post](https://developer.wordpress.org/reference/functions/wp_remote_post/). The form guard is not optional — without it every form on the site posts to your endpoint, including the contact form you forgot about.

PHP — send a WPForms entry to an endpoint

```
add_action( 'wpforms_process_complete', function( $fields, $entry, $form_data, $entry_id ) {

    // Only the form we care about.
    if ( 42 !== (int) $form_data['id'] ) {
        return;
    }

    // $fields is keyed by field ID; 'name' is the label, 'value' the answer.
    $payload = [
        'form'     => $form_data['settings']['form_title'],
        'entry_id' => $entry_id ?: null,
        'answers'  => wp_list_pluck( $fields, 'value', 'name' ),
    ];

    wp_remote_post( 'https://example.com/hooks/wpforms', [
        'timeout'  => 5,
        'headers'  => [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . getenv( 'HOOK_TOKEN' ),
        ],
        'body'     => wp_json_encode( $payload ),
        'blocking' => true,
    ] );

}, 10, 4 );
```

Two details in there are easy to get wrong. The `4` at the end is the argument count — omit it and PHP hands your callback one parameter, so `$form_data` is undefined and the guard fails. And `$fields` is keyed by numeric field ID, not by label, which is why the example flattens it with `wp_list_pluck` before sending. Field IDs are stable across label edits; that is a feature, not an inconvenience.

/ Entry IDs

## Why does **$entry\_id** come back as 0?

Because nothing was saved. WPForms returns `0` when entry storage is disabled for that form or when the site is running WPForms Lite, which does not store entries at all. The submission still processed and your callback still ran — there is simply no row to point at.

This matters more than it looks. A receiving endpoint usually wants an idempotency key so a replayed request does not create a duplicate record downstream, and `$entry_id` is the obvious candidate. On a Lite site every request would arrive with the same key of `0`, and a well-behaved consumer would discard all but the first. Generate your own identifier instead — a UUID minted in the callback, or a hash of the form ID and the submission timestamp — and treat `$entry_id` as a convenience link back to wp-admin when it happens to be non-zero.

/ Delivery

## What happens when the endpoint is **slow or down**?

The visitor waits, and then the data is gone. A blocking `wp_remote_post` inside the hook adds the endpoint's full response time to the submission, and the default WordPress HTTP timeout is 5 seconds. A receiver having a bad afternoon at 4 seconds per request means every person filling in your form waits an extra 4 seconds after pressing submit for no visible reason.

The failure mode is worse than the latency. If the request times out or returns a 500, the visitor still sees the success message — the entry saved fine, only the delivery failed — and nothing anywhere records that the lead never reached your CRM. This is the same trap that catches [Elementor form deliveries](https://wpwebhooks.org/blog/elementor-forms-webhook/) and [Contact Form 7 submissions](https://wpwebhooks.org/blog/wordpress-wpcf7-mail-sent-hook/), and the fix is the same in all three: do not send in the request, enqueue.

| Behaviour | wp\_remote\_post in the hook | Queued delivery |
| --- | --- | --- |
| Submission time | Visitor waits for the endpoint | Returns immediately |
| Endpoint down | Request lost, no record | Stays queued, retried later |
| Retries | None | Exponential backoff until a cap |
| Visibility | Nothing to inspect | Per-attempt log with request and response |

/ Debugging

## How do you debug a webhook that **never arrives**?

Work outwards from the hook. Most of these turn out to be the guard or the argument count rather than anything to do with HTTP.

1.  **Confirm the hook runs at all.** Drop an `error_log( 'process_complete fired' )` as the first line of the callback and submit the form. No line in the log means the submission failed validation, or the snippet is not loaded.
2.  **Confirm the form guard matches.** Log `$form_data['id']` and compare it with the ID in the form's edit URL. Comparing a string to an integer without a cast is the classic version of this bug.
3.  **Check the argument count.** If `$form_data` is `null`, the fourth argument to `add_action` is missing or wrong.
4.  **Log the response, not just the request.** `wp_remote_post` returns either a `WP_Error` or an array — log `wp_remote_retrieve_response_code()` so a 401 is not mistaken for a request that never left.
5.  **Rule out the outbound connection.** A host that blocks external HTTP, or a firewall that drops the destination, produces a `WP_Error` with a connection message and no server-side trace at the receiver.

If step 5 is where it fails, that is an infrastructure problem, not a code one, and no amount of rewriting the callback will fix it.

/Footnotes

¹ Signature and parameter descriptions from the WPForms developer reference for [wpforms\_process\_complete](https://wpforms.com/developers/wpforms_process_complete/).

² Licence requirement, request methods, body formats, custom headers and the Secret option from the [WPForms Webhooks addon documentation](https://wpforms.com/docs/how-to-install-and-use-the-webhooks-addon-with-wpforms/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WPForms Webhook: Send Form Entries to Any Endpoint","description":"WPForms webhook setup: the Elite-only Webhooks addon, and the free wpforms_process_complete hook for when you need your own payload and headers.","datePublished":"2026-08-06","dateModified":"2026-08-06","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/wpforms-webhook/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/og_image.jpg","width":1200,"height":630,"caption":"WPForms Webhook: Send Form Entries to Any Endpoint"},"keywords":["wpforms webhooks","wpforms webhook addon","wpforms process complete","wpforms form submission hook","wordpress form webhook","wpforms send data to api","wpforms elite addon","wpforms entry hook"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WPForms Webhook: Send Form Entries to Any Endpoint","item":"https://wpwebhooks.org/blog/wpforms-webhook/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Does WPForms have a webhook?","acceptedAnswer":{"@type":"Answer","text":"Yes, two ways. The Webhooks addon adds a no-code webhook panel to the form builder, but it requires an Elite licence. On any tier you can hook wpforms_process_complete in PHP and send the request yourself, which is also the only route that lets you shape the payload exactly."}},{"@type":"Question","name":"Which WPForms licence do I need for webhooks?","acceptedAnswer":{"@type":"Answer","text":"Elite. WPForms documents the Webhooks addon as an Elite-level addon, so Lite, Basic, Plus and Pro licences do not have it. The wpforms_process_complete action is part of the plugin itself rather than an addon, so a code-based webhook works on any tier including WPForms Lite."}},{"@type":"Question","name":"What is wpforms_process_complete?","acceptedAnswer":{"@type":"Answer","text":"It is the action WPForms fires at the very end of a successful entry submission, after the entry has been saved and notification emails have been sent. Its signature is do_action('wpforms_process_complete', $fields, $entry, $form_data, $entry_id), and it does not fire when validation fails."}},{"@type":"Question","name":"Why is $entry_id always 0 in wpforms_process_complete?","acceptedAnswer":{"@type":"Answer","text":"Because entry storage is off. WPForms documents $entry_id as returning 0 when entry storage is disabled or the site is running WPForms Lite. Use it for a link back to the entry when it is non-zero, but never make it the idempotency key your endpoint deduplicates on."}},{"@type":"Question","name":"What request methods and formats does the WPForms Webhooks addon support?","acceptedAnswer":{"@type":"Answer","text":"GET, POST, PUT, PATCH and DELETE, with the body sent either as JSON (application/json) or as form-encoded data (application/x-www-form-urlencoded). You can add custom request headers for an API key, and a Secret option adds a per-request hash header the receiver can check."}}]}
```
