HMAC Verification

Overview

Webhooks now carry a hash-based message authentication code (HMAC) header called x-signature, which contains a base64 SHA256 hash that can be used to verify the validity of webhooks.

Validation

The x-signature header's value is generated by running a SHA256 function against the JSON webhook body with whitespace removed, with the secret key being the string of the token used to create the resource.

📘

Note

Webhooks are sent to the notificationURL of the resource. See Notifications (IPN) for more information. Your application will need to listen for webhooks on an HTTPS endpoint.

  1. Receive the webhook
  2. Read the webhook body
  3. Generate an HMAC signature using your language's crypto library's SHA256 function against the webhook body using the token as the secret key
  4. Compare your output to the x-signature header of the webhook

If the results match, then it is safe to process the webhook. Otherwise, it should be ignored, and perhaps logged and analyzed depending on your preference.

Examples

The examples below are simplified to show the logic for verifying a webhook. Please note that they not take into account your application's framework, platform, structure, typing, etc. which should be considered when writing your actual implementation.

using System;
using System.Security.Cryptography;
using System.Text;

public class WebhookVerifier
{
  public static bool Verify(string signingKey, string sigHeader, string webhookBody)
  {
    var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey));
    byte[] signatureBytes = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(webhookBody));
    string calculated = Convert.ToBase64String(signatureBytes);
    bool match = sigHeader.Equals(calculated);

    Console.WriteLine("header:    : " + sigHeader);
    Console.WriteLine("calculated : " + calculated);
    Console.WriteLine("match      : " + match);

    return match;
  }
}
import static java.nio.charset.StandardCharsets.*;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class WebhookVerifier {
  public static Boolean verify(String signingKey, String sigHeader, String webhookBody) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
    String algorithm = "HmacSHA256";
    
    Mac mac = Mac.getInstance(algorithm);
    SecretKeySpec secretKeySpec = new SecretKeySpec(signingKey.getBytes(UTF_8), algorithm);
    mac.init(secretKeySpec);
    
    byte[] signatureBytes = mac.doFinal(webhookBody.getBytes(UTF_8));
    
    String calculated = Base64.getEncoder().encodeToString(signatureBytes);
    Boolean match = sigHeader.equals(calculated);

    System.out.println("header     : " + sigHeader);
    System.out.println("calculated : " + calculated);
    System.out.println("match      : " + match);

    return match;
  }
}
<?php

class WebhookVerifier
{
  public static function verify($signing_key, $sig_header, $webhook_body)
  {
    $hmac = base64_encode(hash_hmac(
      'sha256',
      $webhook_body,
      $signing_key,
      true
    ));

    $match = boolval($sig_header === $hmac) ? 'true' : 'false';

    print_r([
      'header' => $sig_header,
      'calculated' => $hmac,
      'match' => $match
    ]);

    return $match;
  }
}
import hmac
import hashlib
import base64

class WebhookVerifier:

  @staticmethod
  def verify(signing_key, sig_header, webhook_body):
    signing_key_b = bytes(signing_key, 'UTF-8')
    body_b = bytes(webhook_body, 'UTF-8')
    h = hmac.new(signing_key_b, body_b, hashlib.sha256)
    calc = base64.b64encode(h.digest()).decode()
    match = (sig_header == calc)
    
    print(f'header    : {sig_header}')
    print(f'calculated: {calc}')
    print(f'match     : {match}')

    return match
import crypto from 'node:crypto';

class WebhookVerifier {
  public verify(signingKey: string, webhookBody: string, sigHeader: string): boolean {
    const hmac = crypto.createHmac('sha256', signingKey);
    hmac.update(webhookBody);

    const digest = hmac.digest('base64');
    const match = (sigHeader === digest);

    console.log(`header     : ${sigHeader}`);
    console.log(`calculated : ${digest}`);
    console.log(`match      : ${match}`);   

    return match;
  }
}