HomeGuidesReference↗ Forage Dashboard
Log In
Guides

How to Configure Webhooks

Set up a webhook endpoint, register it in the Forage dashboard, and verify signatures

To start receiving webhooks from Forage, complete the following steps. (See Webhook Events for available events and payload schemas.)

To configure a webhook:

  1. Set up your receiving endpoint
  2. Add the webhook to the Forage dashboard
  3. Verify the webhook signature
  4. Verify the webhook timestamp

Step 1: Set up your receiving endpoint

Your endpoint should be publicly available and accept a POST request with a JSON payload. Confirm that your endpoint returns a 200 when you receive the webhook. If the 200 isn't received, then Forage retries the webhook with an exponential backoff.

Example endpoints

# Python Django Example
@api_view(http_method_names=["POST"])
def handle_webhook(request):
    event = request.data
    received_signature = request.headers.get("Webhook-Signature")
    # Validate signature (more instructions in Step 3)

    event_type = event["type"]

    # Handle the event
    if event_type == "ORDER_STATUS_UPDATED":
        # do something with order
        order_ref = event["data"]["order_ref"]
        pass
    elif event_type == "PAYMENT_STATUS_UPDATED":
        payment_ref = event["data"]["payment_ref"]
        pass
    elif event_type == "REFUND_STATUS_UPDATED":
        refund_ref = event["data"]["refund_ref"]
        pass

    # Return a 200 response to acknowledge receipt of the event
    return HttpResponse(status=200)

Always return 2XX

Your endpoint should return 2XX for any well-formed payload, whether or not you act on the data.

Step 2: Add the webhook to the Forage dashboard

Navigate to "Webhooks" in the Forage dashboard. From the Webhooks page, click "+Add webhook" and provide the requested information:

  • Webhook URL: The endpoint that receives the webhook (you set this up in Step 1)
  • Status: Whether the webhook is "Active", currently being listened for, or "Inactive"
  • Subscribed Events: The Forage webhook events to listen for
  • Notification Email: An email address that Forage can use to contact you in case of webhook errors

Step 3: Verify the webhook signature

Forage uses an HMAC with SHA-256 to sign its webhooks. The signature is passed in the webhook payload Webhook-Signature header, for example:

Webhook-Signature: be521964c21a8eb7f5ddd0f45b5bf83d8904a4dd4238b7b14f3eee73fa9c21f2

As a security measure, Forage requires that you validate the Webhook-Signature value against a signature that you calculate using the webhook's secret from the Forage dashboard. This verifies that the webhook message came from Forage.

To calculate the signature, retrieve the webhook's secret from the Forage dashboard and convert the webhook payload to a JSON string. Pass the secret as the key and the stringified payload as the message to a hash function to compute the HMAC with SHA-256. Finally, compare the signature that you computed with the Webhook-Signature.

flowchart TD
    A[Receive webhook POST] --> B[Extract Webhook-Signature header]
    B --> C[Get webhook secret from Forage dashboard]
    C --> D[Compute HMAC-SHA256\nsecret + raw request body]
    D --> E{Constant-time compare:\ncomputed == received?}
    E -->|Match| F[Process webhook payload]
    E -->|No match| G[Reject: discard payload]

Example comparison

⚠️

Use the raw request body

Forage calculates the signature using the webhook's raw request body. To avoid miscalculating the signature, make sure that any framework that you're using doesn't manipulate the request body. For example, you might need to skip parsing if the framework removes whitespaces (e.g. Express.js).

⚠️

Use constant-time comparison

Use a constant-time string comparison to compare the two signatures to avoid timing attacks.

import hashlib
import hmac

def get_hmac_string(secret, message):
    return hmac.new(
        secret.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

# Django example
@api_view(http_method_names=["POST"])
def webhook_view(request):
    webhook_payload = request.body # ie. "{\"ref\": \"6ce5bdb204\", \"created\": \"2023-06-23T18:48:13.791077+00:00\", \"type\": \"ORDER_STATUS_UPDATED\", \"data\": {\"order_ref\": \"3b96a5312a\", \"status\": \"canceled\", \"snap_total\": \"20.00\", \"ebt_cash_total\": \"20.00\", \"remaining_total\": \"0.00\"}}"
    my_secret = "wh_secretabc123"
    # Compute expected signature
    expected_signature = get_hmac_string(my_secret, webhook_payload)

    received_signature = request.headers.get("Webhook-Signature")

    # Assert that computed signature above equals Webhook-Signature header using constant-time string comparison
    if not hmac.compare_digest(expected_signature, received_signature):
        raise SignatureError()

Step 4: Verify timestamp

To further validate the webhook, compare the created value in the payload against your system's timestamp for the corresponding resource. For example, confirm that the created value of a PAYMENT_STATUS_UPDATED webhook and the timestamp for when the payment was created in your system are within a few seconds, or whatever your risk tolerance is.

Related documentation