The meaning of webhooks is simple: they are automated event notifications sent from one application to another. Instead of your app repeatedly asking whether something changed, the service that knows about the change sends your app an HTTP request when the event occurs.

That sounds small, but webhooks are the connective tissue behind many useful automations: marking an order as paid, starting a deployment after a code push, updating a CRM when a lead submits a form, or recording whether a transactional email was delivered.

What webhooks mean in plain English

A webhook is an HTTP callback triggered by an event. One system, often called the event source, is configured with a URL belonging to another system, often called the receiver or consumer. When a subscribed event occurs, the event source sends data to that URL.

For example, Stripe can send a webhook when a payment succeeds, and GitHub can send one when somebody pushes code to a repository. AWS describes webhooks as lightweight, event-driven communication that automatically sends data between applications using HTTP; GitHub describes them as notifications delivered to an external web server when specified events occur. (docs.aws.amazon.com)

The word is a blend of web and hook. Think of it as leaving a hook inside another service: when the event you care about happens, that service “hooks” into your endpoint and calls it.

A webhook is not one universal protocol with one fixed payload shape, signature header, retry schedule, or event naming system. It is an integration pattern built on web standards. Each provider documents its own event catalog, registration settings, authentication method, and delivery behavior.

The shortest useful definition

A webhook is:

  • An event happens in Service A.
  • Service A sends an HTTP request to a URL in Service B.
  • Service B verifies the request and performs a follow-up action.

For a concrete example, imagine an online course business. A customer completes checkout, the payment provider emits a payment_succeeded-style event, and your webhook endpoint grants the customer access to the course. The payment provider does not need to wait for a browser redirect or rely on the customer returning to your site.

How a webhook works step by step

Although provider details differ, a webhook delivery usually follows the same lifecycle.

  1. You create a receiving endpoint. This is a route in your backend such as https://api.example.com/webhooks/github.
  2. You register that URL with the sending service. In its dashboard or API, you select the events you want, such as push, invoice.paid, or email.delivered.
  3. An event occurs. For instance, a developer pushes a commit or a payment is confirmed asynchronously.
  4. The sender builds a request. The request commonly contains JSON data, event metadata in headers, and a cryptographic signature.
  5. The sender makes an HTTP request to your endpoint. GitHub repository webhooks, for example, send HTTP POST payloads for subscribed repository events. (docs.github.com)
  6. Your endpoint authenticates and accepts the delivery. It verifies the sender’s signature before trusting the data.
  7. Your application records and processes the event. A durable queue or database record is usually safer than completing all business logic inside the HTTP request.
  8. Your endpoint returns an HTTP success response. A 2xx response tells the sender that the delivery was accepted. A timeout, network failure, or non-2xx response may be treated as a failed delivery.

The key idea is that the sender initiates the request after an event. Your application does not ask for an update on a schedule.

A conceptual webhook request

Here is an illustrative request. The URL, event name, headers, and signature format are examples; real names depend on the provider.

POST /webhooks/payments HTTP/1.1
Host: api.example.com
Content-Type: application/json
User-Agent: PaymentPlatform-Webhooks
X-Webhook-Event: payment.succeeded
X-Webhook-Id: evt_01JXYZ123
X-Webhook-Signature: t=1730000000,v1=abc123...

{
  "id": "evt_01JXYZ123",
  "type": "payment.succeeded",
  "created": "2025-01-15T14:32:08Z",
  "data": {
    "payment_id": "pay_123",
    "customer_id": "cus_456",
    "amount": 4900,
    "currency": "usd"
  }
}

Your endpoint should not assume this exact structure. Stripe sends JSON event objects to HTTPS webhook endpoints, while GitHub lets webhook payloads be delivered as JSON or URL-encoded data. Always implement against the sender’s current documentation for the provider and event types you use. (docs.stripe.com)

Webhooks vs APIs vs polling vs WebSockets

Webhooks are often confused with APIs, polling, and WebSockets because all four move data between systems. They solve different problems.

Webhooks vs APIs

