NAME

Business::Stripe::Webhook - A Perl module for handling webhooks sent by Stripe

SYNOPSIS

use Stripe::Webhook;

my $webhook = Stripe::Webhook->new(
    signing_secret                => 'whsec_...',
    api_secret                    => 'sk_test_...',
    invoice-paid                  => \&update_invoice,
    checkout-session-completed    => \&update_session,
);

my $result = $webhook->process();

if ($webhook->success()) {
    $webhook->reply(status => 'OK');
} else {
    $webhook->reply(error => $webhook->error());
}

sub invoice-paid {
    # Process paid invoice
    ...
}

sub checkout-session-completed {
    # Process checkout
    ...
}

DESCRIPTION

Business::Stripe::Webhook is a Perl module that provides an interface for handling webhooks sent by Stripe. It provides a simple way to verify the signature of the webhook, and allows the user to define a number of methods for processing specific types of webhooks.

This module is designed to run on a webserver as that is where Stripe webhooks would typically be sent. It reads the payload sent from Stripe from STDIN because Stripe sends an HTTP POST request. Ensure that no other module is reading from STDIN or Business::Stripe::Webhook will not get the correct input.

Workflow

The typical workflow for Business::Stripe::Webhook is to initally create an instance of the module and to define one or more Stripe events to listen for. This is done by providing references to your subroutines as part of the new method. Note that the webhook events you want to listen for need to be enabled in the Stripe Dashboard.

my $webhook = Stripe::Webhook->new(
    invoice-paid => \&sub_to_handle_paid_invoice,
);

The Stripe event names have a fullstop replaced with a minus sign. So, invoice.paid becomes invoice-paid.

Next is to process the webhook from Stripe.

my $result = $webhook->process();

This will call the subroutines that were defined when the module was created and pass them the event object from Stripe.

Finally, a reply is sent back to Stripe.

print reply(status => 'OK');

This produces a fully formed HTTP Response complete with headers as required by Stripe.

Reply time

Stripe requires a timely reply to webhook calls. Therefore, if you need to carry out any lengthy processing after the webhook has been sent, this should be done after calling the reply method and flushing STDOUT

use Stripe::Webhook;

my $webhook = Stripe::Webhook->new(
    signing_secret    => 'whsec_...',
    invoice-paid      => \&update_invoice,
);

$webhook->process();

# Send reply for unhandled webhooks
$webhook->reply();

sub invoice-paid {
    # Send reply quickly and flush buffer
    print $webhook->reply();
    select()->flush();
    
    # Process paid invoice which will take time then do not return
    ...
    exit;
}
    

Errors and Warnings

By default, any errors or warnings are sent to STDERR. These can be altered to instead go to your own subroutine to handle errors and/or warnings by defining these when create the object.

my $webhook = Stripe::Webhook->new(
    invoice-paid => \&sub_to_handle_paid_invoice,
    error        => \&my_error_handler,
    warning      => \&my_warning_handler,
);

Additionally, warnings can be turned off by setting the warning parameter to nowarn. Errors cannot be turned off.

METHODS

new

Creates a new Stripe::Webhook object.

my $webhook = Stripe::Webhook->new(
    signing_secret => 'whsec_...',
);

This method takes one or more parameters:

  • signing_secret: The webhook signing secret provided by Stripe. If omitted, the Stripe Signature will not be checked.

  • api_secret: The Stripe secret API Key - see https://stripe.com/docs/keys. Optional but will be required if the get_subscription method is needed.

  • stripe-event: One or more callbacks to the subroutines to handle the webhooks events sent by Stripe. See https://stripe.com/docs/api/events/list.

    To listen for an event, change the fullstop in the Stripe event name to a minus sign and use that as the parameter. The events you define should match the events you ask Stripe to send. Any events Stripe sends that do not have a callback defined will be ignored (unless all-webhooks is defined).

    Stripe event invoice.paid becomes invoice-paid Stripe event invoice.payment_failed becomes invoice-payment_failed

  • all-webhooks: A callback subroutine which will be called for every event received from Stripe even if a callback subroutine for that event has not been defined.

  • error: A callback subroutine to handle errors. If not defined, errors are sent to STDERR.

  • warning: A callback subroutine to handle warnings. If not defined, warnings are sent to STDERR. If set to nowarn, warnings are ignored.

