What is a webhook endpoint? The URL that receives events, explained with a real one
post.published or post.failed to your endpoint the second a network accepts or refuses a post. An endpoint is the receiving half of a webhook: public, over HTTPS, answering 2xx within seconds, and checking a signature so it only trusts the sender it expects. Below: the parts of one, a real request, and a working endpoint in Node.js and Python you can copy.The word gets used for three things: the URL, the server code behind it, and the row in the sender's settings that holds the URL and the events it should get. They are the same thing seen from three sides. This guide uses PostWire's webhooks as the worked example because every field is public in its OpenAPI file, but the shape is the same one Stripe, GitHub and Shopify use.
Updated . Written from PostWire's own code; vendor prices read on Oct 23, 2026. Live networks: TikTok, Instagram, YouTube, LinkedIn (personal profiles), Facebook Pages, Bluesky, Mastodon, Telegram, Discord, and X on paid plans.
Set up a webhook endpoint, step by step
A receiving endpoint from zero, in about ten minutes, with PostWire as the sender. The free plan includes webhooks, so the whole thing costs nothing.
- Create a free PostWire account and an API key
Open the dashboard and sign in with your email: a 6-digit code, no password, no card. The free plan is 1 business, 10 posts a month, 2 networks per post, no card, with the REST API, the MCP server and 2 webhook endpoints. Under API & MCP press Create key; the key is sent as
Authorization: Bearer. - Put a public HTTPS URL in front of your code
The endpoint must be reachable from the internet over
https://: a route on your deployed app (/hooks/postwirein the examples below), a serverless function, or the production URL of an n8n Webhook node or a Make or Zapier custom webhook. Alocalhostaddress, a private IP or anhttp://URL is refused withbad_url, and redirects are not followed, so give the final URL. - Register the endpoint with the sender
In the dashboard: API & MCP → Webhooks → add the URL. Or one request. The response carries the signing secret once; store it as an environment variable, it is never shown again.
curl -X POST https://postwire.io/api/webhooks \ -H "Authorization: Bearer $POSTWIRE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/postwire", "events": ["post.published", "post.failed", "connection.reauth_required"], "description": "Production app" }' # → 201 { "webhook": { "id": "…", "url": "…", "events": [...] }, "secret": "whsec_…" } - Send a test event before anything real
The sender posts a signed
webhook.testevent to the URL now and reports what your endpoint answered, so you see the headers and the body without publishing anything.curl -X POST https://postwire.io/api/webhooks/<id>/test \ -H "Authorization: Bearer $POSTWIRE_API_KEY" - Verify the signature, then act on the event
Read the raw body, compute HMAC-SHA256 with your secret over
<t>.<raw body>, compare it withv1in constant time, and reject atolder than 5 minutes. Only then parse the JSON.// Node.js (Express) import crypto from "node:crypto"; app.post("/hooks/postwire", express.raw({ type: "application/json" }), (req, res) => { const parts = Object.fromEntries(req.get("PostWire-Signature").split(",").map((p) => p.split("="))); const want = crypto.createHmac("sha256", process.env.POSTWIRE_WEBHOOK_SECRET) .update(`${parts.t}.${req.body}`).digest("hex"); const a = Buffer.from(want), b = Buffer.from(parts.v1 || ""); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; if (!fresh || a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400); const event = JSON.parse(req.body); // dedupe on req.get("PostWire-Delivery"), then branch on event.type res.sendStatus(200); }); - Answer 2xx fast, do the slow work after
The sender waits 5 seconds for an answer. Reply
200as soon as the signature checks out and push the real work (a Slack message, a database row, an email to the client) onto a queue. Anything other than a 2xx — a timeout, a 4xx, a 5xx, a redirect — is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h, with the samePostWire-Deliveryid each time, so an endpoint that was down for a night still gets every event. - Read the delivery log when something looks off
Every try is written down with its status, the number of attempts, your endpoint's last answer and the payload, so a missing event is never a mystery.
curl https://postwire.io/api/webhooks/<id>/deliveries \ -H "Authorization: Bearer $POSTWIRE_API_KEY"
Using n8n? The PostWire Trigger node of the PostWire n8n node does steps 3 to 6 by itself: it registers the endpoint when the workflow is activated, removes it when it is deactivated and checks the signature of every delivery. See n8n social media automation and the webhooks guide.
The parts of a webhook endpoint
Four things make a URL a working webhook endpoint. The right-hand column is what PostWire sends to yours, read in its code and OpenAPI file on Oct 23, 2026.
| Part | What it is | In PostWire's webhooks |
|---|---|---|
| The URL | A public address the sender can reach; stored in the sender's settings with the events it should receive | POST /api/webhooks with url, optional events and description; https and public hosts only, no redirects; 2 endpoints on Free, 5 on Starter, 10 on Pro, 25 on Agency, 50 on Scale |
| The event | One HTTP POST with a JSON body per thing that happened, with a type and a unique id | { id: "evt_…", type, created_at, data }; one event per network, so a post to three networks that worked on two sends two post.published and one post.failed |
| The signature | A header that proves the request came from the sender and was not altered | PostWire-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>, the format Stripe uses, so existing verification code fits; the secret is shown once, when the endpoint is created |
| The answer and the retries | Your endpoint replies 2xx to say "got it"; anything else is tried again later | 2xx within 5 seconds; otherwise retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h: 9 tries over about 46 hours, then marked failed; every try in the delivery log |
A real webhook request, header by header
This is what arrives at a PostWire endpoint when a scheduled YouTube post goes out (from the webhooks guide, Oct 23, 2026):
POST /hooks/postwire
Content-Type: application/json
PostWire-Event: post.published
PostWire-Delivery: 3f6c2a0e-… (the same on every retry: dedupe on it)
PostWire-Signature: t=1759489200,v1=5d1c…
{
"id": "evt_9b0f…",
"type": "post.published",
"created_at": "2026-10-03T14:00:02.118Z",
"data": {
"platform": "youtube",
"status": "published",
"post_id": "dQw4w9WgXcQ",
"url": "https://youtube.com/shorts/dQw4w9WgXcQ",
"error": null, "code": null,
"account": { "platform": "youtube", "handle": "@acme", "brand_id": "…", "brand": "Acme" },
"source": "schedule", "schedule_id": "8a1d…",
"idempotency_key": null, "label": "Monday launch"
}
}
PostWire-Eventis the type, so a router can branch before parsing the body.PostWire-Deliveryis the delivery id. Deliveries can arrive twice and out of order; keep the ids you have handled and skip repeats.PostWire-Signaturecarries the timestamp and the HMAC. Verify over the raw bytes, not over re-serialized JSON, or the hash will not match.data.idempotency_keycomes back if you sent one with the publish, so an event can be matched to the request that caused it.
The events an endpoint can ask for: post.published, post.failed, connection.reauth_required (a network's sign-in expired or was revoked; includes reconnect_url), approval.requested and approval.decided (only for endpoints that list them) and webhook.test. An endpoint created without events gets the first three. The full schema is WebhookEvent in openapi.json; every field is on the webhooks guide.
Webhook endpoint vs API endpoint vs polling
The three get mixed up because all of them are URLs that take HTTP requests. The difference is who calls whom, and when.
| API endpoint | Polling | Webhook endpoint | |
|---|---|---|---|
| Who calls | You call the service | You call the service, on a timer | The service calls you |
| When | When you want something done: POST /api/post publishes a post | Every N seconds, whether or not anything changed | The moment the thing happens |
| Who hosts the URL | The service | The service | You |
| Auth | Your API key in the request | Your API key in each request | The sender's signature, checked by you |
| On PostWire | POST /api/post, GET /api/post/status | GET /api/post/status?platform=…&id=… in a loop | post.published / post.failed POSTed to your endpoint, with the final URL of the post |
Most integrations use both halves: an API call to make something happen, a webhook endpoint to learn how it ended. A YouTube upload PostWire finishes on a later run, or an Instagram video still transcoding, sends its event when it is settled, so the endpoint hears the real outcome without a status loop.
The same endpoint in Python
# Flask
import hmac, hashlib, time, os
from flask import request
@app.post("/hooks/postwire")
def postwire_hook():
parts = dict(p.split("=", 1) for p in request.headers["PostWire-Signature"].split(","))
body = request.get_data() # raw bytes
want = hmac.new(os.environ["POSTWIRE_WEBHOOK_SECRET"].encode(),
f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(want, parts.get("v1", "")):
return "", 400
event = request.get_json()
# dedupe on request.headers["PostWire-Delivery"], then handle event["type"]
return "", 200
Publishing from Python in the first place: post to social media with Python.
What a webhook endpoint is not, and what to watch
- Not an inbound API for the sender to query. It only receives; the sender never reads from it. Anything you want to ask goes through the sender's API.
- Not guaranteed exactly once. A retry after a slow 200 means the same event twice: dedupe on the delivery id.
- Not a place for slow work. Answer first, work later; the 5-second window is for the acknowledgement, not for the job.
- Not private. The URL is on the public internet, so the signature check is what keeps strangers out, and the secret belongs in an environment variable, not in the code.
- Not a PostWire-only idea. The header names differ by sender (
Stripe-Signature,X-Hub-Signature-256,X-Shopify-Hmac-Sha256), but the shape — a public URL, a POST, an HMAC, a fast 2xx, retries — is the same, so an endpoint written for one is a small edit from another.
Questions people ask
What is a webhook endpoint?
A URL on your server that another service sends an HTTP POST to, with a JSON body, when an event happens on its side. You register the URL with the service, it stores it next to the events you want, and each event becomes one request to that URL. It is the receiving half of a webhook; the sending half is the service's job.
What is the difference between a webhook endpoint and an API endpoint?
Direction. An API endpoint is a URL the service hosts and you call when you want something done, with your API key. A webhook endpoint is a URL you host and the service calls when something has happened, signed so you can trust it. On PostWire, POST /api/post is an API endpoint that publishes; your https://example.com/hooks/postwire is a webhook endpoint that receives post.published and post.failed.
What should a webhook endpoint return?
Any 2xx status, quickly: PostWire waits 5 seconds. A 2xx means delivered. A timeout, a 4xx, a 5xx or a redirect is treated as not delivered and retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h, 9 tries in all, then marked failed in the delivery log.
How do I secure a webhook endpoint?
Use HTTPS (PostWire refuses an http:// or private URL), verify the signature header on every request over the raw body with the secret you were given once, compare in constant time, reject timestamps older than 5 minutes, and keep the delivery ids you have processed so a repeat is skipped. Keep the secret in an environment variable.
How can I test a webhook endpoint without writing a server?
Point the endpoint at the production URL of an n8n Webhook node or a Make or Zapier custom webhook, register it, then call POST /api/webhooks/{id}/test: PostWire sends a signed webhook.test event and reports what the URL answered. In n8n, the PostWire Trigger node registers the endpoint and checks the signature for you.
What happens if my webhook endpoint is down?
The delivery is retried with growing waits (1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h) under the same PostWire-Delivery id, about 46 hours in all, and every try is listed by GET /api/webhooks/{id}/deliveries with the status, the attempts and your endpoint's last answer. A failing webhook never delays or fails the publish itself.
How many webhook endpoints can I register on PostWire?
It depends on the plan: 2 on Free, 5 on Starter ($9 a month), 10 on Pro ($29), 25 on Agency and 50 on Scale. GET /api/webhooks returns your limit as max. Webhooks themselves are on every plan, including Free.
Try it on one business, free.
The free plan is 1 business, 10 posts a month, 5 AI drafts a day (about 20 a month) and 2 networks per post — every network connects free, no card, no time limit. Paid plans start at $9 a month and have a 14-day refund; Pro can be tried free for 7 days and Agency for 14, with a card at checkout and nothing charged that day.