An API is an interface that lets one system request data from or instruct another system. Your application chooses when to call it.

A webhook reverses the direction of initiation. The provider calls your application when something happens. In practice, many integrations use both:

  • The webhook says, “A payment changed state.”
  • Your backend uses the provider’s API to retrieve the newest payment record, if needed.

This combination is valuable when webhook payloads are intentionally compact, when you need fields not included in the event, or when your business action requires the latest authoritative state.

Webhooks vs polling

Polling means your app repeatedly calls an API: “Any new orders? Any new orders now? How about now?” The interval might be every minute, every five minutes, or every hour.

Webhooks avoid many of those empty requests. The provider sends a notification only after the relevant event occurs. AWS notes that polling can be inefficient and creates a lag between data becoming available and downstream synchronization. (docs.aws.amazon.com)

Polling is still useful when a source has no webhook support, when you need periodic reconciliation, or when missing a notification would be unacceptable and the source offers an API that can be queried by timestamp or cursor. A reliable system often uses webhooks for fast reactions and a scheduled reconciliation job for completeness.

Webhooks vs WebSockets

A WebSocket is a persistent, two-way connection between a client and server. It is often used for live chat, collaborative editing, dashboards, games, or streaming market data.

A webhook is normally a one-way HTTP request sent for an event. It does not keep a connection open and does not inherently support a conversational back-and-forth exchange. If your server must tell a browser about a completed background task, a common pattern is: provider webhook reaches your backend, backend updates its data store, then your frontend receives the update through WebSockets, server-sent events, or a refresh.

A quick comparison

MethodWho starts communication?Best forMain trade-off
API requestYour appFetching data or commanding a serviceYou must decide when to ask
PollingYour app, repeatedlySources without events; periodic reconciliationEmpty requests and delay
WebhookEvent sourceReacting to discrete eventsYou must operate a public receiver
WebSocketEither side over an open connectionContinuous, interactive updatesPersistent connection management

The parts of a webhook integration

Knowing the vocabulary makes vendor documentation much easier to follow.

Event source

The application that detects the event and sends the delivery. Examples include a payment processor, source-code host, form builder, email platform, commerce tool, or your own SaaS product.

Event type

The kind of thing that happened. Names are vendor-specific, but often resemble push, order.created, customer.updated, or invoice.paid.

Subscribe only to events that your system can actually handle. GitHub specifically recommends subscribing only to the events you plan to process, which limits unnecessary HTTP requests to your server. (docs.github.com)

Webhook endpoint

The URL that receives requests, such as:

https://api.example.com/webhooks/provider-name

In production, providers commonly require a publicly reachable HTTPS endpoint. Stripe documents that a public endpoint must use HTTPS, though a local development endpoint can use HTTP when testing with its tooling. (docs.stripe.com)

Payload

The request body containing event data. JSON is common, but do not automatically parse the body before you understand the provider’s signature-verification requirements.

Headers

Headers often carry the event name, message ID, delivery timestamp, signature, API version, and source account. GitHub, for example, uses headers to identify the event and can include an X-Hub-Signature-256 value when a webhook secret is configured. (docs.github.com)

Webhook secret and signature

A shared secret is a value known to you and the webhook sender. The sender uses it to calculate a signature over the request body or a provider-defined signed content string. Your server calculates the expected signature independently and compares the values.

That check establishes two important properties: the request came from someone who knows the secret, and the signed content was not altered after signing. It does not mean that any valid event is automatically safe to apply; your application still needs authorization rules, validation, and idempotency.

Delivery attempt

One HTTP request from the sender to your endpoint. A single logical event can result in multiple attempts if the sender retries after it sees a failure or cannot determine whether the first attempt completed.

How to set up a webhook endpoint

The exact dashboard screens vary, but the implementation process is repeatable.

1. Define the business action first

Start with the outcome, not the event name. Write a short rule such as:

When a subscription payment is confirmed, grant the account access to the premium workspace.

