KAHIBARO
Discord Login Register

27.4. Webhooks

Why Webhooks Exist

Most APIs use the request response style: your backend sends a request, the other service returns a response, and that is the end of the story.

Sometimes this model is not enough. Examples:

You could poll their API repeatedly, but that is wasteful and slow. Webhooks solve this.

A webhook is a way for one system to call another system automatically when an event happens. Instead of you calling their API, they call yours.

In API terms:

Your backend exposes an HTTP endpoint, and another service sends POST requests to it whenever a specific event occurs.

Core idea: A webhook is an HTTP callback. Event happens, then the provider sends an HTTP request to your URL automatically.

Typical flow:

  1. You register a URL with the provider (for example, https://api.example.com/webhooks/payment).
  2. An event happens in the provider system (for example, payment succeeded).
  3. The provider creates a message describing the event.
  4. The provider performs an HTTP POST to your URL with that message.
  5. Your backend receives, validates, and processes the event, then returns an HTTP response.

Webhooks vs REST APIs

Webhooks and REST APIs complement each other. You will often use both in the same integration.

Direction of Communication

AspectREST APIWebhook
Who initiatesClient (you)Provider (external service)
Typical HTTP verbGET, POST, PUT, PATCH, DELETEUsually POST
When it happensWhenever you decideWhenever provider decides after an event
Common usageQuery or change data on demandReceive notifications about asynchronous events

Example:

Push vs Pull

REST APIs are pull based. You pull information, usually by polling if you want to know about changes.

Webhooks are push based. Provider pushes information to you as soon as it changes.

This makes webhooks useful for:

When to Use Which

Use REST APIs when:

Use webhooks when:

In many designs:

For example:

  1. Webhook: "Order 123 status updated."
  2. Your system receives the webhook, then calls the provider REST API: GET /orders/123 to get the latest data.

Typical Webhook Architecture

Key Components

A simple webhook setup has these parts:

  1. Provider: External service that sends notifications.
  2. Webhook endpoint: Your HTTP endpoint that receives notifications.
  3. Verification layer: Checks that the request is really from the provider and not tampered with.
  4. Processing logic: Updates your database and performs business actions.
  5. Response logic: Returns the correct HTTP status code quickly.
  6. Error handling / retries: Deals with failures and duplicate deliveries.

High level diagram:

Example JSON Payloads

Most webhooks use JSON. A provider will often include:

Example: payment provider webhook:

json
{
  "id": "evt_12345",
  "type": "payment.succeeded",
  "created_at": "2026-08-28T10:00:00Z",
  "data": {
    "payment_id": "pay_98765",
    "amount": 4999,
    "currency": "USD",
    "order_id": "order_42",
    "status": "succeeded"
  }
}

Example: Git hosting webhook for a push:

json
{
  "id": "push_abc",
  "type": "git.push",
  "created_at": "2026-08-28T10:30:00Z",
  "data": {
    "repository": "backend-course",
    "branch": "main",
    "commit_id": "a1b2c3d4",
    "pusher": "alice"
  }
}

Many providers also pass metadata in headers, for example:

You will use these headers to route and verify events.

Designing Webhook Endpoints

Endpoint URL Design

You can use different URL patterns. Pick one that is simple and clear.

Common approaches:

StyleExample URLNotes
Single endpoint/webhooksAll providers and event types in one place.
Per provider/webhooks/stripe, /webhooks/githubClear ownership, separate auth logic possible.
Per domain area/webhooks/payments, /webhooks/subscriptionsRoute by business domain.

For beginners, use per provider. It is straightforward and contains provider specific logic in one file.

Example design:

HTTP Methods and Status Codes

Webhooks almost always use POST. Some rare cases may use PUT, but POST is the standard.

Your endpoint must return a meaningful HTTP status code:

StatusMeaning for provider
2xxEvent accepted successfully.
4xxYour endpoint thinks there is a permanent problem.
5xxTemporary server problem, provider may retry later.

Simple guideline:

Always try to return 2xx if:

  • The signature is valid.
  • The payload is well formed.
  • You have successfully stored or queued the event.
    Return 4xx if:
  • Signature is invalid or missing.
  • The payload format is wrong or missing required fields.
    Return 5xx for unexpected internal errors.

Providers often retry automatically when they receive 5xx or sometimes when they receive network errors.

Example Minimal Endpoint Logic

Imagine a simple Python FastAPI endpoint (conceptual, not a full FastAPI lesson):

python
from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/webhooks/payments")
async def payments_webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("X-Signature")
    if not is_valid_signature(raw_body, signature):
        raise HTTPException(status_code=400, detail="Invalid signature")
    event = await request.json()
    # Basic validation
    if "id" not in event or "type" not in event:
        raise HTTPException(status_code=400, detail="Invalid payload")
    # Store event for later processing
    save_event_to_db(event)
    # Return quickly
    return {"received": True}

Key points:

Securing Webhooks

Security is one of the most important parts of webhook design. You cannot trust any incoming HTTP request automatically.

You must solve at least three problems:

  1. Is this request really from the provider?
  2. Has the body been tampered with?
  3. Can attackers replay old events?

Shared Secrets and Signatures

The most common solution uses a shared secret and a signature header.

Basic idea:

  1. You and the provider agree on a secret string, for example WEBHOOK_SECRET.
  2. When sending a webhook, the provider:
    • Takes the raw request body as bytes.
    • Optionally prefixes it with a timestamp string.
    • Calculates an HMAC using a hash function like SHA256.
    • Puts the result in a header, for example X-Signature: sha256=<hex>.
  3. Your backend:
    • Reads the raw request body.
    • Reads the timestamp, if used.
    • Recomputes the HMAC using the same secret.
    • Compares your result with the header, using a constant time comparison function.

If the values match, the message is authentic.

In pseudo code:

python
import hmac
import hashlib
def is_valid_signature(body: bytes, header_signature: str, secret: str) -> bool:
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=body,
        digestmod=hashlib.sha256
    ).hexdigest()
    return constant_time_compare(expected, header_signature)

