Skip to content
Back to blog

Technologies

Payment Webhooks: The Complete Guide to a Robust and Secure Architecture

Master payment webhook architecture. This technical guide for developers covers everything from idempotency to signature verification, ensuring a reliable and secure integration for your payment notifications.

Bridge Pay editorial team

14 min read

Payment Webhooks: The Complete Guide to a Robust and Secure Architecture
Table of contents
  1. Beyond Polling, the Era of Event-Driven Architecture
  2. Prerequisites
  3. Detailed Steps for a Robust Integration
  4. Code/Configuration Example (Python/Flask)
  5. Verification
  6. Troubleshooting: Common Issues and Solutions
  7. Conclusion

Beyond Polling, the Era of Event-Driven Architecture

In the world of online payments, real-time communication between your application and your payment gateway is crucial. Historically, the simplest method was 'polling': querying the API at regular intervals to check if a payment's status had changed. This approach, while straightforward, is inefficient, resource-intensive, and introduces unacceptable latency.

Webhooks flip this paradigm. Instead of constantly asking for updates, your system waits to be notified. A webhook is an HTTP callback triggered by an event. When a payment is successful, a refund is processed, or a dispute is created, the payment gateway (like Bridge Pay) sends an instant notification to a URL you specify. This is the foundation of a modern, responsive, and scalable event-driven architecture.

According to an internal study, webhook-based payment systems process order confirmations 99.8% faster than those based on polling every 60 seconds, drastically improving the post-purchase customer experience.

This in-depth tutorial will guide you through the essential steps to build a payment webhook management system that is not only functional but also secure, resilient, and production-ready. We will cover architecture, security, error handling, and the best practices we apply at Bridge Pay for our clients in high-risk sectors, where reliability is non-negotiable.

Prerequisites

Before diving into the code and architecture, make sure you have the following:

  • A Bridge Pay Account: A sandbox account is sufficient to follow this tutorial. It will give you access to the developer dashboard to configure endpoints and obtain your secret keys.
  • A Server-Side Development Environment: An application capable of receiving HTTP POST requests. The examples will use Python (Flask), but the principles are applicable to any language (Node.js, PHP, Ruby, Java, etc.).
  • Basic Knowledge of APIs and JSON: You should be comfortable handling HTTP requests and parsing JSON-formatted data.
  • A Tunneling Tool (for local testing): A service like ngrok is essential for exposing your local development server to the public internet, allowing Bridge Pay's webhooks to reach it.

Detailed Steps for a Robust Integration

Step 1: Configure Your Endpoint and Acknowledge Receipt Immediately

The first step is to create a URL on your server, known as a "webhook endpoint," which will listen for incoming notifications. The golden rule is to make this endpoint as fast and lightweight as possible. Its only initial responsibility is to acknowledge receipt of the event and place it in a queue for later processing.

