TrueRoute Developers

Authentication and request signing

Every request carries your API key. Every POST also carries a signature over the exact bytes sent, made with your request-signing secret. GETs are never signed.

ConcernRule
AuthenticationAuthorization: Bearer <API_KEY> on every call
SigningX-TrueRoute-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> on every POST
Clockthe signature's t must be within 300 seconds of TrueRoute's clock, in both directions: keep NTP
Idempotencya fresh UUID per logical operation (at most 255 characters), reused with the SAME bytes on every retry

API keys

A key is issued once, with its request-signing secret, and shown once. A sandbox key begins tr_sandbox_ and a production key tr_live_. The prefix is a human affordance so a key found in a log names its own blast radius: never validate keys against it, and treat the whole value as opaque.

Example (fake): tr_sandbox_EXAMPLE_KEY_DO_NOT_USE

The two signed forms

The form is fixed per endpoint; the reference marks each one.

FormUsed onSigned material
unkeyedPOSTs that do not read an Idempotency-Key{t}.{rawBody}
keyedPOSTs that require an Idempotency-KeyTRv1.{t}.{keyLength}.{key}.{rawBody}

keyLength is the key's length in characters, so a key and a body can never be re-split into a different pair. The MAC is HMAC-SHA256 with your request-signing secret, written as lower-case hex.

Test vector

Check your signer against these values before you send anything. The secret is a fake published for this purpose; TrueRoute's test suite verifies both headers with the server's own verifier.

InputValue
secrettrsec_EXAMPLE_SECRET_DO_NOT_USE
t1767225600
rawBody{"example":true}
Idempotency-Key (keyed form)00000000-0000-4000-8000-000000000000
unkeyed: X-TrueRoute-Signature: t=1767225600,v1=1e79a362dd96b749a0143d7ba5fec629aefef816292e9e5f7d422f68bf1013d4
keyed:   X-TrueRoute-Signature: t=1767225600,v1=2c02b72be6f939630509257a26163eaf567815095d882eb71bf7762597b06141

A signer in TypeScript

Lifted at build time from TrueRoute's partner sample, which the test suite executes against the server's verifier (Node 18+, no dependencies).

import { createHmac } from 'node:crypto'

/**
 * The `X-TrueRoute-Signature` header for one request.
 *
 * `rawBody` is the exact string you send. `idempotencyKey` is the `Idempotency-Key` header value on
 * an endpoint that requires one (the "keyed" form), or null on an endpoint that does not read it
 * (the "unkeyed" form). Which form an endpoint uses is fixed: see REFERENCE.md section 4.
 */
export function trueRouteSignature(input: {
  signingSecret: string
  rawBody: string
  idempotencyKey: string | null
  timestampSeconds: number
}): string {
  const t = String(Math.floor(input.timestampSeconds))
  const material =
    input.idempotencyKey === null
      ? `${t}.${input.rawBody}`
      : `TRv1.${t}.${input.idempotencyKey.length}.${input.idempotencyKey}.${input.rawBody}`
  const v1 = createHmac('sha256', input.signingSecret).update(material, 'utf8').digest('hex')
  return `t=${t},v1=${v1}`
}

Idempotency

Why a failure says so little

authentication_failed deliberately does not say whether the key or the signature was wrong: that answer would confirm a stolen key. Check the key, the signing secret, the form for that endpoint, and that you signed the bytes you sent.