Then identify the event or events that prove the condition. Payment status changes, subscriptions, disputes, refunds, and delayed confirmation flows can be asynchronous, which is why payment platforms document webhooks as a way to react to events that occur outside the immediate payment flow. (docs.stripe.com)

Avoid vague rules such as “when checkout finishes.” A browser redirect can be interrupted, manipulated, or never happen. The provider’s server-to-server event is normally the better trigger for backend state changes.

2. Create a narrow endpoint

Use a dedicated path per provider or integration when practical:

POST /webhooks/github
POST /webhooks/stripe
POST /webhooks/email

Separate routes simplify secrets, request-size limits, logs, permissions, and incident response. Do not reuse a generic unauthenticated endpoint and decide which provider sent the request based only on a field in unverified JSON.

3. Preserve the raw request body

Many providers sign the exact bytes they send. If your framework parses JSON first, it may alter whitespace, character encoding, or representation before verification.

Stripe explicitly warns that framework processing that changes the request body, including parsing or reformatting it, can cause signature verification to fail. (docs.stripe.com)

Configure raw-body handling for the webhook route, validate the signature, and only then parse JSON. The framework syntax is version-specific, so follow the provider’s SDK instructions for Express, Next.js, Laravel, Django, Rails, serverless functions, or your chosen platform.

4. Store the event ID before doing side effects

Create a table or collection such as webhook_events with fields like:

FieldPurpose
providerIdentifies the event sender
event_idUnique delivery or event ID supplied by the sender
event_typeMakes routing and reporting easier
received_atSupports debugging and replay windows
payloadStores the original payload securely, subject to retention rules
statusSuch as received, processing, processed, or failed

Put a unique constraint on (provider, event_id) when the provider has a stable event ID. If the same event arrives again, the insert fails or becomes a no-op, and your system can return success without repeating a charge, email, entitlement grant, or CRM update.

5. Acknowledge quickly, then process asynchronously

Webhook senders are not designed to wait while your endpoint sends several emails, calls multiple APIs, renders a PDF, or runs a long AI workflow. Put the verified event into durable storage or a queue, return a 2xx response, and let a worker perform longer operations.

The time limit is provider-specific. GitHub’s hosted webhook guidance says a server should return a 2XX response within 10 seconds; otherwise GitHub ends the connection and treats delivery as failed. (docs.github.com)

A 200 OK, 202 Accepted, or 204 No Content can all be appropriate depending on your API design and provider documentation. The crucial distinction is that you should return success only after the event is safely accepted for processing, not merely because an HTTP request reached your load balancer.

6. Register the endpoint and select events

In the provider’s dashboard or API, enter the endpoint URL, create or copy the webhook secret, and select the events. Many services support test and live environments with different endpoint registrations and secrets. Keep those separate.

For email systems, delivery, bounce, complaint, and unsubscribe events can keep your product database accurate. If you are building the sending side as well as receiving events, consult the email API setup guides for provider-specific integration patterns.

7. Test an event you can recognize

Use a provider’s test-event button, CLI, sandbox, or a deliberate real action in a non-production environment. Inspect the provider’s delivery log and your application logs. Do not treat “the endpoint returned 200” as complete proof; confirm that the intended database change or queued job occurred exactly once.

GitHub sends a simple ping event after a new webhook is created, which is useful for confirming basic endpoint reachability and configuration. (docs.github.com)

Worked example: receive and verify a GitHub webhook in Node.js

This example shows the shape of a secure receiver for a GitHub webhook. It handles a push event, verifies GitHub’s HMAC SHA-256 signature, and acknowledges delivery. It is intentionally small so you can understand the moving parts; production systems should also persist event IDs and queue work.

GitHub’s signature header is named X-Hub-Signature-256. When a webhook secret is configured, the value uses a sha256= prefix followed by the HMAC digest. GitHub recommends signature validation before further processing. (docs.github.com)

Prerequisites

  • Node.js installed.
  • A GitHub repository where you can add a webhook.
  • A random webhook secret stored as an environment variable.
  • A publicly reachable HTTPS URL for production, or a secure tunnel for local testing.

Install Express:

npm init -y
npm install express