Why is this crucial? Webhook services, including Bridge Pay, have timeouts. If your server takes too long to respond (for example, because it's trying to process a complex order, send an email, and update 5 database tables all at once), the connection will be dropped. The webhook service will consider the delivery a failure and attempt to resend the same event later, creating potential duplicates and unnecessary load.

Your endpoint must therefore:

  1. Receive the POST request.
  2. (Optional but recommended) Place the payload (the raw JSON) into a queue (like RabbitMQ, Redis Queue, or AWS SQS).
  3. Immediately respond with an HTTP 200 OK status.

This approach decouples event reception from its processing. A separate process (a "worker") will then consume events from the queue and execute the complex business logic. This ensures your endpoint responds in milliseconds, avoiding timeouts and unnecessary retries.

Step 2: Secure the Endpoint with Signature Verification

A webhook endpoint is a public URL. Anyone can send data to it. It is therefore imperative to verify that every incoming request actually comes from Bridge Pay and not from a malicious actor trying to inject fake transactions into your system. This is where signature verification comes in.

The mechanism is based on an HMAC-SHA256 hash:

  1. Bridge Pay generates a "webhook signing secret" (whsec_...) that only you know.
  2. For each event sent, we create a signature by hashing the request body (the JSON payload) with this secret key.
  3. This signature is added to the request in an HTTP header, typically Bridge-Pay-Signature.
  4. Upon receipt, your server must recalculate the signature using the same method (payload + secret key) and compare it to the one received in the header.

If the signatures match, you can be certain the request is authentic. If they don't match, you must ignore the request and reject it with a 400 Bad Request error. For enhanced security, the signature header also includes a timestamp to allow you to reject requests that are too old, thereby protecting against replay attacks.

Important: Always perform the signature comparison using a constant-time string comparison function to prevent timing attacks.

Step 3: Ensure Idempotency to Prevent Duplicate Processing

"At-least-once" is the standard delivery contract for webhooks. This means that even under perfect network conditions, you might receive the same event multiple times. Your system must be "idempotent," meaning it can process the same event multiple times without creating unwanted side effects (like charging a customer twice or shipping the same product twice).

The most robust solution is to use the unique event identifier. Every webhook sent by Bridge Pay includes a unique id (e.g., evt_1KFZAmFzYfVdCqZy1aBcD3e4).

The idempotent processing flow is as follows:

  1. When your worker takes an event from the queue, it extracts the unique event ID.
  2. It checks a database (a simple SQL table or a key in Redis) to see if this ID has already been processed.
  3. If the ID has already been processed: The worker ignores the event and moves on to the next one.
  4. If the ID has not been processed: The worker executes the business logic. Once completed successfully, it records the event ID in the database to mark it as processed.

This check should be atomic. If you are using a transactional database, you can insert the ID and execute your business logic in the same transaction. If it fails, the transaction is rolled back, and the ID is not saved, allowing for a safe retry.

Step 4: Understand and Use the Retry Policy

Your server won't always be available. It might be deploying, experiencing a database outage, or encountering an unexpected error. A reliable payment gateway must anticipate these issues.

Bridge Pay includes an automatic retry policy with an exponential backoff mechanism. If your endpoint doesn't respond with a 2xx status (like 200 OK), we consider the delivery a failure and will retry:

  • 1 minute after the first failure.
  • 5 minutes after the second.
  • 15 minutes after the third.
  • ...and so on, over a period of up to 72 hours.

Your responsibility is to return the appropriate HTTP status code:

  • 200 OK (or any other 2xx): You have successfully received the event. Bridge Pay will stop retrying.
  • 5xx (e.g., 500, 503): A temporary error occurred on your end. Bridge Pay should retry later according to its policy.
  • 4xx (e.g., 400, 401): A client-side error. This could mean an invalid signature issue or a malformed request. Bridge Pay will NOT attempt to resend a webhook that received a 4xx code.

This combined strategy ensures maximum resilience. If your service experiences a short outage, you will automatically receive the missed notifications once it's back online, without any manual intervention.

Step 5: Do Not Trust the Order of Events

A common pitfall is to assume that webhooks will arrive in the chronological order they were created. For example, assuming customer.subscription.created will always arrive before invoice.payment_succeeded. Due to the distributed nature of the internet and retry policies, delivery order is never guaranteed.

An invoice.payment_succeeded event might be delivered instantly, while a customer.subscription.created event sent a few seconds earlier might fail and only be redelivered 5 minutes later. If your code depends on the order, your system will end up in an inconsistent state.

The best practice to work around this is to treat webhooks not as a definitive source of state, but as a simple notification that prompts you to check the object's current state. When you receive a webhook for an object (e.g., invoice.paid for invoice inv_123), use the Bridge Pay API to fetch the full invoice object. GET /v1/invoices/inv_123.

The object returned by the API will always contain the most recent and canonical state. Your logic should be based on this state, not on the potentially outdated content of the webhook payload. This makes your system resilient to ordering and latency issues.

Step 6: Monitor, Log, and Debug Effectively

In production, a system without visibility is a ticking time bomb. Webhook management is no exception.

Logging: Your service should log every incoming webhook. Store the event ID, event type, and the processing outcome (e.g., [INFO] Received event evt_123 (invoice.paid). Authenticated successfully. Queued for processing. or [WARN] Signature verification failed for event evt_456.).

Monitoring and Alerts: Use tools like Datadog, Prometheus, or Sentry to monitor your endpoint. Configure alerts for anomalous situations:

  • A spike in 5xx responses.
  • A sudden drop in webhook volume to zero.
  • A high rate of signature verification failures.

Debugging: The Bridge Pay dashboard is your best friend. In the Webhooks section, you'll find a detailed log of every delivery attempt for each event, including the full payload, the headers sent, and your server's response status code. If a webhook fails, you can inspect the exact reason and even trigger a manual resend once you've fixed the issue on your server.

Code/Configuration Example (Python/Flask)

Here is a Python code example using the Flask micro-framework that implements signature verification and basic idempotency handling.


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

app = Flask(__name__)

# Simulates a database for processed event IDs
processed_event_ids = set()

# The secret key for your webhook endpoint (retrieve from the Bridge Pay dashboard)
BRIDGE_PAY_WEBHOOK_SECRET = 'whsec_your_secret_key_here'

@app.route('/webhooks/bridgepay', methods=['POST'])
def bridgepay_webhook():
    # 1. Verify the signature
    signature_header = request.headers.get('Bridge-Pay-Signature')
    if not signature_header:
        print("Signature header missing")
        abort(400)

    # The payload is the raw request body
    payload = request.get_data()

    try:
        # Recalculate the signature
        expected_signature = hmac.new(
            key=BRIDGE_PAY_WEBHOOK_SECRET.encode('utf-8'),
            msg=payload,
            digestmod=hashlib.sha256
        ).hexdigest()
    except Exception as e:
        print(f"Error calculating signature: {e}")
        abort(500)

    # Securely compare signatures
    if not hmac.compare_digest(expected_signature, signature_header):
        print("Signature mismatch")
        abort(400)

    # 2. Parse the event and check for idempotency
    try:
        event = json.loads(payload)
    except json.JSONDecodeError:
        print("Invalid JSON payload")
        abort(400)

    event_id = event.get('id')
    if not event_id:
        print("Event ID missing")
        abort(400)

    if event_id in processed_event_ids:
        print(f"Event {event_id} already processed (idempotency check)")
        return 'OK', 200

    # 3. Business logic (here, we just print it)
    # IN A REAL APP, THIS WOULD BE SENT TO A QUEUE
    print(f"Processing event {event_id} of type {event['type']}")
    # ... execute your business logic here ...
    print(f"Data: {event['data']['object']}")

    # 4. Mark the event as processed
    processed_event_ids.add(event_id)

    # 5. Acknowledge receipt
    return 'OK', 200

if __name__ == '__main__':
    app.run(port=5000)

Verification

How can you verify that your integration is working?

  1. Start your local server and expose it with ngrok.
  2. In the Bridge Pay dashboard, enter your ngrok URL as the endpoint and click "Send a test event".
  3. Observe your server's logs. You should see messages indicating the event was received, the signature was successfully verified, and the event was processed.
  4. Click "Send a test event" again. Your server should now log a message indicating that the event has already been processed (idempotency test).
  5. Check your actual database (e.g., an order status) to ensure the expected action occurred the first time, but not the second.

If all these steps succeed, your basic implementation is correct.

Troubleshooting: Common Issues and Solutions

Issue: My Bridge Pay dashboard shows 5xx errors for my webhooks.

Possible Causes: Your server might be offline, have crashed, or took too long to respond. This is the most common problem and is almost always related to having too much heavy logic in the webhook handler.
Solution: Ensure your application is running. Check your server logs for any errors or crashes. Implement a queueing system so your endpoint responds in under 2 seconds, delegating heavy processing to an asynchronous worker.

Issue: I'm getting signature mismatch errors.

Possible Causes: The secret key is incorrect. More subtly: you are not using the raw request body to generate the signature. If your framework parses and reformats the JSON before you can access it, the signature will no longer match.
Solution: Double-check the secret key by copying and pasting it again from the Bridge Pay dashboard. Ensure your code is accessing the raw, unmodified body of the POST request before any JSON parsing, just as request.get_data() does in the Flask example.

Issue: My webhooks are never received on my development machine.

Possible Causes: Your server is running on localhost, which is not accessible from the public internet. A firewall is blocking incoming requests.
Solution: Use ngrok to create a secure tunnel. The command ngrok http 5000 (if your app is running on port 5000) will generate a public URL like https://xxxxx.ngrok.io. Use this URL in your webhook endpoint configuration on Bridge Pay. This allows Bridge Pay's servers to reach your local machine.

Conclusion

Implementing a robust payment webhook architecture is an essential investment for any business that processes online transactions. By following the principles of immediate response, signature verification, idempotency, and having a clear strategy for error handling and monitoring, you build a reliable system that can scale with your business and handle the unexpected effortlessly.

At Bridge Pay, we have designed our webhook systems to be as transparent and resilient as possible, giving you the tools to build first-class integrations. A correct integration will save you valuable time in debugging and maintenance while guaranteeing the integrity of your transaction data.

Ready to take the next step? For code examples in other languages and step-by-step integration guides, check out our complete technical documentation.

Turned down elsewhere?

An analyst who knows your sector reviews your application. First response within 24-48h, no setup fees.

  • PCI DSS
  • No setup fees
  • First response within 24-48h