> For the complete documentation index, see [llms.txt](https://docs.struct.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.struct.com/tutorials/guides/how-to-use-webhooks/how-to-set-up-authorization-on-a-webhook.md).

# How to set 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.

<figure><img src="/files/A6MvNC3oMSmOFNdBVyN1" alt=""><figcaption><p>Webhook configuration page</p></figcaption></figure>

Open an existing webhook or create a new one.

<figure><img src="/files/XzmTYPsA3GZ4F4H1j9Fw" alt=""><figcaption><p>Webhook configurations setup page</p></figcaption></figure>

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.

<figure><img src="/files/Hn5117zwRfxLfOqc1goK" alt=""><figcaption><p>Example of webhook with OAuth2 sent to token endpoint</p></figcaption></figure>

If the credentials are valid, an access token is returned.&#x20;

The system then automatically includes the token in the Authorization header and sends the webhook request to the configured endpoint.

<figure><img src="/files/q6pWcEKvOvp5TDHblyp0" alt=""><figcaption><p>Example of token returned</p></figcaption></figure>

## 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.

<figure><img src="/files/Y4PH78WmgtHS66ydzA28" alt=""><figcaption><p>Webhook configuration page</p></figcaption></figure>

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

<figure><img src="/files/GAGWpvZpLa0FezzFhw1m" alt=""><figcaption><p>Webhook configurations setup page</p></figcaption></figure>

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.

<figure><img src="/files/HMFhbyBjAZ4NuR7425QA" alt=""><figcaption><p>Example of HMAC webhook with signature in header based on secret</p></figcaption></figure>

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:

```
signature = Base64( 
HMAC_SHA256( key = clientSecret, message = "{timestamp}.{rawRequestBody}" ) )
```

* **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:

| Header             | Description                                                          |
| ------------------ | -------------------------------------------------------------------- |
| `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:

1. Read the raw request body exactly as received, before parsing or modifying it.
2. Read the `X-Hook-Timestamp` header.
3. 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.
4. Rebuild the signed message by concatenating the timestamp, a period (`.`), and the raw body string:

   ```
   "{timestamp}.{rawRequestBody}"
   ```
5. Generate an HMAC-SHA256 signature through the algorithm `Base64(HMAC_SHA256(clientSecret, message))`.
6. 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.

| Input              | Value                                                          |
| ------------------ | -------------------------------------------------------------- |
| 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

```typescript
import crypto from "node:crypto";

/**
 * @param clientSecret  the shared secret issued by Struct
 * @param timestamp     value of the timestamp header (string is fine)
 * @param rawBody       the raw request body, exactly as received
 * @param signature     value of the signature header
 * @param toleranceSec  max age of a request, in seconds (Struct uses 300)
 */
export function verifyStructSignature(
  clientSecret: string,
  timestamp: string,
  rawBody: string,
  signature: string,
  toleranceSec = 300,
): boolean {
  const ts = Number(timestamp);
  if (!Number.isFinite(ts)) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > toleranceSec) return false;

  const message = `${ts}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", clientSecret)
    .update(message, "utf8")
    .digest("base64");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

{% hint style="info" %}
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)`.
{% endhint %}

### C\#

```csharp
using System.Security.Cryptography;
using System.Text;

public static bool VerifyStructSignature(
    string clientSecret,
    string timestampHeader,
    string rawBody,
    string signature,
    int toleranceSeconds = 300)
{
    if (!long.TryParse(timestampHeader, out var timestamp))
        return false;

    var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (Math.Abs(now - timestamp) > toleranceSeconds)
        return false;

    var message = $"{timestamp}.{rawBody}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(clientSecret));
    var expected = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature));
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.struct.com/tutorials/guides/how-to-use-webhooks/how-to-set-up-authorization-on-a-webhook.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
