What is a webhook endpoint? The URL that receives events, explained with a real one

A webhook endpoint is a URL on your server that another service calls with an HTTP POST and a JSON body the moment something happens, so your code reacts to the event instead of asking for it again and again. The service stores the URL you give it and turns each event into one request to it: PostWire, for example, POSTs 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.

  1. 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.

  2. 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/postwire in the examples below), a serverless function, or the production URL of an n8n Webhook node or a Make or Zapier custom webhook. A localhost address, a private IP or an http:// URL is refused with bad_url, and redirects are not followed, so give the final URL.

  3. 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_…" }
  4. Send a test event before anything real

    The sender posts a signed webhook.test event 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"
  5. Verify the signature, then act on the event

    Read the raw body, compute HMAC-SHA256 with your secret over <t>.<raw body>, compare it with v1 in constant time, and reject a t older 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);
    });
  6. Answer 2xx fast, do the slow work after

    The sender waits 5 seconds for an answer. Reply 200 as 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 same PostWire-Delivery id each time, so an endpoint that was down for a night still gets every event.

  7. 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.

PartWhat it isIn PostWire's webhooks
The URLA public address the sender can reach; stored in the sender's settings with the events it should receivePOST /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 eventOne 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 signatureA header that proves the request came from the sender and was not alteredPostWire-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 retriesYour endpoint replies 2xx to say "got it"; anything else is tried again later2xx 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-Event is the type, so a router can branch before parsing the body.
  • PostWire-Delivery is the delivery id. Deliveries can arrive twice and out of order; keep the ids you have handled and skip repeats.
  • PostWire-Signature carries the timestamp and the HMAC. Verify over the raw bytes, not over re-serialized JSON, or the hash will not match.
  • data.idempotency_key comes 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 endpointPollingWebhook endpoint
Who callsYou call the serviceYou call the service, on a timerThe service calls you
WhenWhen you want something done: POST /api/post publishes a postEvery N seconds, whether or not anything changedThe moment the thing happens
Who hosts the URLThe serviceThe serviceYou
AuthYour API key in the requestYour API key in each requestThe sender's signature, checked by you
On PostWirePOST /api/post, GET /api/post/statusGET /api/post/status?platform=…&id=… in a looppost.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.