Business-Payr
view release on metacpan or search on metacpan
lib/Business/Payr/Webhook.pm view on Meta::CPAN
package Business::Payr::Webhook;
=head1 NAME
Business::Payr::Webhook - class for verifying and parsing Payr webhook
notifications.
=head1 SYNOPSIS
use Business::Payr::Webhook;
# In your webhook endpoint handler:
my $Webhook = Business::Payr::Webhook->new(
body => $raw_request_body, # raw POST body string
signature => $x_payr_signature_header, # X-Payr-Signature header value
secret => $webhook_secret, # your Payr webhook secret
);
# Signature is verified during construction - an exception is thrown
# if the signature does not match. Always wrap in eval / try:
my $Payment = eval { $Webhook->resource };
if ( $@ ) {
warn "Webhook verification failed: $@";
return http_response( 400 );
}
if ( $Payment->is_payment_success ) {
my $amount_gbp = $Payment->amount / 100;
printf "Received %.2f %s for student %s\n",
$amount_gbp, $Payment->currency, $Payment->student_ref;
if ( $Payment->schedule_activated ) {
printf "Schedule %s activated; next instalment: %s\n",
$Payment->schedule_id, $Payment->next_installment_date // 'N/A';
}
}
elsif ( $Payment->is_payment_failed ) {
warn sprintf "Payment %s failed (%s): %s\n",
$Payment->payment_id,
$Payment->error_code // 'unknown',
$Payment->error_message // 'no detail';
}
elsif ( $Payment->is_payment_pending ) {
warn "Payment " . $Payment->payment_id
. " is pending: " . ( $Payment->pending_reason // 'unknown reason' );
}
=head1 DESCRIPTION
C<Business::Payr::Webhook> handles the receipt, cryptographic verification,
and parsing of webhook notifications sent by the Payr platform.
All webhooks are signed with HMAC-SHA256 using a secret shared between Payr
and your platform. The signature is carried in the C<X-Payr-Signature> HTTP
header. Verification is performed automatically during object construction; an
exception is thrown if the signature is missing, incorrect, or the payload
cannot be parsed.
B<Important:> You should always pass the B<raw request body> string as the
C<body> argument, before any deserialisation. Re-serialising a parsed
structure may produce different byte sequences and will cause verification to
fail.
Contact B<support@mypayr.co.uk> to configure your webhook endpoint URL and
receive your webhook secret.
=head1 SIGNATURE VERIFICATION DETAILS
Payr signs webhook payloads as follows:
=over
=item 1.
The JSON payload is serialised with compact separators (no spaces) and
lexicographically sorted keys.
=item 2.
An HMAC-SHA256 digest of the serialised payload is computed using the shared
webhook secret.
=item 3.
The hex-encoded digest is placed in the C<X-Payr-Signature> HTTP header.
=back
This module computes the same digest over the raw request body and compares
it against the header value using a constant-time comparison to prevent
timing attacks.
=head1 DEBUGGING
Set C<MOJO_CLIENT_DEBUG=1> for user agent and transport debug output when
fetching JWKS or other remote resources.
=cut
use strict;
use warnings;
use feature qw/ signatures /;
use Moose;
no warnings qw/ experimental::signatures /;
use namespace::autoclean;
use Carp qw/ croak confess /;
use Digest::SHA qw/ hmac_sha256_hex /;
use String::Compare::ConstantTime qw/ equals /;
use JSON;
use Business::Payr::Webhook::Payment;
=head1 ATTRIBUTES
=over
=item body (Str, required)
The raw (undecoded) HTTP request body string exactly as received from Payr.
Do B<not> deserialise and re-serialise this value before passing it in; the
signature is computed over the original byte sequence.
( run in 1.207 second using v1.01-cache-2.11-cpan-364913b4093 )