One Plugin, Every Client Stack: Building a Universal WordPress Webhook Adapter

php dev.to

A client asked me for something simple a while back: send a Telegram message every time someone fills out their contact form. Another client had a HubSpot connection that wasn't behaving, so we ended up routing everything through Zapier first, then into HubSpot from there — easier to debug that way anyway. A third wanted Slack pinged on WooCommerce events, different channels depending on whether it was a new order, a refund, or a cancellation.

Three clients, three completely different destinations, but the same underlying job every time: something happens on WordPress, and it needs to go somewhere else automatically. I was rebuilding this by hand often enough that it stopped making sense not to build it properly once.

Most plugins that do this kind of thing hard-code support for specific plugins — CF7 today, WPForms next month, LearnDash whenever someone asks enough times. You're always one release behind whatever your client happens to be running. I wanted something that didn't have that ceiling.

Dedicated adapters where the payload actually needs cleanup. Contact Form 7, WPForms, Fluent Forms, Ninja Forms, Formidable Forms, and WooCommerce each get their own adapter. These plugins pass messy native objects full of nonces, reCAPTCHA tokens, and internal metadata mixed in with the real submission data — without cleanup, the payload is basically unusable. Each adapter strips that noise and hands back clean, labeled fields.

A generic adapter for literally everything else. This is the part that actually solves the ceiling problem. Instead of writing a new adapter every time a client uses some plugin I've never heard of, there's one listener:

add_action( $hook_name, function() {
    $args    = func_get_args();           // catch all arguments
    $payload = $this->serialize( $args ); // normalize to array
    $this->dispatch( $payload );          // send to webhook
}, 10, PHP_INT_MAX ); // accept any number of arguments
Enter fullscreen mode Exit fullscreen mode

PHP_INT_MAX as the accepted-argument count means it captures every argument a hook fires, no matter which plugin fires it. Point it at learndash_course_completed, a membership plugin's renewal hook, or something from a plugin that doesn't exist yet — it just works.

Raw hook arguments aren't that useful on their own, though. user_id = 42 doesn't tell you much by itself. So there's a serialization layer that flattens known WordPress types (WP_Post, WP_User, WP_Term, anything exposing get_data()) into safe common fields, and — the part I like most — attaches a companion expanded object for documented ID arguments. user_id gets a full userobject with email, login, and roles attached automatically. Undocumented integer arguments even get a best-effort resolve against posts/users/terms/orders, so an unlabeled hook argument usually still returns something usable instead of a bare number.

There's also a small data-only dictionary of known WordPress core hooks with pre-documented field names, purely for UI discoverability — no logic in it, just labels so the dropdown makes sense.

End result: the handful of plugins that genuinely need clean-payload treatment get it, and everything else — any LMS, any membership tool, any custom hook, anything built after this ships — is covered without writing new code for it.

It's free and live on WordPress.org if you want to see it in action.

Source: dev.to

arrow_back Back to Tutorials