Using Webhooks

    Use webhooks to be notified about events that happen in a Stripe account.

    Interacting with a third-party API like Stripe's can introduce two problems:

    • Services not directly responsible for making an API request may still need to know the response of that request
    • Some events, like disputed charges and many recurring billing events, are not the result of a direct API request

    Webhooks solve these problems by letting you register a URL that we will notify anytime an event happens in your account. When the event occurs—for example, when a successful charge is made on a customer's subscription, Stripe creates an Event object. This object contains all the relevant information about what just happened, including the type of event and the data associated with that event. Stripe then sends the Event object to any URLs in your account's webhooks settings via an HTTP POST request. You can find a full list of all event types in the API docs.

    When to use webhooks

    Webhooks are only necessary for behind-the-scenes transactions. The results of most Stripe requests (e.g., creating charges or refunds) are reported synchronously to your code and don’t require webhooks for verification.

    If you only accept card payments using Stripe, webhooks are optional. Most payment methods using Sources require the use of webhooks so your integration can be notified about asynchronous changes to the status of Source and Charge objects.

    You might use webhooks as the basis to:

    • Update a customer's membership record in your database when a subscription payment succeeds
    • Email a customer when a subscription payment fails
    • Check out the Dashboard if you see that a dispute was filed
    • Make adjustments to an invoice when it's created (but before it's been paid)
    • Log an accounting entry when a transfer is paid

    Configuring your webhook settings

    Webhooks are configured in the webhooks settings section of the Dashboard. Clicking Add endpoint reveals a form to add a new URL for receiving webhooks.

    Stripe supports two webhook types: Account and Connect. You'll likely want to create an account webhook, unless you've created a Connect application.

    You can enter any URL you'd like to have events sent to, but this should be a dedicated page on your server, coded per the instructions below. The mode determines whether test events or live events are sent to this URL; if you want to send both live and test events to the same URL, you need to create two separate settings. You may add as many URLs as you like, and basic access authentication is supported.

    You can also choose to be notified of all event types, or only specific ones.

    Receiving a webhook notification

    Creating a webhook endpoint on your server is no different from creating any page on your website. With PHP, you might create a new .php file on your server; with a framework like Sinatra, you would add a new route with the desired URL.

    Webhook data is sent as JSON in the POST request body. The full event details are included and can be used directly, after parsing the JSON into an Event object.

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    require "json"
    
    # Using Sinatra
    post "/my/webhook/url" do
      # Retrieve the request's body and parse it as JSON
      event_json = JSON.parse(request.body.read)
    
      # Do something with event_json
    
      status 200
    end
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    import json
    from django.http import HttpResponse
    
    # Using Django
    def my_webhook_view(request):
      # Retrieve the request's body and parse it as JSON
      event_json = json.loads(request.body)
    
      # Do something with event_json
    
      return HttpResponse(status=200)
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    // Retrieve the request's body and parse it as JSON
    $input = @file_get_contents("php://input");
    $event_json = json_decode($input);
    
    // Do something with $event_json
    
    http_response_code(200); // PHP 5.4 or greater
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_BQokikJOvBiI2HlWgH4olfQ2";
    
    // Using Spark framework (http://sparkjava.com)
    public Object handle(Request request, Response response) {
      // Retrieve the request's body and parse it as JSON
      Event eventJson = APIResource.GSON.fromJson(request.body(), Event.class);
    
      // Do something with eventJson
    
      response.status(200);
      return "";
    }
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    // Using Express
    app.post("/my/webhook/url", function(request, response) {
      // Retrieve the request's body and parse it as JSON
      var event_json = JSON.parse(request.body);
    
      // Do something with event_json
    
      response.send(200);
    });
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    StripeConfiguration.SetApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    using System;
    using System.IO;
    using Microsoft.AspNetCore.Mvc;
    using Stripe;
    
    namespace workspace.Controllers
    {
        [Route("api/[controller]")]
        public class StripeWebHook : Controller
        {
            [HttpPost]
            public void Index() {
                var json = new StreamReader(HttpContext.Request.Body).ReadToEnd();
                var stripeEvent = StripeEventUtility.ParseEvent(json);
    
                // Do something with stripeEvent
            }
        }
    }
    

    Receiving webhooks with a CSRF-protected server

    If you're using Rails, Django, or another web framework, your site may automatically check that every POST request contains a CSRF token. This is an important security feature that helps protect you and your users from cross-site request forgery attempts. However, this security measure may also prevent your site from processing legitimate webhooks. If so, you may need to exempt the webhooks route from CSRF protection.

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    class StripeController < ApplicationController
      # If your controller accepts requests other than Stripe webhooks,
      # you'll probably want to use `protect_from_forgery` to add CSRF
      # protection for your application. But don't forget to exempt
      # your webhook route!
      protect_from_forgery :except => :webhook
    
      def webhook
        # Process webhook data in `params`
      end
    end
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    import json
    
    # Webhooks are always sent as HTTP POST requests, so we want to ensure
    # that only POST requests will reach your webhook view. We can do that by
    # decorating `webhook()` with `require_POST`.
    #
    # Then to ensure that the webhook view can receive webhooks, we need
    # also need to decorate `webhook()` with `csrf_exempt`.
    @require_POST
    @csrf_exempt
    def webhook(request):
      # Process webhook data in `request.body`
    

    Receiving webhooks with an HTTPS server

    If you use an HTTPS URL for your webhook endpoint, Stripe will validate that the connection to your server is secure before sending your webhook data. For this to work, your server must be correctly configured to support HTTPS with a valid server certificate. See our guide on SSL/TLS for details on setting this up.

    Responding to a webhook

    To acknowledge receipt of a webhook, your endpoint should return a 2xx HTTP status code. Any other information returned in the request headers or request body is ignored. All response codes outside this range, including 3xx codes, will indicate to Stripe that you did not receive the webhook. This does mean that a URL redirection or a "Not Modified" response will be treated as a failure.

    If a webhook is not successfully received for any reason, Stripe will continue trying to send the webhook once an hour for up to three days (in test mode, only five hourly retries are attempted). Webhooks cannot be manually retried after this time, though you can query for the event to reconcile your data with any missed events.

    When viewing a specific event information through the Dashboard, you can check how many times we've attempted to send an event to an endpoint by clicking on that endpoint URL in the Webhook details section. This will show you the latest response we received from your endpoint, along with a list of all attempted webhooks and the respective HTTP status codes we received.

    Best practices

    Before going live, test that your webhook is working properly. You can do so by sending dummy test events from the webhooks settings pane. To do so, be sure you are viewing test data, and add the endpoint where test events should be sent. Then click that endpoint's row to view details, and finally click the send a test webhook button. Understand that as these are fake, test events, they will not map to real customers, invoices, charges, or other objects in your account.

    If your webhook script performs complex logic, or makes network calls, it's possible the script would timeout before Stripe sees its complete execution. For that reason, you may want to have your webhook endpoint immediately acknowledge receipt by returning a 2xx HTTP status code, and then perform the rest of its duties.

    Webhook endpoints may occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent. One way of doing this is logging the events you've processed, and then not processing already-logged events. Additionally, we recommend verifying webhook signatures to confirm that received events are being sent from Stripe.

    Webhooks and API versions

    The structure of an Event object sent in a webhook is dictated by the API version in your account settings at the time of the event's occurrence. For example, if your account is set to an older API version, such as 2015-02-16, and you change the API version for a specific request via versioning, the Event object generated and sent to your endpoint is still based upon the 2015-02-16 API version.

    Event objects can never be changed once created. Subsequent updates to your account's API version do not retroactively alter existing Event objects. Fetching older events by calling /v1/events using a newer API version also has no impact on the structure of the received events.

    Test webhook endpoints can be set to either your API version in use or the most current API version. The Event sent to the webhook URL is structured for the endpoint's specified version. You can use endpoint versioning to help test the latest API version before upgrading to it.

    Checking signatures

    Stripe can optionally sign the webhook events it sends to your endpoint, allowing you to validate that they were not sent by a third-party. You can verify signatures using either our official libraries or manually using your own solution. To make use of signatures, you first need to retrieve your endpoint’s secret. Stripe then starts to sign each webhook sent to the endpoint. This signature is included in the Stripe-Signature header, and sent along with the event.

    You can obtain your endpoint’s secret in your webhooks settings in the Dashboard. Select an endpoint you want to obtain the secret for, then click the Click to reveal button. Each secret is unique to the endpoint it corresponds to. If you make use of multiple endpoints, you must obtain a secret for each one.

    Verifying signatures using our official libraries

    We recommend using one of our official libraries to verify signatures. This is done by providing the event payload, the Stripe-Signature header, and the endpoint’s secret. If verification fails, an error is returned.

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    # You can find your endpoint's secret in your webhook settings
    endpoint_secret = "whsec_..."
    
    # Using Sinatra
    post "/my/webhook/url" do
      payload = request.body.read
      sig_header = request.env['HTTP_STRIPE_SIGNATURE']
      event = nil
    
      begin
        event = Stripe::Webhook.construct_event(
          payload, sig_header, endpoint_secret
        )
      rescue JSON::ParserError => e
        # Invalid payload
        status 400
        return
      rescue Stripe::SignatureVerificationError => e
        # Invalid signature
        status 400
        return
      end
    
      # Do something with event
    
      status 200
    end
    
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"
    
    from django.http import HttpResponse
    
    # You can find your endpoint's secret in your webhook settings
    endpoint_secret = "whsec_..."
    
    # Using Django
    @csrf_exempt
    def my_webhook_view(request):
      payload = request.body
      sig_header = request.META['HTTP_STRIPE_SIGNATURE']
      event = None
    
      try:
        event = stripe.Webhook.construct_event(
          payload, sig_header, endpoint_secret
        )
      except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
      except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)
    
      # Do something with event
    
      return HttpResponse(status=200)
    
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    // You can find your endpoint's secret in your webhook settings
    $endpoint_secret = "whsec_...";
    
    $payload = @file_get_contents("php://input");
    $sig_header = $_SERVER["HTTP_STRIPE_SIGNATURE"];
    $event = null;
    
    try {
      $event = \Stripe\Webhook::constructEvent(
        $payload, $sig_header, $endpoint_secret
      );
    } catch(\UnexpectedValueException $e) {
      // Invalid payload
      http_response_code(400); // PHP 5.4 or greater
      exit();
    } catch(\Stripe\Error\SignatureVerification $e) {
      // Invalid signature
      http_response_code(400); // PHP 5.4 or greater
      exit();
    }
    
    // Do something with $event
    
    http_response_code(200); // PHP 5.4 or greater
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_BQokikJOvBiI2HlWgH4olfQ2";
    
    // Using Spark framework (http://sparkjava.com)
    public Object handle(Request request, Response response) {
      String payload = request.body();
      String sigHeader = request.headers("Stripe-Signature");
      Event event = null;
    
      try {
        event = Webhook.constructEvent(
          payload, sigHeader, endpointSecret
        );
      } catch (JsonSyntaxException e) {
        // Invalid payload
        response.status(400);
        return "";
      } catch (SignatureVerificationException e) {
        // Invalid signature
        response.status(400);
        return "";
      }
    
      // Do something with event
    
      response.status(200);
      return "";
    }
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    var stripe = require("stripe")("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    // You can find your endpoint's secret in your webhook settings
    const endpointSecret = "whsec_blSjcGhK1UHRbyGwXa9wSaK9FzLErNIS";
    
    // This example uses Express to receive webhooks
    const app = require("express")();
    
    // Retrieve the raw body as a buffer and match all content types
    app.use(require("body-parser").raw({type: "*/*"}));
    
    app.post("/webhook/example", (req, res) => {
      let sig = req.headers["stripe-signature"];
      let event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    
      // Do something with the event here
    
      // Return a response
      res.json({received: true});
    });
    
    app.listen(8000, () => console.log("Running on port 8000"));
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    StripeConfiguration.SetApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
    using System;
    using System.IO;
    using Microsoft.AspNetCore.Mvc;
    using Stripe;
    
    namespace workspace.Controllers
    {
        [Route("api/[controller]")]
        public class StripeWebHook : Controller
        {
            // You can find your endpoint's secret in your webhook settings
            const secret = "whsec_blSjcGhK1UHRbyGwXa9wSaK9FzLErNIS";
    
            [HttpPost]
            public void Index() {
                var json = new StreamReader(HttpContext.Request.Body).ReadToEnd();
                var stripeEvent = StripeEventUtility.ConstructEvent(json,
                    Request.Headers["Stripe-Signature"], secret);
    
                // Do something with the event here
            }
        }
    }
    

    Preventing replay attacks

    A replay attack is when an attacker intercepts a valid payload and its signature, and re-transmits them. To mitigate such attacks, Stripe includes a timestamp in the Stripe-Signature header. As this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can also decide to reject the payload.

    Our libraries have a default tolerance of five minutes between the timestamp and the current time. You can change this by providing an additional parameter when verifying signatures. We recommend that you use Network Time Protocol (NTP) to ensure that your server’s clock is accurate and synchronizes with the time on Stripe’s servers.

    The timestamp and signature are generated each time Stripe sends an event to your endpoint. If Stripe replays an event (e.g., your endpoint previously replied with a non-2xx status code), then a new signature and timestamp are generated for the new delivery attempt.

    Verifying signatures manually

    Although we recommend using our official libraries to verify webhook event signatures, you can use the following information to create a custom solution.

    The Stripe-Signature header contains a timestamp and one or more signatures. The timestamp is prefixed by t=, and each signature is prefixed by a “scheme”. Schemes start with v, followed by an integer. The only valid signature scheme at this time is v1.

    Stripe-Signature: t=1492774577,
        v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
        v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
    

    Note that newlines have been added in the example above for clarity, but a real Stripe-Signature header will be all one line.

    Signatures are generated using a hash-based message authentication code (HMAC) with SHA-256. To prevent downgrade attacks, you should ignore all other schemes that are not v1. To aid with testing, Stripe sends an additional signature with a fake v0 scheme for test mode events.

    It is possible to have multiple signatures with the same scheme. This can happen when you roll an endpoint’s secret from the Dashboard, and choose to keep the previous secret active for up to 24 hours. During this time, your endpoint has multiple active secrets and Stripe generates one signature for each secret.

    Step 1: Extract the timestamp and signatures from the header

    Split the header using the , character as the separator to get a list of elements. Then, split each element, using the = character as the separator, to get a prefix and value pair.

    The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature(s). All other elements can be discarded.

    Step 2: Prepare the signed_payload string

    This is achieved by concatenating:

    • The timestamp (as a string)
    • The character .
    • The actual JSON payload (i.e., the request’s body)

    Step 3: Determine the expected signature

    Compute a HMAC with the SHA256 hash function, using the endpoint’s signing secret as the key and the signed_payload string as the message.

    Step 4: Compare signatures

    Compare the signature(s) in the header to the expected signature. If a signature matches, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

    To prevent against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

    More information