ScreenshotNeo

BlogGuides

What Is a Webhook? How Webhooks Work, Webhook vs API, Security, Retries, and Code

A webhook is an HTTP callback sent when an event happens. Learn how delivery, security, retries, idempotency, and polling comparisons work.

By the ScreenshotNeo team30 September 20269 min read

What Is a Webhook? How Webhooks Work, Webhook vs API, Security, Retries, and Code

A webhook is an event-driven HTTP callback. When an event occurs in one application, that application sends an HTTP request to a URL owned by another application. The receiving application can then validate the request, acknowledge it, and process the event without repeatedly asking whether anything changed.

For example, a payment provider can send a payment.succeeded event to your server, or GitHub can notify your deployment system when code is pushed. Webhooks usually use an HTTPS POST request with a JSON body and provider-specific headers.

How a webhook works

  1. You expose a reachable HTTPS endpoint, such as https://example.com/webhooks/orders.
  2. You register that URL with the provider and subscribe to one or more event types.
  3. The provider detects an event and sends an HTTP POST request.
  4. Your endpoint authenticates the request and validates its payload.
  5. You record the delivery or event ID, return a fast 2xx response, and queue slower work.

GitHub describes webhooks as a way to receive data as it happens instead of polling an API. The request includes event metadata such as X-GitHub-Event, X-GitHub-Delivery, and a signature header. Other providers use different names and payload formats, so their documentation is authoritative.

Typical request

POST /webhooks/orders HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: payments-provider
X-Event-Type: order.created
X-Delivery-Id: 8f4e2c...
X-Signature: sha256=...

{"id":"ord_123","status":"created","total":4200}

CloudEvents’ HTTP binding requires a POST and a Content-Type header. The Standard Webhooks specification recommends JSON but does not define one universal event schema.

Webhook versus API

An API is an interface that your code calls when it wants data or wants to perform an action. A webhook is a delivery mechanism in which the provider calls your endpoint after an event occurs. A webhook may carry data that was produced by an API operation, but it is not a replacement for the API. Most integrations use both: the webhook signals that something happened, and an API call retrieves complete or current details.

Question Webhook API request
Who starts the request? The event provider Your application
When does it run? When a subscribed event occurs Whenever your code calls it
Latency Usually near real time Depends on your polling or request schedule
Receiver requirement Publicly reachable endpoint Outbound network access
Main reliability concern Retries, duplicates, replay, and missed delivery Rate limits, pagination, and repeated requests

Webhook versus polling

Polling repeatedly asks an API whether anything changed. A webhook pushes a notification when the change occurs. Webhooks can reduce needless requests and notification delay, but they require an endpoint that is reachable from the provider and code that handles authentication, retries, duplicates, and monitoring.

Webhooks push an event when it happens, while polling asks repeatedly whether anything changed.
Webhooks push an event when it happens, while polling asks repeatedly whether anything changed.
Concern Webhook Polling
Notification delay Provider sends after the event At least the polling interval
Request volume Mostly event-driven Requests occur even when nothing changed
Network setup Inbound HTTPS endpoint required Outbound API access is usually enough
Failure handling Verify signatures, retry safely, deduplicate Track cursors, rate limits, and missed intervals

Build a webhook endpoint

Minimal Node.js example

import express from 'express';
import crypto from 'node:crypto';

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

// Capture the exact bytes so the signature is verified before JSON parsing.
app.post('/webhooks/orders', express.raw({ type: 'application/json' }), async (req, res) => {
  const received = req.get('X-Signature') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!valid) return res.status(401).send('invalid signature');

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.status(400).send('invalid JSON');
  }

  const deliveryId = req.get('X-Delivery-Id');
  // Persist deliveryId in a database with a unique constraint here.
  // If it already exists, acknowledge the retry and do not repeat the work.
  console.log({ deliveryId, type: req.get('X-Event-Type'), event });

  // Queue expensive work, then acknowledge promptly.
  return res.sendStatus(202);
});

app.listen(3000);

The header names and signature construction in this example are illustrative. Use the exact algorithm, signed value, header, timestamp rules, and replay window documented by your provider.

Minimal Python example

import hashlib
import hmac
import json
import os
from flask import Flask, request, abort

app = Flask(__name__)
secret = os.environ["WEBHOOK_SECRET"].encode()

