How to set up authorization on a webhook
A guide to setting up authorization on a webhook.
How to set up OAuth 2.0 authorization for a webhook
OAuth 2.0 authorization lets your webhook obtain and use an access token for services that require one.
Before configuring OAuth 2.0 authorization, make sure you have the information required to obtain an access token from the external service:
Client ID: Identifies the client requesting the access token
Client Secret: Used with the Client ID to request an access token
Token URL: The endpoint used to request access tokens
Scope (optional): Defines the permissions granted to the access token
To configure OAuth 2.0 authorization, go to Settings > Integration > Webhooks.

Open an existing webhook or create a new one.

In the webhook configuration page, locate the authorization section and select "OAuth 2.0".
Enter the previously mentioned required information into the corresponding fields.
Next, choose how the client credentials should be sent when requesting an access token:
Basic Auth Header: Sends the "Client ID" and "Client Secret" in the authorization header
Request Body: Sends the "Client ID" and "Client Secret" as parameters in the request body
If the external service requires additional parameter you can add them using "Add parameter".
You can also configure custom headers for the webhook request under "Request headers".
Once you have entered the required settings, save the webhook configuration.
What happens when the webhook is triggered
When the webhook is triggered, the system first requests an access token from the configured Token URL using the provided client credentials.

If the credentials are valid, an access token is returned.
The system then automatically includes the token in the Authorization header and sends the webhook request to the configured endpoint.

How to set up HMAC authorization for a webhook
When using HMAC authorization for webhooks, Struct signs the outbound HTTP requests it sends to your application so that you can verify that a request genuinely came from Struct and that the body was not tampered with in transit. The signature also carries a timestamp so you can reject replayed requests.
This documentation describes exactly how the signature is produced (by RequestSignatureService.GenerateSignature) so that your receiving application can recompute the same value and compare it against the one we send.
To get started we first need to enable HMAC authorization. To do this, we navigate to Settings > Integration > Webhooks and open an existing webhook or create a new one.

In the Authorization section, select HMAC as the authorization method.

Enter or generate a secret that will be used to sign outgoing webhook requests. The same shared secret must also be configured in the receiving system so it can verify the signature.
For security, use a strong random value of at least 32 characters for the secret.
The generated signature is included in the header of the webhook request.

The receiving system uses its copy of the shared secret to calculate the expected signature and compares it to the signature included in the request. If the signatures match, the request is considered authentic.
What happens when the webhook is triggered
When the webhook is triggered, Struct generates an HMAC-SHA256 signature using the configured shared secret.
The signature is generated using the following algorithm:
MAC: HMAC with SHA-256.
Key: the shared secret, encoded as UTF-8 bytes.
Message: the Unix timestamp, a literal
.(period), then the raw request body, the whole string encoded as UTF-8 bytes.Output: the raw HMAC digest (32 bytes) encoded as standard Base64 (
+//alphabet, with=padding — not base64url, not hex).
The generated signature is included in the X-Hook-Signature header, and the timestamp used to generate the signature is included in the X-Hook-Timestamp header. The shared secret is never transmitted as part of the request.
With HMAC authorization enabled, Struct sends the webhook payload together with the following headers:
X-Hook-Timestamp
The Unix timestamp (UTC, in seconds) used to generate the signature.
X-Hook-Signature
The Base64-encoded HMAC-SHA256 signature.
Verifying the Signature
To verify the webhook request:
Read the raw request body exactly as received, before parsing or modifying it.
Read the
X-Hook-Timestampheader.Reject stale requests: Check the age of the requests through
abs(now − timestamp)in seconds and reject if it exceeds your tolerance. Struct itself validates with a 300-second (5-minute) window, so 300 s is the recommended value.Rebuild the signed message by concatenating the timestamp, a period (
.), and the raw body string:Generate an HMAC-SHA256 signature through the algorithm
Base64(HMAC_SHA256(clientSecret, message)).Use constant-time comparison: Compare the two signatures with a length-constant, value-constant comparison (e.g.
crypto.timingSafeEqual,hmac.compare_digest,CryptographicOperations.FixedTimeEquals) rather than==, to avoid leaking information through timing. This mirrors what Struct does when it validates a signature.
If the generated signature matches the received signature and the timestamp is within the accepted time window, the request can be considered authentic and the request body has not been modified.
Common Pitfalls
Always verify the raw request body. Deserializing and re-serializing the JSON can result in the key ordering, whitespace, Unicode escaping, and number formatting to all change, and the HMAC will not match. Always sign/verify the raw request body.
The timestamp is expressed in seconds, not milliseconds.
Use a constant-time comparison when comparing signatures to avoid timing attacks.
If HMAC authorization is enabled and the signature headers are missing, reject the request.
Validate your implementation
Use this known-good vector to validate your implementation. All three inputs are fed in verbatim; the body has no surrounding whitespace.
ClientSecret
stss_example_secret_do_not_use
Timestamp
1700000000
Body
{"appConnectionUid":"a1b2c3","tenantSlug":"acme"}
Signed message
1700000000.{"appConnectionUid":"a1b2c3","tenantSlug":"acme"}
Expected signature
n6Kw56YP7SFCqyyJAt5YP/Xg/98xbfyrRNmVPtOEFiM=
Examples of implementation
Below you can see various examples of implementation across different languages that can be used as reference for your implementation.
Node.js / TypeScript
Make sure you capture the raw body. With express.json() use the verify hook (express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })) and verify against req.rawBody.toString("utf8"), not JSON.stringify(req.body).
C#
Last updated