---
title: Signing
description: Produce a Sigil-Signature header that binds the timestamp into the signed material.
url: https://pr-1-10d18e06d3bf.thally.app/signing
---

# Signing

Produce a Sigil-Signature header that binds the timestamp into the signed material.

## Produce a signature header

`sign` returns a complete `Sigil-Signature` header value. Pass the raw body
and one or more secrets:

```ts
import { sign } from '@sigil/core';

const body = JSON.stringify({ id: 'evt_1', type: 'invoice.paid' });
const header = sign(body, process.env.WEBHOOK_SECRET!);
// "t=1767225600,v1=6b1f…"
```

The header contains the current Unix timestamp and one HMAC-SHA256 digest per
secret, in the order given. Passing multiple secrets is the
[key-rotation](/key-rotation) path — sign with both the outgoing and incoming
secret until every receiver has the new one:

```ts
const header = sign(body, [
  process.env.SECRET_OLD!,
  process.env.SECRET_NEW!,
]);
// "t=1767225600,v1=<old>,v1=<new>"
```

## Options

| Option      | Type     | Default                          | Description                        |
| ----------- | -------- | -------------------------------- | ---------------------------------- |
| `timestamp` | `number` | `Math.floor(Date.now() / 1000)` | Unix seconds to stamp the signature with. Pass it explicitly to make a test deterministic. |

```ts
sign(body, secret, { timestamp: 1767225600 });
```

## Lower-level functions

### `computeSignature`

Returns the lower-case hex HMAC-SHA256 digest of `signingPayload(timestamp, body)`:

```ts
import { computeSignature } from '@sigil/core';

const digest = computeSignature(1767225600, body, secret);
// 64 lower-case hex characters
```

### `signingPayload`

Returns the exact bytes that get signed — the timestamp, a single ASCII dot,
then the raw body:

```ts
import { signingPayload } from '@sigil/core';

const bytes = signingPayload(1767225600, body);
// Uint8Array of "1767225600." + body
```

Binding the timestamp into the signed material makes it tamper-evident. If the
timestamp were carried alongside an unbound signature, an attacker could replay
an old body with a fresh timestamp and it would still verify.

A string body is encoded as UTF-8; a `Uint8Array` is used as-is.

## Validation

- An empty string is not a valid secret. `sign` throws `secret_empty`.
- An empty array of secrets throws `secret_empty`.
- A fractional or negative timestamp throws `header_timestamp_invalid`.