Receiving webhooks

Validating webhooks

Sender IP

Our webhooks are sent from the following IP address

54.216.11.145

Verifying the signature

Our webhooks are signed, to ensure no one other than Marble can send webhooks to your (public) webhooks endpoint. You must absolutely verify the signature, using the endpoint secret that is returned when you created the endpoint, before you accept an incoming webhook.

Using a client library

You can use one of the following client libraries to verify the webhook signature:

import (
    "io"
    "net/http"
    convoy "github.com/frain-dev/convoy-go/v2"
)

func HandleWebhook(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(400)
    }

    webhook := convoy.NewWebhook(
        convoy.WebhookOpts{
            Hash: "SHA256",
            Secret: os.Getenv("WEBHOOKS_ENDPOINT_SECRET"),
            Payload: body,
            Encoding: "base64",
            SigHeader: "X-Convoy-Signature",
            Tolerance: 3600 * time.Second,
        }
    )
    err = webhook.VerifyRequest(r)
    if err != nil {
        w.WriteHeader(400)
    }

    // write webhook to queue.
    w.WriteHeader(200)
}
import { Webhook } from 'convoy.js';

// Using Express
app.post("/my/webhook/url", function(req, res) {
    // verfiy an advanced signature
    const webhook = new Webhook({
        header: 't=2048976161,v1=afdb90313acfa15a3fc425755ae651a204947710315bb2a90bccaa87fce88998,v1=fLBDCBUiX5iIs0L5zfNq45h23EkX1HAMpFF+2lHrnes=',
        payload: { email: '[email protected]' },
        secret: '8IX9njirDG',
        hash: 'sha256',
        encoding: 'base64',
        tolerance: 3600
    });

    if (webhook.verify()) {
        // Retrieve the request's body
        const event = req.body;
        // Do something with event
    }
    res.send(200);
});
class WebhookController < ApplicationController
  skip_before_action :authorize_request, only: :webhook

  def webhook
    body = request.body.read
    sig_header = request.headers['X-Convoy-Signature']

    begin
      convoy = Convoy::Webhook.new(secret = endpoint_secret, hash: "SHA256", encoding: "base64")
      match = convoy.verify body, sig_header
      raise Convoy::SignatureVerificationError, 'failed to verify signature' unless match
    end

    payload = JSON.parse(body)
    event_type = payload['event_type']

    # Handle the event
    case event_type
    when 'event.type'
      # write event to a queue for processing.
    else
      puts "Unhandled webhook event type: #{event_type}"
    end

  rescue JSON::ParserError,
         Convoy::SignatureVerificationError => e
    render status: 400, json: nil and return
  end

  private 

  def endpoint_secret
    ENV['WEBHOOK_ENDPOINT_SECRET']
  end
end

You should use SHA256 as the hash method, base64 encoding, and your endpoint's secret (see the "Setting up the webhooks" section).

Verifying manually

Alternatively, you can perform the verification "manually". We refer you to the documentation of Convoy, the service we use for sending webhooks.

Verify the timestamp

An optional security is to verify that the webhook timestamp (present in the header) is within a certain tolerance window from the current timestamp. This is used to protect you against a replay attack, that is to say an attack where a malicious third party intercepts webhooks destined to you and resends them at a later date.

You should also try to make your webhooks handling idempotent, but by using timestamp verification you do not need to perform any database reads to reject an old webhook.

Please note that the timestamp included in the signature represents the time when the webhook was sent (generated for each attempt), not the time of the original trigger event (which can be found in the payload body).

Rotating the secret

We allow a seamless rotation of the endpoint's secret. In this process, a new secret is created, and your consumer should test that any of the signatures present in the header match the computed signature (this is done automatically by the client libraries). The old secret remains valid for a transit period (that can be configured).

The app interface for secret rotation is not yet implemented, but will follow soon.

Retries

If you respond with any other status code than 2XX, or if you respond later than the configured endpoint timeout, the webhook will be retried. Webhooks will be retried up to 10 times.

This is another reason for implementing idempotent handling of the webhooks, because a given webhook can be delivered more than once.