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

# Authentication

> HMAC-SHA256 request signing with a five-minute replay window.

Every request to `/api/v1/*` is authenticated against a tenant's `signing_secret`. Two modes are supported: HMAC signatures (recommended) and bearer tokens (simpler, equivalent strength when transport is TLS).

<img src="https://mintcdn.com/cherttechnologiesinc/7NuyCzLl8sIsJEKo/api/diagrams/signature.svg?fit=max&auto=format&n=7NuyCzLl8sIsJEKo&q=85&s=c15e8f70fc2ba3b03078b8bce4cf736b" alt="Signature verification flow" width="1000" height="360" data-path="api/diagrams/signature.svg" />

## Required headers

| Header              | Required    | Description                                                                                                                                                                                                                                                                          |
| ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `x-chert-tenant`    | Conditional | Your `tenant_slug` from registration. Required for tenants registered with `multi_tenant: true` (strict routing) and required whenever `x-chert-signature` is sent. Optional for default single-tenant accounts using bearer auth — the bearer token alone identifies the workspace. |
| `x-chert-signature` | One of      | `v1,<unix_seconds>,<hex>` — preferred. Always send `x-chert-tenant` alongside it.                                                                                                                                                                                                    |
| `authorization`     | One of      | `Bearer <signing_secret>` — single-tenant default. The bearer token resolves the tenant when `x-chert-tenant` is omitted.                                                                                                                                                            |
| `content-type`      | On `POST`   | `application/json`                                                                                                                                                                                                                                                                   |

If both `x-chert-signature` and `authorization` are present, the signature is checked first.

## Signature format

```
x-chert-signature: v1,<unix_seconds>,<hmac_hex>
```

| Field          | Description                                                                  |
| -------------- | ---------------------------------------------------------------------------- |
| `v1`           | Format version.                                                              |
| `unix_seconds` | Request timestamp. Must be within 5 minutes of server time.                  |
| `hmac_hex`     | Lowercase hex of `HMAC-SHA256(signing_secret, "<unix_seconds>.<raw_body>")`. |

### What gets signed

The HMAC input is the literal string `<unix_seconds>` + `.` + `<raw_body>`.

* **For `POST` requests**, `raw_body` is the exact bytes you transmit. Do not re-serialize, do not strip whitespace, do not reorder keys after signing.
* **For `GET` requests**, `raw_body` is the empty string. The HMAC input ends in a trailing dot.

The server reads the request body byte-for-byte before parsing, so any reformatting on the client invalidates the signature.

### Replay window

Timestamps more than **5 minutes** off in either direction are rejected with `401` and the distinct code `2013 AUTH_TIMESTAMP_SKEW` — separate from a genuinely bad signature (`2004`). Sync your clock and resign on retry — never reuse a signature.

## Reference implementations

<CodeGroup>
  ```bash curl theme={null}
  TS=$(date +%s)
  BODY='{"phone":"+14155551234","body":"Hi"}'
  SIG=$(printf "%s.%s" "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

  curl https://console.trychert.com/api/v1/send \
    -H "x-chert-tenant: $SLUG" \
    -H "x-chert-signature: v1,$TS,$SIG" \
    -H "content-type: application/json" \
    -d "$BODY"
  ```

  ```js Node.js theme={null}
  import { createHmac } from "node:crypto"

  function sign(secret, body) {
    const ts = Math.floor(Date.now() / 1000)
    const hex = createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex")
    return `v1,${ts},${hex}`
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, time

  def sign(secret: str, body: str) -> str:
      ts = str(int(time.time()))
      hex_ = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
      return f"v1,{ts},{hex_}"
  ```
</CodeGroup>

## Bearer token fallback

For scripts, prototypes, and trusted server-to-server calls, send the secret directly:

```
authorization: Bearer <signing_secret>
```

The server compares with a constant-time check. Bearer is fine when the request never leaves a controlled server environment — TLS 1.2+ is the only protection between the secret and the network. Use HMAC wherever the signing process could be exposed (e.g. edge functions with third-party dependencies, client-side code, shared build pipelines).

## Errors

Auth and signature failures return **distinct numeric codes** — they are no longer collapsed into one opaque "unauthorized". Branch on the numeric `code`:

| Status | Code   | Name                  | Cause                                                                                       | Action                                                      |
| ------ | ------ | --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| 401    | `2012` | `AUTH_MISSING`        | No credentials supplied at all — both `x-chert-signature` and `authorization` are absent.   | Add a signature or bearer header.                           |
| 401    | `2004` | `AUTH_INVALID`        | Credentials supplied but rejected — malformed or mismatched signature, or bad bearer token. | Re-sign with the current timestamp / check the token.       |
| 401    | `2013` | `AUTH_TIMESTAMP_SKEW` | Signature timestamp outside the 5-minute replay window.                                     | Sync your clock and re-sign with a fresh timestamp.         |
| 403    | `2007` | `EMAIL_NOT_VERIFIED`  | Tenant email not yet confirmed.                                                             | Click the link in the verification email.                   |
| 404    | `2001` | `TENANT_NOT_FOUND`    | `tenant_slug` not recognized or tenant suspended.                                           | Verify `x-chert-tenant` matches your registration response. |

Error responses carry the standard envelope — `{ success, error: { status, code, message, retryable }, trace_id }` — with `retryable: false` on every auth failure. The `message` is generic; the crypto-step detail (which header was missing, which verification step failed) is logged server-side under `trace_id` and is no longer leaked in the response. See [Errors](/api/errors) for the full table.

<Note>
  `POST /api/v1/send` is a legacy-shaped endpoint and still emits a flat `{ error, code: "auth_failed" }` body for auth failures rather than the numeric envelope above. All other `/api/v1/*` routes use the distinct numeric codes.
</Note>

## Rotating the signing secret

The `signing_secret` is shown exactly once at registration. To rotate it, contact your Chert administrator. Re-registering the same email returns the existing tenant and does not regenerate the secret.

## Webhook signatures

Inbound webhook events are signed independently with the webhook subscription `secret` (returned once when the subscription is created). Two signature header formats are emitted simultaneously:

| Header                | Format            | Notes                                                |
| --------------------- | ----------------- | ---------------------------------------------------- |
| `x-chert-signature`   | `v1,<ts>,<hex>`   | Legacy format. Supported for backward compatibility. |
| `X-Webhook-Signature` | `t=<ts>,v1=<hex>` | New format. Recommended for new integrations.        |

Both compute the same HMAC: `HMAC-SHA256(subscription_secret, "<ts>.<raw_body>")`. The raw body is the exact bytes received — do not parse and re-serialize before verifying. See [Receiving replies](/api/receiving-replies) for the consumer-side verification snippet that handles both formats.

Additional headers on every webhook delivery:

| Header                      | Description                                         |
| --------------------------- | --------------------------------------------------- |
| `X-Webhook-Event`           | Event type, e.g. `message.received`                 |
| `X-Webhook-Event-Id`        | Stable dedup key                                    |
| `X-Webhook-Timestamp`       | Unix seconds                                        |
| `X-Webhook-Subscription-Id` | Id of the subscription that triggered this delivery |

## See also

* [Quickstart](/api/quickstart)
* [Receiving replies](/api/receiving-replies)
* [Webhook subscriptions](/api/webhook-subscriptions)
* [Errors](/api/errors)