Create server.js:

const crypto = require("node:crypto");
const express = require("express");

const app = express();
const secret = process.env.GITHUB_WEBHOOK_SECRET;

if (!secret) {
  throw new Error("Set GITHUB_WEBHOOK_SECRET before starting the server.");
}

function verifyGitHubSignature(rawBody, signatureHeader) {
  if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
    return false;
  }

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const receivedBuffer = Buffer.from(signatureHeader, "utf8");
  const expectedBuffer = Buffer.from(expected, "utf8");

  if (receivedBuffer.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(receivedBuffer, expectedBuffer);
}

app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("x-hub-signature-256");
    const eventName = req.get("x-github-event");

    if (!verifyGitHubSignature(req.body, signature)) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(req.body.toString("utf8"));

    if (eventName === "ping") {
      console.log("GitHub webhook verified for:", payload.repository?.full_name);
      return res.status(204).end();
    }

    if (eventName === "push") {
      console.log("Push received for:", payload.repository?.full_name);
      console.log("After commit:", payload.after);

      // Production pattern:
      // 1. Store a unique delivery ID and raw payload in a database.
      // 2. Enqueue a background job.
      // 3. Return success only after the record/queue write succeeds.
    }

    return res.status(204).end();
  }
);

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});

Start it with a secret:

GITHUB_WEBHOOK_SECRET='replace-with-a-long-random-secret' node server.js

In your repository’s webhook settings, set the payload URL to your public tunnel URL plus /webhooks/github, choose application/json, enter the same secret, and select the Pushes event. GitHub’s webhook configuration lets you subscribe to specific events, and its delivery records help you see requests, responses, and errors. (docs.github.com)

What success looks like

You know this integration works when all of these are true:

  1. GitHub’s delivery view shows a successful response in the 2xx range.
  2. Your local or production logs show the ping event after setup.
  3. A real push creates a push delivery.
  4. Your endpoint logs the expected repository and commit SHA.
  5. Changing the webhook secret or altering the request causes a 401 Invalid signature, proving verification is active.

Do not copy the GitHub signature implementation to Stripe, Slack, or another provider without modification. The cryptographic algorithm, header, timestamp rules, signed string, and official libraries are vendor-specific.

Webhook security: the controls that matter

A webhook endpoint is an internet-facing route. Treat it as a privileged input boundary, not as a private internal API.

Verify signatures before processing

Use the sender’s documented signature-validation method, and do it before database writes or business actions. GitHub recommends using a webhook secret and validating deliveries; Stripe includes a signature header so an endpoint can verify that a request came from Stripe rather than a third party. (docs.github.com)

Never use a static token embedded in a query string as your only protection if the provider supports signed payloads. URLs are more likely to appear in access logs, analytics tools, screenshots, browser history, or monitoring systems.

Use HTTPS and protect secrets

Use HTTPS in production. Keep webhook secrets in a secrets manager or environment configuration rather than source control. Create distinct secrets for distinct endpoints and environments, and rotate them using the provider’s documented process.

A secret should be high entropy. Do not reuse your production secret in a test environment, and do not use an API key as a webhook secret unless the provider explicitly instructs you to do so.

Defend against replay attacks

A valid signed request can sometimes be replayed. If the sender supplies a timestamp, reject deliveries that are too old according to the provider’s tolerance guidance. More importantly, store and deduplicate the event ID or delivery ID.

Signature verification proves authenticity and integrity; idempotency prevents an authentic event from performing the same side effect twice.

Limit what the endpoint can do

Validate the event type and required fields. Map the event to an explicit handler rather than dynamically invoking code based on a string from the payload. Use least-privilege credentials for any downstream services the worker calls.

If an event says that a user should get access, verify that the referenced customer, account, price, or product matches your own expected configuration. A signature does not substitute for business validation.

Avoid logging sensitive payloads indiscriminately

Webhook payloads can contain names, email addresses, account identifiers, order data, or other regulated information. Redact secrets and sensitive fields from application logs. Apply a retention policy to stored raw payloads, and restrict who can view delivery history.

