Skip to main content
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). Signature verification flow

Required headers

HeaderRequiredDescription
x-chert-tenantConditionalYour 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-signatureOne ofv1,<unix_seconds>,<hex> — preferred. Always send x-chert-tenant alongside it.
authorizationOne ofBearer <signing_secret> — single-tenant default. The bearer token resolves the tenant when x-chert-tenant is omitted.
content-typeOn POSTapplication/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>
FieldDescription
v1Format version.
unix_secondsRequest timestamp. Must be within 5 minutes of server time.
hmac_hexLowercase 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

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"

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:
StatusCodeNameCauseAction
4012012AUTH_MISSINGNo credentials supplied at all — both x-chert-signature and authorization are absent.Add a signature or bearer header.
4012004AUTH_INVALIDCredentials supplied but rejected — malformed or mismatched signature, or bad bearer token.Re-sign with the current timestamp / check the token.
4012013AUTH_TIMESTAMP_SKEWSignature timestamp outside the 5-minute replay window.Sync your clock and re-sign with a fresh timestamp.
4032007EMAIL_NOT_VERIFIEDTenant email not yet confirmed.Click the link in the verification email.
4042001TENANT_NOT_FOUNDtenant_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 for the full table.
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.

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:
HeaderFormatNotes
x-chert-signaturev1,<ts>,<hex>Legacy format. Supported for backward compatibility.
X-Webhook-Signaturet=<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 for the consumer-side verification snippet that handles both formats. Additional headers on every webhook delivery:
HeaderDescription
X-Webhook-EventEvent type, e.g. message.received
X-Webhook-Event-IdStable dedup key
X-Webhook-TimestampUnix seconds
X-Webhook-Subscription-IdId of the subscription that triggered this delivery

See also