@app.post("/webhooks/orders")
def orders_webhook():
    raw = request.get_data(cache=False)
    received = request.headers.get("X-Signature", "")
    expected = "sha256=" + hmac.new(secret, raw, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(received, expected):
        abort(401)

    try:
        event = json.loads(raw)
    except json.JSONDecodeError:
        abort(400)

    delivery_id = request.headers.get("X-Delivery-Id")
    # Insert delivery_id with a unique constraint before irreversible work.
    # Enqueue event for a worker and return quickly.
    print(delivery_id, event)
    return ("", 202)

if __name__ == "__main__":
    app.run(port=3000)

cURL request for local testing

body='{"id":"ord_123","status":"created"}'
curl -i https://example.com/webhooks/orders \
  -H 'Content-Type: application/json' \
  -H 'X-Event-Type: order.created' \
  -H 'X-Delivery-Id: test-123' \
  -H 'X-Signature: sha256=REPLACE_WITH_PROVIDER_SIGNATURE' \
  --data "$body"

Secure a webhook endpoint

  1. Use HTTPS. Do not accept production webhook traffic over plain HTTP.
  2. Verify the provider signature. Compute the HMAC over the exact raw request body before parsing JSON. Use a constant-time comparison.
  3. Keep secrets out of URLs. Store the secret in a secret manager or protected environment variable.
  4. Check timestamps when supported. Reject requests outside the provider’s replay window.
  5. Restrict event types. Ignore or quarantine events your endpoint did not subscribe to.
  6. Limit payload size. Apply a server and proxy limit appropriate for the provider’s documented maximum.
  7. Log safely. Record delivery IDs, event types, status, and timing without logging tokens or sensitive payload fields.

GitHub recommends an HMAC SHA-256 value in X-Hub-Signature-256, constant-time comparison, HTTPS, and keeping credentials out of URLs. Never trust an incoming webhook merely because it reached your route.

A reliable webhook flow verifies the request, records the delivery, acknowledges quickly, and processes work asynchronously.
A reliable webhook flow verifies the request, records the delivery, acknowledges quickly, and processes work asynchronously.

Retries, duplicates, and idempotency

Providers can retry when your endpoint times out, returns an error, or cannot be reached. Networks can also duplicate requests. Treat every delivery as potentially repeated.

  1. Read the provider’s stable delivery or event ID.
  2. Insert it into a durable table with a unique constraint before doing irreversible work.
  3. If insertion reports a duplicate, return a successful response without repeating the operation.
  4. Perform work from a queue or worker.
  5. Record success, failure, and retry state for operations staff.

The Standard Webhooks specification says the unique event identifier remains the same when a failed webhook is retried. GitHub recommends acknowledging within 10 seconds and moving expensive work to background processing.

CREATE TABLE webhook_deliveries (
  delivery_id TEXT PRIMARY KEY,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  event_type TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'queued'
);

Response codes and processing strategy

Response When to use it
2xx Signature and basic validation passed; delivery is accepted or already processed.
400 The request is malformed and cannot be interpreted.
401 or 403 Authentication or signature validation failed.
404 Only when the route is not a valid subscription endpoint.
429 Use only when you understand the provider’s retry behavior and need backpressure.
5xx Temporary failure that should cause a provider retry, if supported.

Return the response after durable acceptance, not after completing a long task. Queue email, database fan-out, image processing, and third-party API calls. A fast acknowledgement reduces timeout-driven retries.

Provider differences to check

There is no universal webhook payload. Before implementing, document:

  • Event names and whether one endpoint receives multiple types
  • Envelope fields and schema versions
  • Signature algorithm, signed content, headers, and timestamp rules
  • Timeout and retry schedule
  • Maximum payload size
  • Delivery and event ID semantics
  • Redelivery controls and replay tooling
  • Whether ordering is guaranteed
  • How the provider reports disabled or failing endpoints

For example, GitHub documents a 25 MB payload cap and provider-specific headers. That limit should not be generalized to another service.

Testing and local development

  1. Create a staging endpoint with a separate secret.
  2. Use the provider’s test event or a signed fixture.
  3. Test valid requests, bad signatures, malformed JSON, oversized bodies, duplicate IDs, old timestamps, and unknown event types.
  4. Force a worker failure and confirm the provider retries or your queue retries safely.
  5. Expose local development through a secure tunnel only when your provider requires a public URL, and never use production secrets locally.

Troubleshooting common webhook errors

Symptom Likely cause Fix
Signature is always invalid JSON was parsed and re-serialized, changing whitespace or field order. Verify the signature against the untouched raw bytes.
Provider reports timeouts The endpoint performs slow work before responding. Persist the delivery, enqueue work, and return a 2xx quickly.
Orders are created twice Retries or duplicate deliveries are processed independently. Use the stable delivery/event ID as a unique idempotency key.
No requests arrive DNS, TLS, firewall, route, or provider configuration problem. Check the public URL, certificate, access logs, provider delivery log, and subscription status.
Unexpected event shape Different event types or schema versions share one endpoint. Branch on the documented event type and validate each schema.
Large deliveries fail Reverse proxy or application body limit is too low. Set a documented limit that accommodates the provider and reject excess bodies safely.
Replay attacks succeed The implementation checks only a static signature. Validate a signed timestamp and enforce the provider’s replay window when available.

Performance, reliability, and cost

Webhook cost is usually driven by the provider’s event volume, request processing, queueing, storage, and any API calls made by your worker. Reduce waste by subscribing only to needed event types, acknowledging quickly, and deduplicating before expensive work.

For reliability, monitor delivery success rate, response latency, retry count, queue age, signature failures, duplicate rate, and events that remain unprocessed. Keep a durable event record so operators can inspect and replay safely. Do not assume delivery order unless the provider guarantees it; use event timestamps or fetch current state from the provider when ordering matters.

Using webhooks with screenshot jobs

Screenshot workflows often need asynchronous processing when a page requires a long wait, many URLs, or a PDF render. A service can submit a job and notify your endpoint through a signed webhook when the result is ready. Apply the same controls: verify the signature, persist the delivery ID, acknowledge quickly, and fetch or store the result in a worker.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. For a one-call capture, use the API shown in the ScreenshotNeo documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response reports the page verdict and billing result in X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Start with 1,000 free ScreenshotNeo screenshots a month.

Webhook FAQ

Can a webhook call a private localhost URL?

Usually no. The provider must be able to reach the endpoint. Use a public staging endpoint or an approved secure tunnel for development.

Should I trust the JSON body because it came from a known IP?

No. Verify the provider’s signature. IP allowlists can be an additional control when the provider documents stable source ranges.

Should a webhook handler call another API before responding?

Only for quick, bounded validation when required. Durable acceptance and background processing are safer for slow or failure-prone work.

Are webhook deliveries ordered?

Only when the provider explicitly guarantees ordering. Design consumers to handle late and duplicated events.

What happens if my endpoint is down?

Many providers retry, but retry timing and retention differ. Confirm the provider’s policy and maintain your own reconciliation process for important data.