Reliability: retries, duplicates, ordering, and idempotency

The most common webhook mistake is treating delivery as exactly once and perfectly ordered. A robust design assumes neither.

Expect retries and duplicate deliveries

Senders can retry because your endpoint was unavailable, returned a non-success response, exceeded a timeout, or experienced a network interruption after the sender sent the request. Some providers also let you manually replay failed deliveries. GitHub documents manual redelivery for failed deliveries, while Stripe documents processing undelivered events alongside its automatic retry behavior. (docs.github.com)

Your handler must be idempotent: processing the same logical event more than once should produce the same final state as processing it once.

A practical idempotency approach is:

  1. Verify the signature.
  2. Extract the provider event ID or delivery ID.
  3. Attempt to insert it into a table with a unique constraint.
  4. If it already exists, return a success response and do not repeat side effects.
  5. If it is new, enqueue work and mark processing status.
  6. Let a worker mark it processed only after the business action succeeds.

Do not rely on arrival order

Two related events can arrive in an unexpected order. For example, an update can reach you before a creation notification, or a retry of an earlier event can arrive after a later event.

Design state updates so they are based on timestamps, versions, or a fresh API fetch where appropriate. If a webhook only tells you “something changed,” retrieve the current object from the provider API before making an irreversible decision.

Return success only after durable acceptance

Returning 204 before writing the event anywhere creates a dangerous gap: the provider believes the event is handled, but your process may crash before doing the work.

The stronger pattern is verify → persist or enqueue durably → return 2xx → process. If the persistence step fails, return an error so that the provider can retry according to its documented behavior.

Build a dead-letter and replay process

Some events will fail because a downstream API is down, a record is malformed, or a temporary configuration issue blocks processing. Keep failed events with error details, retry counts, and a way for an authorized operator to replay them after fixing the underlying problem.

This is especially important for money movement, account provisioning, unsubscribes, data deletion requests, and operational alerts. A delivery dashboard is useful, but it is not a substitute for your own application-level audit trail.

Common webhook use cases

Webhooks fit events that should cause an immediate or near-immediate backend action.

Payments and subscriptions

Use payment events to grant or revoke access, create invoices, update accounts receivable, start fulfillment, or flag disputes. Webhooks are especially useful because payment confirmation, bank processing, recurring billing, refunds, and disputes may happen after the user’s browser session ends. Stripe documents webhooks for events such as successful payments, disputes, and available balance changes. (docs.stripe.com)

Source control and deployments

A push, pull request, release, or workflow event can trigger builds, preview deployments, security checks, changelog automation, and notifications. GitHub webhooks are designed for external servers to react when repository events occur. (docs.github.com)

Email lifecycle events

Email providers can report accepted, delivered, bounced, complained, clicked, opened, unsubscribed, or suppressed events. These events can update contact status, protect sender reputation, stop follow-up sequences, and help support teams diagnose a missing message.

CRM, forms, and marketing automation

A form submission can create a lead, enrich a profile, notify a sales channel, create a task, and begin a nurture sequence. A subscription preference change can sync audience status across your product, CRM, and email platform.

Commerce and fulfillment

Order-created, payment-confirmed, refund-issued, shipment-created, and delivery-completed events can synchronize storefronts, warehouses, accounting systems, and customer notifications without waiting for scheduled imports.

Your own SaaS platform

If customers need to integrate with your product, outbound webhooks are often more useful than forcing them to poll your API. Provide a clear event catalog, versioned payload schemas, signatures, endpoint management, delivery logs, retries, and replay controls.

For interoperability, CloudEvents is a CNCF specification for describing event data in common formats across services and platforms. It can be useful when you control multiple systems or want a standardized event envelope, but it is not required for ordinary webhook integrations. (github.com)

When webhooks are the wrong tool

Webhooks are powerful, but they add operational responsibilities. Do not choose them merely because they sound real-time.

