Webhook Verification

The SDK provides utilities to verify webhook signatures, ensuring that incoming webhook payloads genuinely originated from Felloh.


Verifying Signatures

Felloh signs every webhook request with an HMAC-SHA256 signature sent in the X-Signature header. The Felloh::Webhooks module provides two exportable functions to verify this signature:

  • verify_webhook_signature — returns 1 (true) or 0 (false)
  • assert_webhook_signature — throws Felloh::Error::WebhookSignature if invalid

Both use timing-safe comparison to prevent timing attacks.

Parameters

  • Name
    payloadrequired
    Type
    string
    Description

    The raw request body. Must not be parsed or modified — use the raw body as received.

  • Name
    signaturerequired
    Type
    string
    Description

    The value of the X-Signature header from the webhook request.

  • Name
    secretrequired
    Type
    string
    Description

    Your webhook signing secret from the Felloh dashboard.

Boolean Check

use Felloh::Webhooks qw(verify_webhook_signature);

my $is_valid = verify_webhook_signature(
    payload   => $raw_request_body,
    signature => $request_headers->{'x-signature'},
    secret    => 'your-webhook-secret',
);

unless ($is_valid) {
    # Return 401
    print "Status: 401\n";
    print "Invalid signature\n";
    exit;
}

# Process the webhook...

Assert (dies on failure)

use Felloh::Webhooks qw(assert_webhook_signature);
use Felloh::Error;

eval {
    assert_webhook_signature(
        payload   => $raw_request_body,
        signature => $request_headers->{'x-signature'},
        secret    => 'your-webhook-secret',
    );
};
if ($@ && $@->isa('Felloh::Error::WebhookSignature')) {
    # Return 401
    print "Status: 401\n";
    print "Invalid signature\n";
    exit;
}

# Signature is valid, process the webhook...

Mojolicious Example

When using Mojolicious, use $c->req->body to get the raw request body for signature verification.

Mojolicious Webhook Handler

use Mojolicious::Lite;
use Felloh::Webhooks qw(assert_webhook_signature);
use Felloh::Error;
use JSON::PP qw(decode_json);

post '/webhooks/felloh' => sub {
    my ($c) = @_;
    my $body = $c->req->body;

    eval {
        assert_webhook_signature(
            payload   => $body,
            signature => $c->req->headers->header('X-Signature'),
            secret    => $ENV{FELLOH_WEBHOOK_SECRET},
        );
    };
    if ($@ && $@->isa('Felloh::Error::WebhookSignature')) {
        return $c->render(text => 'Invalid signature', status => 401);
    }

    my $event = decode_json($body);
    app->log->info("Webhook received: $event->{type}");

    $c->render(text => 'OK', status => 200);
};

app->start;

Dancer2 Example

When using Dancer2, use request->body to get the raw request body.

Dancer2 Webhook Handler

use Dancer2;
use Felloh::Webhooks qw(assert_webhook_signature);
use Felloh::Error;
use JSON::PP qw(decode_json);

post '/webhooks/felloh' => sub {
    my $body = request->body;

    eval {
        assert_webhook_signature(
            payload   => $body,
            signature => request->header('X-Signature'),
            secret    => $ENV{FELLOH_WEBHOOK_SECRET},
        );
    };
    if ($@ && $@->isa('Felloh::Error::WebhookSignature')) {
        status 401;
        return 'Invalid signature';
    }

    my $event = decode_json($body);
    debug "Webhook received: $event->{type}";

    status 200;
    return 'OK';
};

start;