> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.fynapse.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.fynapse.app/_mcp/server.

# Security

> Learn how Fynapse signs webhook requests and how to validate them to ensure authenticity and integrity before processing.

## Overview

To ensure that webhook requests originate from Fynapse and have not been tampered with, each request is signed using a shared secret.

Your system must validate this signature before processing any webhook payload.

## Why Validation Is Required

Webhook endpoints are public HTTP endpoints. Without validation, an attacker could:

* Send forged requests to your system
* Replay previously captured webhook payloads
* Trigger unintended processing

Signature verification ensures that:

* the request was sent by Fynapse
* the payload has not been modified in transit

## Signing Model

Fynapse signs each webhook request using HMAC-SHA256.

The signature is calculated over the following input:

`<timestamp>.<raw_request_body>`

Where:

* `timestamp` is a Unix timestamp (seconds)
* `raw_request_body` is the exact HTTP request body as received

## Signature Header Format

Each request includes a Webhook-Signature header:

`Webhook-Signature: t=<timestamp>,v1=<signature>[,v1=<previous_signature>]`

**Components**

`t` — Unix timestamp used in the signature
`v1` — HMAC-SHA256 signature (hex-encoded)

During secret rotation, multiple v1 values may be present.
Your system should accept the request if any one of the signatures is valid.

## How to Validate a Request

To verify a webhook request:

* Extract the `Webhook-Signature` header
* Parse the timestamp (`t`) and all `v1` signatures
* Reconstruct the signing input:
  `<timestamp>.<raw_request_body>`
* Compute the HMAC-SHA256 using your secret
* Compare the computed signature with each provided `v1` value
* Accept the request if any match

### Validation Rules

When implementing validation, follow these rules:

| Rule                              | Description                                                           |
| --------------------------------- | --------------------------------------------------------------------- |
| Use raw request body              | Use the body exactly as received; do not reformat or reserialize JSON |
| Perform constant-time comparison  | Prevent timing attacks when comparing signatures                      |
| Validate timestamp freshness      | Reject requests outside an acceptable time window (e.g., 5 minutes)   |
| Support multiple signatures       | Allow validation against multiple signatures for secret rotation      |
| Include timestamp in signed input | Always include timestamp; never validate against the body alone       |

#### Example - Signature Verification (Java)

The following example demonstrates how to validate a webhook signature in Java:

```java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class WebhookSignatureVerifier {
    private static final String HMAC_SHA256 = "HmacSHA256";
    private static final HexFormat HEX = HexFormat.of();
    private static final Duration MAX_CLOCK_SKEW = Duration.ofMinutes(5);
    public static boolean verify(String signatureHeader, String rawBody, String secret) {
        if (signatureHeader == null || signatureHeader.isBlank()) {
            return false;
        }
        if (rawBody == null || secret == null || secret.isBlank()) {
            return false;
        }
        ParsedSignature parsed = parseHeader(signatureHeader);
        long now = Instant.now().getEpochSecond();
        if (Math.abs(now - parsed.timestamp()) > MAX_CLOCK_SKEW.toSeconds()) {
            return false;
        }
        String signingInput = parsed.timestamp() + "." + rawBody;
        String expected = hmacSha256Hex(signingInput, secret);
        for (String provided: parsed.signatures()) {
            if (MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), provided.getBytes(StandardCharsets.UTF_8))) {
                return true;
            }
        }
        return false;
    }
    private static String hmacSha256Hex(String value, String secret) {
        try {
            Mac mac = Mac.getInstance(HMAC_SHA256);
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
            return HEX.formatHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalStateException("Cannot verify webhook signature", e);
        }
    }
    private static ParsedSignature parseHeader(String header) {
        Long timestamp = null;
        List < String > signatures = new ArrayList < > ();
        for (String part: header.split(",")) {
            String[] keyValue = part.trim().split("=", 2);
            if (keyValue.length != 2) {
                continue;
            }
            if ("t".equals(keyValue[0])) {
                timestamp = Long.parseLong(keyValue[1]);
            } else if ("v1".equals(keyValue[0])) {
                signatures.add(keyValue[1]);
            }
        }
        if (timestamp == null || signatures.isEmpty()) {
            throw new IllegalArgumentException("Invalid Webhook-Signature header");
        }
        return new ParsedSignature(timestamp, signatures);
    }
    private record ParsedSignature(long timestamp, List < String > signatures) {}
}
```

## Secret Management

### Creating a Secret

A webhook secret is generated by Fynapse and used to sign requests.

* The secret value is returned only when:
  * creating a secret
  * rotating a secret
  * retrieving secret details
* You must store this value securely on your side

### Using a Secret

* Each webhook subscription references a secret
* The same secret is used to sign all deliveries for that subscription

### Rotating a Secret

Secrets can be rotated to improve security.

During rotation:

* Fynapse may include multiple signatures in the header
* One corresponds to the new secret
* One corresponds to the previous secret

This allows you to:

* deploy the new secret safely
* continue accepting requests during the transition