Webhook Signing Methods Compared: HMAC vs Public-Key

Webhook Signing Methods Compared (HMAC vs Public-Key Signatures)

Webhook signing ensures the authenticity of incoming requests. This guide helps you compare webhook signing methods hmac vs rsa vs ecdsa across popular providers like Stripe, Paddle, SendGrid, and Mailgun. You’ll also learn how to design a unified verification layer in Laravel.

Two families of webhook signatures

Shared secret HMAC (fast, simple)

HMAC-based signatures use a shared secret to generate a hash of the payload. This method is:

  • Fast and computationally inexpensive.
  • Simple to implement but requires secure secret management.

This family is the usual choice when you need to compare webhook signing methods hmac vs rsa vs ecdsa for performance and simplicity trade-offs.

Public-key signatures (rotation-friendly, more moving parts)

Public-key signatures use asymmetric cryptography, where the provider signs the payload with a private key, and you verify it with a public key. This method is:

  • Rotation-friendly (keys can be updated without sharing secrets).
  • More complex, requiring key management and additional processing.

Suggested diagram: A comparison table showing HMAC vs public-key methods (e.g., speed, complexity, rotation).

What major providers do (high-level)

Stripe: verify using payload + Stripe-Signature + endpoint secret

Stripe signs the raw payload and includes the signature in the Stripe-Signature header. Use their library or manually verify by:

  1. Extracting the timestamp and signature from the header.
  2. Generating an HMAC-SHA256 hash of the payload.
  3. Comparing the computed hash with the signature.

Learn more in Stripe Docs.

Paddle: secret-based signature verification

Paddle uses a shared secret to sign the payload. Verify by:

  1. Concatenating the payload fields.
  2. Generating an HMAC-SHA256 hash.
  3. Comparing the computed hash with the signature.

Learn more in Paddle Developer Docs.

SendGrid: signed event webhook + public key management

SendGrid signs webhook events using a private key. Verify by:

  1. Fetching the public key from their endpoint.
  2. Verifying the signature against the payload.

Learn more in Twilio Docs.

Mailgun: timestamp+token → HMAC-SHA256 → compare to signature

Mailgun includes a timestamp, token, and signature in webhook requests. Verify by:

  1. Concatenating the timestamp and token.
  2. Generating an HMAC-SHA256 hash.
  3. Comparing the computed hash with the signature.

Learn more in Mailgun Docs.

Mini incident: A team once ignored Mailgun’s timestamp validation, leading to replay attacks. Always validate timestamps.

Designing your verification layer (adapter pattern)

Micro checklist:

  • Detect provider by signature headers
  • Always verify using the raw body
  • Normalize events to verified / rejected / replay suspected
  • Centralize logging for mismatches

Identify provider by headers

Use headers to determine the provider and route the request to the appropriate verification logic. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
final class WebhookVerifier
{
    public static function verify(\Illuminate\Http\Request $r): bool
    {
        $raw = $r->getContent();

        if ($r->hasHeader('X-SP-Signature')) {
            return self::verifySendPromptly($r, $raw);
        }

        if ($r->hasHeader('Stripe-Signature')) {
            return self::verifyStripe($r, $raw); // implement per Stripe docs
        }

        if ($r->hasHeader('Paddle-Signature')) {
            return self::verifyPaddle($r, $raw); // implement per Paddle docs
        }

        // Mailgun uses timestamp/token/signature fields (often form-encoded)
        if ($r->input('timestamp') && $r->input('token') && $r->input('signature')) {
            return self::verifyMailgun($r);
        }

        return false;
    }

    private static function verifySendPromptly($r, string $raw): bool
    {
        $ts = $r->header('X-SP-Timestamp');
        $sig = $r->header('X-SP-Signature');
        $expected = hash_hmac('sha256', $ts.'.'.$raw, config('services.sendpromptly.webhook_secret'));
        return hash_equals($expected, $sig);
    }

    // verifyStripe / verifyPaddle / verifyMailgun intentionally left as provider-specific implementations
}

Always verify using raw body

Always use the raw body for signature generation. Parsed payloads can introduce whitespace or order changes, invalidating the signature.

Emit consistent “verified / rejected / replay suspected”

Standardize your verification responses to:

  • verified: Signature matches.
  • rejected: Signature mismatch.
  • replay suspected: Timestamp outside the allowed window.

Start with Sample Project and confirm verification in Message Log.

Operational choices

Secret rotation

Rotate secrets regularly to minimize exposure. Use environment variables to manage secrets securely.

Timestamp windows + replay protection

Validate timestamps within a ±5-minute window to prevent replay attacks. Reject requests outside this window.

Logging without leaking keys

Log verification failures without exposing secrets or signatures. For example:

1
Log::warning('Webhook verification failed', ['provider' => $provider, 'reason' => $reason]);

Micro checklist:

  • Rotate secrets every 90 days.
  • Validate timestamps within ±5 minutes.
  • Use environment variables for secrets.
  • Log failures securely.
  • Test with local curl vectors.

Common failure modes

  1. Not using raw body (breaks Stripe/Paddle-style verification frequently). (Stripe Docs)
  2. Wrong secret/public key (environment mismatch). (Stripe Docs)
  3. Signature parsed incorrectly (multiple signatures, comma-separated parts).
  4. Timestamp window ignored → replay attacks possible (Mailgun explicitly recommends replay protection). (Mailgun Docs)
  5. Logging secrets/signatures → incident risk.
  6. Treating verification as “optional in dev” → production drift.

Learn more about common mismatch causes.

Related docs:

Conclusion

If you integrate multiple providers, standardize on one ‘verified’ event and audit it in Message Log.

Key takeaways

  • Compare HMAC and public-key methods based on your needs.
  • Always verify using raw body and validate timestamps.
  • Rotate secrets regularly and log failures securely.
  • Use an adapter pattern to handle multiple providers.
  • Test with local curl vectors to debug issues.