Use an API request instead when your application needs to ask a question on demand, such as “what is this customer’s current plan?” Use polling when the source cannot send events or when you need a periodic reconciliation sweep. Use a message queue or event bus inside your own infrastructure when you need controlled delivery between internal services and do not want to expose public HTTP endpoints.

Webhooks can also be a poor fit when:

  • The receiving system cannot expose a reachable endpoint.
  • The sender cannot provide adequate authentication or signing.
  • You need a guaranteed global ordering across many events.
  • The event stream is extremely high volume and the provider’s webhook limits do not match your throughput needs.
  • The consumer needs to request arbitrary historical data rather than react to discrete changes.

A mature architecture often combines methods: webhooks for quick notification, an API for current state and recovery, a queue for internal work, and a scheduled reconciliation process for assurance.

Testing and monitoring a webhook integration

A webhook is only useful if you can tell whether it delivered, verified, queued, and completed its intended action.

Test locally without weakening production security

Use the provider’s official CLI where available, or a secure tunnel that forwards a public URL to your local machine. Stripe documents using the Stripe CLI to forward events to a local webhook endpoint; GitHub documents testing webhook handling against a local server or codespace with forwarding. (docs.stripe.com)

Never disable signature checks just to make local testing convenient. Instead, use the test endpoint secret generated for your local listener or test environment.

Monitor four layers

Track more than HTTP status codes:

  • Delivery layer: attempts received, signature failures, response codes, latency, and timeouts.
  • Acceptance layer: events persisted, duplicate events detected, and queue publish failures.
  • Processing layer: worker success rate, retries, dead-letter count, and processing duration.
  • Business layer: subscriptions granted, orders fulfilled, contacts suppressed, deployments started, or any other real outcome.

Use a correlation ID where possible. Store the webhook event ID and propagate it to logs, queue messages, and downstream API calls. That gives support and engineering teams one thread to follow from a provider delivery to a final business result.

Troubleshoot in the right order

When an expected action did not happen, work through this sequence:

  1. Did the upstream event actually occur?
  2. Was the correct event type selected in the webhook subscription?
  3. Did the provider attempt delivery?
  4. Did your endpoint receive the request?
  5. Did signature verification pass?
  6. Did your application persist or queue the event?
  7. Did the worker complete the business action?
  8. Was the action skipped because the event was a duplicate or stale?

GitHub’s troubleshooting guidance recommends checking the recent delivery log, confirming that the relevant event subscription is enabled, and verifying that the triggering action happened in the repository or location where the webhook is configured. (docs.github.com)

The practical takeaway

The meaning of webhooks is not just “an automated notification.” A reliable webhook is a small event-ingestion system: it receives an HTTP request, proves who sent it, records it safely, handles duplicates and retries, processes it asynchronously, and leaves enough evidence for humans to debug it.

Start with one narrow event and one measurable business outcome. Use a dedicated HTTPS endpoint, validate the provider’s signature against the raw body, store a unique event ID, return 2xx only after durable acceptance, and build a replay path for failures. Those practices matter far more than the programming language or automation platform you choose.

FAQ

What is a webhook in simple terms?

A webhook is an automated HTTP notification. When an event happens in one application, it sends data to a URL in another application so the receiving app can react without constantly checking for changes.

Is a webhook the same as an API?

No. With an API, your application usually initiates a request when it wants information or wants to perform an action. With a webhook, the provider initiates the request to notify your application that an event occurred. Many integrations use both together.

Are webhooks secure?

They can be secure when implemented correctly. Use HTTPS, verify the provider’s cryptographic signature against the raw request body, protect and rotate secrets, validate event data, deduplicate event IDs, and restrict what downstream actions the event can trigger.

Why did my webhook run twice?

Webhook senders may retry after a timeout, network error, or non-2xx response; providers or operators may also replay deliveries. Build idempotent processing so a duplicate event ID does not repeat the same side effect.

Do webhooks always arrive immediately and in order?

No. Delivery timing, retry behavior, and ordering are provider-specific. Treat webhook payloads as notifications that can be delayed, duplicated, or arrive out of order, then design processing around event IDs, timestamps, versions, and—when needed—a follow-up API read.