> ## Documentation Index
> Fetch the complete documentation index at: https://alpha.developer.tomorro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Technical documentation

> Signature verification, delivery, retries, and security policy

## Security policy

Anyone who knows your endpoint URL could send it requests. To be sure an event genuinely comes
from Tomorro, **verify the signature on every request** before acting on it.

### Signing secret

Each webhook has its own secret, generated when you create it and shared only between Tomorro
and you. Tomorro uses it to sign every event; you use it to verify them. Treat it like a
password — never expose it client-side or commit it to source control.

### Signature header

Every event is sent with a signature header. Tomorro signs the event by computing an
HMAC-SHA256 over the timestamp and the raw request body, using your webhook secret:

```
sha256 = HMAC_SHA256(secret, timestamp + "." + rawBody)
```

The signature is delivered in the `Leeway-Signature` header, in the form `t=<timestamp>,sha256=<hash>`:

```http theme={null}
Leeway-Signature: t=1492774577000,sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

* `t` — the timestamp the event was signed at, in **milliseconds** since the Unix epoch.
* `sha256` — the computed HMAC-SHA256 hash, hex-encoded.

<Note>
  For backward compatibility the same value is also sent in a `Leeway_Signature` header (with an
  underscore). Prefer the canonical `Leeway-Signature`; some proxies strip headers that contain
  underscores.
</Note>

### Verifying the signature

To verify, recompute the HMAC over `timestamp + "." + rawBody` with your secret and compare it
to the `sha256` value from the header.

<Warning>
  Compute the HMAC over the **raw request body bytes**, exactly as received — do not re-serialize
  the parsed JSON. Re-serializing can reorder keys or change whitespace and break the comparison.
</Warning>

```javascript Node.js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * @param {string} header   - the `Leeway-Signature` header value
 * @param {string} secret   - the webhook's signing secret
 * @param {string} rawBody  - the raw request body, exactly as received
 */
function isValidSignature(header, secret, rawBody) {
  // Parse "t=<timestamp>,sha256=<hash>"
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.trim().split('=')));
  const { t: timestamp, sha256: received } = parts;
  if (!timestamp || !received) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // Constant-time comparison to avoid timing attacks
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  if (a.length !== b.length || !timingSafeEqual(a, b)) return false;

  // Replay protection: reject events older than 5 minutes
  const ageMs = Math.abs(Date.now() - Number(timestamp));
  return ageMs < 5 * 60 * 1000;
}
```

<Note>
  Always use a **constant-time** comparison (e.g. `timingSafeEqual`) rather than `===` to avoid
  [timing attacks](https://en.wikipedia.org/wiki/Timing_attack).
</Note>

### Replay protection

The signed timestamp also lets you reject **replayed** events: compare `t` to the current time
and discard anything outside a tolerance window you control (5 minutes is a reasonable default,
as shown above). This prevents an attacker from re-sending a previously valid, intercepted
request.

## Delivery & retries

### What counts as a successful delivery

When Tomorro sends an event, your endpoint must:

* respond with a **`2xx`** HTTP status code, and
* do so within the **3-second** timeout.

Any other status code, or a response that takes longer than the timeout, is treated as a
**failed** delivery.

<Tip>
  Acknowledge fast, process later. Return `2xx` as soon as you've received the event, then do
  any heavy work asynchronously. This keeps you comfortably under the 3-second timeout.
</Tip>

### Retry policy

If a delivery fails, Tomorro automatically retries it:

* Failed events are re-queued with a **5-minute delay** between attempts.
* Tomorro retries up to **10 times** for a given event.
* If every attempt fails, Tomorro stops retrying and **disables the webhook**. A disabled
  webhook receives no further events until you re-enable it from the
  [Webhooks settings](https://app.tomorro.com/settings/integrations?integration=webhook).

<Note>
  Because retries are spaced 5 minutes apart, your endpoint may receive the same event more than
  once. Make your handler **idempotent** — deduplicate using the `eventId` field so reprocessing
  a delivery has no side effects.
</Note>

<Info>
  Exact timeout and retry values may change over time as we tune delivery.
</Info>
