Sending a webhook when you publish. (Pro)
Parts 1 and 2 cover bringing information in. This part covers sending it out — the moment a post, page or custom post type is published — so an automation platform can react. Everything here needs the Pro add-on.
Setting it up · Testing it first · What is sent · Custom post types · Custom fields & ACF · Proving it came from you · If delivery fails
← Part 1: getting started · Part 2: reference & troubleshooting
Sending a webhook when a post is published (Pro)
This is the reverse of the sections above. Those bring information in and turn it into a post. This sends a webhook out, the moment a post is published — so something else, such as an automation platform that posts to WhatsApp or Slack, can react.
It fires for any post, page or custom post type becoming published — whether created by this plugin’s own publishing, above, or written normally in the WordPress editor.
Setting it up
- Go to Deftmind Hookwright → Publishing.
- Scroll to Post publish webhooks. There is a block for every post type on your site.
- Tick Send a webhook the first time a post of this type is published.
- Enter the Webhook URL.
- Under Send, tick at least one item.
- Click Save Changes.
Important
This must be turned on for a post type before the per-post option appears. Until then there is nothing to override, so the box will not show up. Turn it on here first.
Testing it before you publish anything
Beside the address there is a Test webhook button. Press it and one real sample request goes to that address straight away, and you are shown three things:
- The exact body it sent, so you can build the receiving end against real data rather than guessing.
- The signature it used, if you have set a signing secret — which lets you prove your signature check works before a real post depends on it.
- Whatever your endpoint replied, including the status code and how long it took.
It uses whatever is currently typed on the screen, even if you have not saved yet, so you can try an address before committing to it. It creates no post and changes nothing on your site.
The sample looks exactly like a real message with one addition: "test": true at the top. Your automation can use that to ignore test messages, or to branch to a safe path while you are still building.
What kind of request is it?
An HTTP POST, with a JSON body and Content-Type: application/json — the same as every real send. Whatever is receiving it must accept POST. A reply of 405 usually means it does not.
Overriding it for one post
- Use the site default for this post type — the normal behaviour.
- Send this post to a different URL — administrators only.
- Do not send this post — opts just this one post out.
When it fires
Once, the moment a post is first published. Editing an already-published post does not send it again, and neither does un-publishing and re-publishing it.
What is sent
Whichever fields you ticked, plus three things that are always included: the post’s web address (permalink), its numeric id (post_id) and its type (post_type).
{
"post_id": 123,
"post_type": "post",
"permalink": "https://example.com/my-new-article/",
"title": "My new article",
"excerpt": "A short summary.",
"body": "<p>The full article, as HTML.</p>",
"categories": ["News", "Health"],
"tags": ["research"],
"featured_image": "https://example.com/photo.jpg",
"meta": {
"event_price": "GBP 49.99",
"event_date": "2026-09-15"
}
}
Custom post types
Everything works the same for a custom post type as for an ordinary post. Each one gets its own block on the Publishing screen, with its own address and its own choice of fields — so Products can go one place and Events somewhere else. You do not have to register anything: as soon as a post type exists on your site, it appears by itself.
Custom fields, including ACF
The most important thing to understand first
The Custom field names box does not create anything. It only reads.
Typing price in that box does not make a price field. It says: “when you send the webhook, also go and look up the field called price on this post, and include whatever you find there.” So the field must already exist on your site, made by something else, and it must already have a value on the post being published.
Think of it as a shopping list you hand to somebody going to your storeroom. You write “shelf 4, shelf 9” on the list. They go and check those shelves and bring back whatever is on them. If shelf 4 does not exist, they come back without it — they do not build a shelf. If shelf 4 exists but is empty, they come back without it — they do not invent contents. And if you wrote the shelf’s nickname instead of its real number, they cannot find it at all.
The plugin is the person with the list. Your custom fields are the shelves. You stock the shelves elsewhere; this box only says which ones to check.
So it is always two separate jobs
| Where you do it | What you do | |
|---|---|---|
| Job 1 | In ACF, or whatever created the field | Create the field, and put a value in it on the post |
| Job 2 | In the Custom field names box | Write the field’s name, so it travels with the webhook |
Almost all confusion here comes from expecting Job 2 to also do Job 1. It does not.
Name, not label — the thing that catches people
In ACF you see two things side by side for every field:
| What it is | Example | |
|---|---|---|
| Label | What you see while editing a post. Written for humans. | Event Price |
| Name | The internal one. Lowercase, usually with underscores. | event_price |
Only the name works here. Find it in ACF → Field Groups → your group, in the Field Name column. Capital letters matter too — Event_Price and event_price are treated as two different fields, and normally only one of them exists.
Nothing warns you when it is wrong
If you get it wrong, nothing complains. There is no error and no warning — the field simply does not appear in the message. That silence is deliberate, because a post with no price should not send an empty price. But it does mean these four situations all look identical from the outside:
- The field does not exist anywhere on your site
- The field exists, but is empty on this particular post
- You typed the label instead of the name
- You got the capitalisation wrong
When custom fields “are not coming through”, it is nearly always one of those four, and checking the Field Name column settles it in seconds.
What the Test webhook button shows you
Pressing Test webhook shows something like "price": "Example value for price" — a placeholder, not a real value from a real post. That is on purpose: the test confirms your field names are wired up correctly without needing to publish anything. To confirm real values flow, publish a real post with those fields filled in.
You do not need ACF
Worth saying plainly, because people often assume otherwise. This works with any custom field, whatever created it — WooCommerce, an events plugin, a membership plugin, your theme, or WordPress’s own built-in Custom Fields panel. ACF is simply the most popular way to create them, and does not need to be installed for this to work.
Setting it up
Custom fields are extra pieces of information you add yourself — a price, an event date, a rating. Many sites create them with Advanced Custom Fields (ACF). To include them:
- Find the field name — not the label you see on screen. In ACF, open ACF → Field Groups, click your group, and read the Field Name column: something like
event_price, not “Event Price”. - Tick Custom fields under Send.
- In Custom field names, type those names, one per line.
- Save.
- Use the field name, not the label. Capital letters matter.
- Empty fields are left out completely rather than sent blank, so check a field is present before using it.
- You do not need ACF. Any custom field works, whatever created it.
Proving the message really came from your site
This is optional. Leave the Signing secret box empty and everything still works exactly as described above.
What it solves. Your webhook address is like a secret door. If somebody else learns it — from a screenshot, a log file, a shared document — they could send fake messages that the receiving end cannot tell from yours. Signing fixes that, and also proves nothing was altered on the way.
How it works. You create one long random password, the signing secret, and give a copy to both sides. Before sending, your site mixes that secret with the exact message and produces a short fingerprint, which travels in a header called X-Hookwright-Signature. The receiving end repeats the same mixing and compares. Match means genuine and unaltered. The secret itself is never sent — only the fingerprint, which cannot be turned back into it. This is HMAC-SHA256, the same method Stripe and GitHub use.
Creating it — you do not invent it yourself. Click Generate, then Copy, then Save Changes, and paste the value into your automation platform. You never need to read or understand it. To stop signing, empty the box and save.
Two long strings, doing two different jobs
This is the mistake almost everyone makes at least once, because both are long strings of letters and numbers and look interchangeable. They are not:
| What it is | Where you get it | Changes? | |
|---|---|---|---|
| The signing secret | A shared password only your site and your workflow know | You copy it directly from the Signing secret box | No — the same every time |
| The signature | A fingerprint of one particular message | Arrives with each request, in the X-Hookwright-Signature header | Yes — different every message |
The secret is copied and pasted straight across. Nothing is calculated, nothing is derived, and it never comes out of the incoming request. Whatever is shown in the Signing secret box is exactly what you paste into your automation platform.
The classic mistake
Putting the signature from the header into the box where the secret belongs. It looks right — it is a long value that arrived with the request, so it feels like “the thing you check with” — but it is the opposite. The header is the answer you are checking against. The secret is the key you use to work that answer out.
If you make that mistake, everything looks correct and the check simply always fails. That is the system protecting you: whoever sends the request chooses what goes in that header, so if the header were used as the key, anyone could invent a message, sign it with their own key, and have it accepted — the check would approve every forgery. A signature is only worth anything when the key is a secret the sender cannot pick.
Whatever tool you use, three things get filled in, and they mean the same thing even when the labels differ:
| The field is called something like | What belongs in it |
|---|---|
| Secret, or Key | The signing secret, pasted directly from this plugin |
| Value, Data, or Message | The raw request body, exactly as it arrived |
| — compare the result against — | The X-Hookwright-Signature header |
Once it works, move the secret out of the workflow itself and into whatever your platform uses for credentials or environment variables, so it is not sitting in plain view.
Checking it on the receiving side. Whatever you use, the three steps are the same:
- Take the raw body of the request — the message exactly as it arrived, as text.
- Calculate an HMAC-SHA256 of it, using your secret as the key, asking for hexadecimal output.
- Compare
sha256=followed by that result against the header. Accept only if identical.
In n8n, this exact configuration is known to work: Webhook node — Options → Raw Body on. Crypto node — Action Hmac; Binary File on with Binary Property Name data (with Raw Body enabled n8n hands the body over as binary, and this is how you reach it); Type SHA256; Property Name computedSignature; Secret: your signing secret pasted in as plain text; Encoding HEX. IF node — compare {{ 'sha256=' + $json.computedSignature }} against the header with String → is equal to. In Make, Zapier, Pipedream or your own code: find whatever the platform calls HMAC, choose SHA256 and hex, and compare. In code it is one line — crypto.createHmac('sha256', secret).update(rawBody).digest('hex') in Node.js, or hash_hmac('sha256', $rawBody, $secret) in PHP.
The one mistake that catches everybody
Use the body exactly as it arrived, character for character. If your platform reads the message as data and then writes it back out as text, the result looks the same but is subtly different underneath — web addresses and accented characters get written another way. The fingerprints will then never match, however correct everything else is. That is why n8n needs “Raw Body” switched on, and other platforms have an option called “raw”, “original” or “unparsed”. If signatures never match and you cannot see why, this is almost always it.
Keep the secret private. Anyone holding it can produce messages that look genuine. If it leaks, click Generate for a new one, save, and update the receiving side — the old secret stops being accepted immediately.
If delivery fails
It is retried twice more — after 5 minutes, then after 30 — before a notice appears in your admin area. Two cases are reported without retrying, because a retry would fail identically: a message over 1 MB, and a message that could not be prepared because of an unusual character.
What it deliberately does not do
- Password-protected posts are not sent. Their content is hidden from visitors, so it is not sent outward either.
- Only administrators can redirect a post to a different address. Other editors can still opt a post out.
Still stuck?
The troubleshooting tables in Part 2 cover what visitors see when something goes wrong, and the security-software section there explains the one problem that is genuinely hard to spot on your own.