success

Returns true if the last operation was successful, or false otherwise.

if ($webhook->success()) {
    ...
}

error

Returns the error message from the last operation, or an empty string if there was no error.

my $error = $webhook->error();

process

This method processes the webhook sent from Stripe. It checks the Stripe Signature if a signing_secret parameter has been included and calls the defined subroutine to handle the Stripe event. Each subroutine is passed a JSON decoded Event Object from Stripe.

my $result = $webhook->process();

This method takes no parameters.

Normally, the return value can be ignored. Returns undef if there was an error or warning.

check_signature

Checks the signature of the webhook to verify that it was sent by Stripe.

my $sig_ok = $webhook->check_signature();

This method takes no parameters.

Normally, this method does not need to be called. It is called by the process method if a signing_secret parameter was included when the object was created.

reply

Sends a reply to Stripe.

print reply(status => 'OK');

It takes one or more optional parameters.

Parameters passed to this method are then passed through to Stripe. These are available in the Stripe Dashboard and are especially useful for troubleshooting during development.

The following parameters are always passed to Stripe:

  • status: noaction if the event did not have a handler, success if the event was handled or failed if it wasn't

  • sent_to: An array containing the names of the callback subroutines that handled the event.

  • sent_to_all: true or false to indicate if the all-webhooks parameter was set

  • timestamp: The server time at which the webhook was handled

get_subscription

Retrieves a subscription object from Stripe. This is required to retrieve information such as the end of the current subscription period or whether the subscription is set to cancel after the current period.

my $response = $webhook->get_subscription($subscription_id, $secret_key);

This method takes two parameters:

  • $subscription_id: The ID of the subscription to retrieve. Required.

  • $secret_key: The secret API key to use to retrieve the subscription. Optional.

    This is usually supplied when the object is created but can be supplied when calling this method. If the API Key has alreay been supplied, this paramter will override the previous key.

An HTTP::Tiny response is returned representing the Subscription Object from Stripe - see https://perldoc.perl.org/HTTP::Tiny#request

Note: times sent from Stripe are in seconds since the epoch. If adding them to a database which would be a typical scenario, use the SQL FROM_UNIXTIME function:

$dbh->do("UPDATE table SET currentPeriodEnd = FROM_UNIXTIME( ? ) WHERE idSubscription = ?", undef, $response->{'current_period_end'}, $response->{'subscription'});

EXAMPLES

Here's an example of how to use the module to handle a webhook:

use Business::Stripe::Webhook;

my $webhook = Business::Stripe::Webhook->new(
    signing_secret => 'whsec_...',
    invoice-paid   => \&pay_invoice,
);

$webhook->process();

print $webhook->reply;

sub pay_invoice {
    my $event = $_[0];
    my $subscription = $event->{'data'}->{'object'}->{'subscription'};
}

Here's an example of how to use the module to retrieve a subscription object:

use Business::Stripe::Webhook;
use JSON::PP;

my $webhook = Business::Stripe::Webhook->new(
    api_secret => 'sk_...',
);

my $response = $webhook->get_subscription('sub_...');

if ($response->{'success'}) {
    my $subscription = decode_json($response->{'content'});
    ...
} else {
    my $error = $response->{'content'};
    ...
}

SEE ALSO

Stripe Subscriptions API

Business::Stripe::WebCheckout

Business::Stripe::Subscription

AUTHOR

Ian Boddison <ian at boddison.com>

BUGS

Please report any bugs or feature requests to bug-business-stripe-subscription at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=bug-business-stripe-subscription. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Business::Stripe::Subscription

You can also look for information at:

ACKNOWLEDGEMENTS

Thanks to the help and support provided by members of Perl Monks https://perlmonks.org/.

COPYRIGHT AND LICENSE

This software is copyright (c) 2023 by Ian Boddison.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.