Authentication & Verification
There are three main areas of programmatic interaction with the Hookdeck platform that make use of authentication:
- Sources
- Destinations
- Hookdeck API
The following sections cover the authentication mechanisms and methods that Hookdeck supports for these interactions.
Source authentication
Requests from sources, or inbound requests to the Hookdeck URL defined in a source, can be authenticated in the following ways:
- HTTP and webhook signature verification: with inbuilt support for a number of third-party providers along with generic signing methods
- Basic auth: username and password
- API Key: provided in a configurable HTTP header or query parameter
Additionally, Hookdeck has built-in support for the following 64 providers and methods:
- 3dEye
- Adyen
- Akeneo
- API Key
- AWS SNS
- Basic Auth
- Bondsmith
- Bridge
- Cloud Signal
- Commercelayer
- Courier
- Discord
- Ebay
- Enode
- Favro
- Fiserv
- FrontApp
- GitHub
- GitLab
- HMAC
- Hubspot
- Linear
- Mailchimp
- Mailgun
- NMI Payment Gateway
- Orb
- Oura
- Paddle
- Paypal
- Persona
- Pipedrive
- Postmark
- Praxis
- Property Finder
- Pylon
- Razorpay
- Recharge
- Repay
- Sanity
- SendGrid
- Shopify
- Shopline
- Slack
- SolidGate
- Square
- Stripe
- Svix
- Synctera
- Tebex
- Telnyx
- TokenIO
- Treezor
- Trello
- Twilio
- Twitch
- Typeform
- Vercel
- Vercel Log Drains
- Wix
- WooCommerce
- WorkOS
- Xero
- Zoom
For more information on configuring source authentication, see source authentication.
Webhook handshaking and validation
Some API providers require a validation step, known as a "handshake" or "challenge." While this is not standard across all providers, we do our best to implement validation for any platform that our users integrate with.
Hookdeck provides built-in support for the following platforms and methods:
- Adobe Acrobat Sign
- Adyen
- Asana
- Finicity (Mastercard)
- Google Business Messages
- helloflex.com
- HelloSign
- InFakt
- Infusionsoft
- Mailchimp
- Microsoft SharePoint
- monday.com
- Nylas
- Okta
- OnFleet
- Oura
- PayPal
- Slack
- Smartsheet
- Strava (use
STRAVA
asverify_token
) - Trello
- Twitch
- Twitter / X (must use our Twitter source verification)
- Zoom (must use our Zoom source verification)
- Wallester
- WebSub generic integration
- REST Hooks implementation
If your provider requires a handshake and is missing from this list, contact us and we will add support within 24 hours.
For details on how to configure handshaking and verification, see sources.
HTTP and webhook signature verification
Verifying the original provider's signature before processing events prevents bad actors from taking illegitimate actions on your server.
Hookdeck verifies the provider's signature when verification is enabled for your source. In this case, an x-hookdeck-verified
header is set to true
to confirm the original request was verified, meaning it is safe to verify just the Hookdeck signature.
It's possible to configure verification with most platforms using our dedicated third-party platform, or our "configure-your-own" provider that supports generic implementations of Basic Auth, API Keys, and HMAC signature.
You can also roll your own private verification for any platform that supports Basic Auth, API Key authentictaion or verification using HMAC.
How verification works
When a source has verification configured, Hookdeck will verify every incoming request either with HMAC, Basic Auth, or an API key. Requests that do not match the verification are rejected and labeled "Verification Failed" in the request page.
When using Hookdeck's built in support for third-party providers, it's safe to skip the original provider's verification step and only verify Hookdeck's header. This saves you time, since you only need to implement a single verification process across multiple providers.
Hookdeck will return a HTTP 401 for the HMAC (expect MD5), API Key, and Basic Auth verification methods and for the Shopify, Zoom, Xero, Twitter. For the other providers, Hookdeck will return a HTTP 200.
Destination authentication
Requests to destinations, or outbound requests made by Hookdeck to the URL defined in a destination, can be authenticated as follows:
- Hookdeck signature: following the Hookdeck signing process using the Hookdeck project signing secret to verify the request
- Custom SHA-256 signature: with a signing secret provided in a configurable HTTP header to verify the request
- Basic auth: username and password
- API key: provided in a configurable HTTP header or query parameter
- Bearer token: using the standard HTTP
Authorization
header prefixed withBearer
For more information on configuring destination authentication, see destination authentication.
Hookdeck webhook signature verification
By default, events sent by Hookdeck to a destination are verified by calculating a digital signature. This authentication method is known as the "Hookdeck Signature".
Each event request includes a x-hookdeck-signature
header, generated using the project's secret along with the data sent in the request. To verify the request, compute the HMAC digest according to the following algorithm and compare it to the value in the x-hookdeck-signature
and x-hookdeck-signature-2
headers. If they match, the event request was sent from Hookdeck.
If the previous secret was rolled with a delay and is still active, a
x-hookdeck-signature-2
header is also provided.
To retrieve the signing secret, head over to the Project Secrets page.
Example of Hookdeck webhook signature verification
Hookdeck signatures use a
SHA-256
algorithm and arebase64
encoded.
const HOOKDECK_WEBHOOK_SECRET = process.env.HOOKDECK_WEBHOOK_SECRET;
app.use(
express.json({
// Store the rawBody buffer on the request
verify: (req, res, buf) => {
req.rawBody = buf;
},
}),
);
app.post("/webhook", async (req, res) => {
//Extract x-hookdeck-signature and x-hookdeck-signature-2 headers from the request
const hmacHeader = req.get("x-hookdeck-signature");
const hmacHeader2 = req.get("x-hookdeck-signature-2");
//Create a hash based on the parsed body
const hash = crypto
.createHmac("sha256", HOOKDECK_WEBHOOK_SECRET)
.update(req.rawBody)
.digest("base64");
// Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
if (hash === hmacHeader || (hmacHeader2 && hash === hmacHeader2)) {
console.log("Webhook is originating from Hookdeck");
res.sendStatus(200);
} else {
console.log("Signature is invalid, rejected");
res.sendStatus(403);
}
});
npm i @hookdeck/sdk
import { verifyWebhookSignature } from "@hookdeck/sdk/webhooks";
const HOOKDECK_WEBHOOK_SECRET = process.env.HOOKDECK_WEBHOOK_SECRET;
app.use(
express.json({
// Store the rawBody buffer on the request
verify: (req, res, buf) => {
req.rawBody = buf;
},
}),
);
app.post("/webhook", async (req, res) => {
const verified = await verifyWebhookSignature({
headers: req.headers,
rawBody: req.rawBody,
signingSecret: HOOKDECK_WEBHOOK_SECRET}
);
if(verified === true) {
console.log("Webhook is originating from Hookdeck");
res.sendStatus(200);
}
else {
console.log("Signature is invalid, rejected");
res.sendStatus(403);
}
});
See the TypeScript quickstart code on GitHub.
import os
from datetime import datetime
import json
import logging
import hashlib
import hmac
import base64
from flask import Flask, request
logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()])
app = Flask(__name__)
app.logger.setLevel(logging.INFO)
HOOKDECK_WEBHOOK_SECRET = os.getenv("HOOKDECK_WEBHOOK_SECRET")
def verify_webhook(request):
if HOOKDECK_WEBHOOK_SECRET is None:
app.logger.warn(
"No HOOKDECK_WEBHOOK_SECRET found in environment variables. Skipping verification."
)
return False
# Extract x-hookdeck-signature and x-hookdeck-signature-2 headers from the request
hmac_header = request.headers.get("x-hookdeck-signature")
hmac_header2 = request.headers.get("x-hookdeck-signature-2")
# Create a hash based on the raw body
hash = base64.b64encode(
hmac.new(
HOOKDECK_WEBHOOK_SECRET.encode(), request.data, hashlib.sha256
).digest()
).decode()
# Compare the created hash with the value of the x-hookdeck-signature
# Also check x-hookdeck-signature-2 header in case the secret was rolled
return hash == hmac_header or (hmac_header2 and hash == hmac_header2)
@app.route("/<path:path>", methods=["POST"])
def handle(path):
app.logger.info(
"webhook_received %s %s",
datetime.now().isoformat(),
json.dumps(request.json, indent=2),
)
if not verify_webhook(request):
return {"status": "UNAUTHORIZED"}, 403
else:
return {"status": "ACCEPTED"}, 200
if __name__ == "__main__":
app.run(debug=True, port=3031)
See the Python quickstart code on GitHub.
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func verifyPayload(jsonBody string, headers http.Header) bool {
secret := os.Getenv("HOOKDECK_WEBHOOK_SECRET")
if secret == "" {
log.Println("No HOOKDECK_WEBHOOK_SECRET found in environment variables. Skipping verification.")
return true
}
hmacHeader := headers.Get("x-hookdeck-signature")
hmacHeader2 := headers.Get("x-hookdeck-signature-2")
hash := hmac.New(sha256.New, []byte(secret))
hash.Write([]byte(jsonBody))
expectedHash := base64.StdEncoding.EncodeToString(hash.Sum(nil))
if expectedHash == hmacHeader ||
(hmacHeader2 != "" && expectedHash == hmacHeader2) {
return true
}
return false
}
func main() {
r := gin.Default()
r.POST("/*path", func(c *gin.Context) {
bodyAsByteArray, err := io.ReadAll(c.Request.Body)
jsonBody := string(bodyAsByteArray)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "could not get request body data",
})
return
}
verified := verifyPayload(jsonBody, c.Request.Header)
if !verified {
c.JSON(http.StatusUnauthorized, gin.H{
"message": "invalid payload",
})
return
}
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, []byte(jsonBody), "", " "); err != nil {
log.Printf("Failed to format JSON: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"message": "failed to format JSON",
})
return
}
log.Printf("webhook_received: %v", prettyJSON.String())
c.JSON(http.StatusOK, gin.H{
"status": "ACCEPTED",
})
})
port := os.Getenv("PORT")
if port == "" {
port = "3032"
}
fmt.Printf("🪝 Server running at http://localhost:%s", port)
r.Run(fmt.Sprintf(":%s", port))
}
See the Go quickstart code on GitHub.
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
const string HOOKDECK_SIGNATURE_HEADER = "X-Hookdeck-Signature";
const string HOOKDECK_WEBHOOK_SECRET_CONFIG_KEY = "inbound:HookdeckWebhookSecret";
string WEBHOOK_SECRET = builder.Configuration[HOOKDECK_WEBHOOK_SECRET_CONFIG_KEY] ?? string.Empty;
static bool VerifyHmacWebhookSignature(HttpContext context, string webhookSecret, string rawBody)
{
if(string.IsNullOrEmpty(webhookSecret))
{
Console.WriteLine("WARNING: Missing webhook secret. Skipping verification.");
return true;
}
string? hmacHeader = context.Request.Headers[HOOKDECK_SIGNATURE_HEADER].FirstOrDefault();
if (string.IsNullOrEmpty(hmacHeader))
{
Console.WriteLine("Missing HMAC headers");
return false;
}
HMACSHA256 hmac = new(Encoding.UTF8.GetBytes(webhookSecret));
string hash = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody)));
return hash.Equals(hmacHeader);
}
app.MapPost("/{**path}", async (string? path, HttpContext context) =>
{
using StreamReader reader = new StreamReader(context.Request.Body);
string rawBody = await reader.ReadToEndAsync();
bool verified = VerifyHmacWebhookSignature(context, WEBHOOK_SECRET, rawBody);
if(!verified)
{
return Results.Unauthorized();
}
Console.WriteLine(new
{
webhook_received = DateTime.UtcNow.ToString("o"),
body = rawBody
});
return Results.Json(new {
STATUS = "ACCEPTED"
});
});
app.UseRouting();
app.Run();
See the C# quickstart code on GitHub.
Hookdeck API authentication
The Hookdeck API supports authentication using a Bearer Token Authentication or Basic Authentication.
For more information, see the authentication section in the API reference.