Rule: Never trust webhook payloads without verifying a signature or using another strong authentication method.
Rule: Always compare signatures using a constant time compare function to avoid timing attacks.

IP Whitelisting

Some providers let you know which IP ranges they use. You can then allow only these IPs for webhook endpoints.

In practice:

IP whitelisting is a useful extra layer, not a replacement for signatures.

No Public Secrets in URLs

Never use secrets directly in URLs like:

https://api.example.com/webhooks/payments?token=my-secret

Reasons:

If a provider allows only URL based secrets, treat the URL as sensitive and rotate it whenever possible. Prefer header based authentication or signatures.

Handling Webhook Reliability

Webhooks are part of a distributed system. You must handle:

Idempotency and De Duplication

Providers often retry webhooks when they do not get a 2xx response. Your endpoint might then receive the same event several times.

Your handler must be idempotent. That means applying the same event multiple times leads to the same final state.

You usually achieve this with event ids.

Typical pattern:

  1. Each event has a unique id, for example evt_12345.
  2. When you receive an event:
    • Check if this id already exists in your database.
    • If yes, ignore it or treat it as already processed.
    • If no, store it and process it.

Example table to store webhook events:

ColumnTypeDescription
idtextEvent id from provider
typetextEvent type, for example payment.succeeded
payloadjsonbFull event body
received_attimestampWhen you received it
processed_attimestampWhen processing completed
statustextpending, succeeded, failed

Pseudo logic:

python
def handle_webhook(event):
    if already_processed(event["id"]):
        return
    save_event(event)  # mark as pending
    try:
        apply_business_logic(event)
        mark_event_as_succeeded(event["id"])
    except Exception:
        mark_event_as_failed(event["id"])
        raise

In many systems, saving the event and processing it will be decoupled. You save and enqueue it in the webhook handler, then a background worker performs the heavy work.

Out of Order Events

Events may not arrive in strict time order.

Example:

Your code must handle this gracefully, for example:

General rule:

Quick Acknowledgment vs Heavy Work

Your webhook endpoint should respond quickly. Heavy operations can cause:

Better approach:

  1. Perform minimal work in the webhook handler:
    • Verify the signature.
    • Do basic validation.
    • Store the event in the database or enqueue a job.
  2. Return HTTP 200 or 204.
  3. Let a background worker perform slow tasks, such as:
    • Intensive calculations.
    • External API calls.
    • Large database updates.
    • Sending emails.

This design improves reliability and makes your webhook system more resilient.

Example Webhook Flows

Payment Webhook Flow

Imagine you run an e commerce backend and use a payment provider.

1. During Checkout

  1. User places an order in your system.
  2. You create a payment session with the provider.
  3. You redirect the user to the provider payment page.
  4. You mark the order as pending in your database.

2. Payment Provider Configuration

In the provider dashboard, you configure:

3. Provider Sends Webhook

After the customer pays:

4. Your Backend Handles It

  1. Reads the raw body and header.
  2. Validates the signature.
  3. Validates JSON fields.
  4. Checks if the event id is already processed.
  5. If not, stores the event row and enqueues a "process payment" job.
  6. Returns 200 OK.

A background job then:

If any of these tasks fail, you can retry the job without needing another webhook.

Git Push Webhook Flow

Imagine you have a backend that runs tests whenever code is pushed to your repository.

1. Configure Webhook in Git Service

In your Git hosting platform:

2. Developer Pushes Code

3. Your CI Backend

  1. Validates signature.
  2. Parses the JSON.
  3. Checks if branch is main.
  4. Creates a new CI build record in your database.
  5. Enqueues a "run tests" job for that commit.
  6. Returns 204 No Content.

Later, your worker runs a test job and updates the build status.

Working with Third Party Webhook Providers

When you integrate with an external webhook provider, always read their webhook documentation carefully. You should understand:

You may also find:

Local Development and Tunnels

In development, your backend usually runs on localhost, which the provider cannot reach over the internet.

Developers use tunneling tools to expose a local server to the internet, such as:

Typical flow:

  1. Run local backend on http://localhost:8000.
  2. Start tunnel: it gives you a public URL, for example https://abcd.ngrok.io.
  3. Configure webhook URL in provider as https://abcd.ngrok.io/webhooks/payments.
  4. Now the provider sends requests to your laptop.

These tools often show an interface with:

This is very helpful when debugging webhook issues.

Common Pitfalls and Best Practices

Common Pitfalls

  1. Ignoring security:
    • Accepting all POSTs without verifying signatures.
    • No IP checks when they are available.
  2. Heavy work in webhook handler:
    • Long running tasks causing timeouts.
    • Provider retries because your response is too slow.
  3. Not handling duplicates:
    • Event id not stored.
    • Same event applied multiple times, for example customer is charged twice.
  4. Assuming events are ordered:
    • Later events overwriting more recent state.
  5. Hard failures on unknown event types:
    • New event types added by provider break your endpoint.
  6. No logging or observability:
    • Difficult to debug missing or failed events.

Best Practices Checklist

Webhook Best Practices:

  1. Always verify authenticity
    • Use HMAC signatures or other strong mechanisms.
    • Use constant time comparison functions.
  2. Make handlers idempotent
    • Use event ids and store processed events.
    • Safely handle duplicate deliveries.
  3. Acknowledge quickly
    • Verify and enqueue, then return 2xx.
    • Do heavy processing in background jobs.
  4. Validate payloads
    • Check required fields, types, and structure.
    • Reject clearly invalid requests with 4xx.
  5. Handle unknown event types gracefully
    • Log and ignore instead of failing the entire request.
  6. Design for out of order delivery
    • Update state using safe transitions.
    • Avoid assumptions about chronological order.
  7. Log important data
    • Log event id, type, and processing result.
    • Be careful not to log secrets.
  8. Monitor failures
    • Track number of failed events.
    • Set alerts for repeated failures.

By following these principles, you can build webhook integrations that are secure, reliable, and easier to maintain, and that work well together with your other REST API endpoints.

Views: 6

Comments

Please login to add a comment.

Don't have an account? Register now!