TERARX_BASE_URL). They use synthetic data only.
| Variable | Default | Used by |
|---|---|---|
TERARX_BASE_URL | http://localhost:4010 | all |
TERARX_ISSUER | https://brand.example.com | patient token: your registered issuer |
TERARX_AUDIENCE | https://sandbox.rx.tera.health | patient token: the audience TeraRx gives you at onboarding |
TERARX_PATIENT_ID | brand-pt-48213 | patient token: your external_patient_id |
TERARX_CLIENT_ID, TERARX_CLIENT_SECRET | demo, demo-secret | enrollment: your brand server’s OAuth client |
TERARX_WEBHOOK_SECRET | none | webhook receiver (--serve) |
Patient token
Generate a key pair, print the JWKS you publish, sign a patient assertion with the six required claims, exchange it and callGET /v1/patient. See Authentication.
// Signs a patient assertion and exchanges it for a 15-minute patient token (docs/auth.md).
// `node sign-patient-jwt.mjs`, against the local mock by default. Synthetic data only.
//
// In production your server keeps the private key, publishes the public half as a JWKS at the
// URL you registered with TeraRx, and signs one assertion each time a patient signs in to your app.
import { randomUUID } from 'node:crypto';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
const baseUrl = process.env.TERARX_BASE_URL ?? 'http://localhost:4010';
const issuer = process.env.TERARX_ISSUER ?? 'https://brand.example.com';
const audience = process.env.TERARX_AUDIENCE ?? 'https://sandbox.rx.tera.health';
const patientId = process.env.TERARX_PATIENT_ID ?? 'brand-pt-48213'; // your external_patient_id
const tenantId = process.env.TERARX_TENANT_ID; // platform partners only: the patient's brand
const kid = 'demo-2026-10';
// 1. A key pair. Generate it once and keep it; rotate by publishing a new key in your JWKS.
const { publicKey, privateKey } = await generateKeyPair('ES256', { extractable: true });
const jwks = { keys: [{ ...(await exportJWK(publicKey)), kid, alg: 'ES256', use: 'sig' }] };
console.log('JWKS to publish at your registered URL:\n' + JSON.stringify(jwks, null, 2));
// 2. The assertion: the six required claims, at most 5 minutes of lifetime, no names or health data.
// A platform partner also names the brand in `tenant_id`.
const now = Math.floor(Date.now() / 1000);
const assertion = await new SignJWT(tenantId ? { tenant_id: tenantId } : {})
.setProtectedHeader({ alg: 'ES256', kid, typ: 'JWT' })
.setIssuer(issuer)
.setSubject(patientId)
.setAudience(audience)
.setIssuedAt(now)
.setExpirationTime(now + 300)
.setJti(randomUUID())
.sign(privateKey);
console.log('\nPatient assertion:\n' + assertion);
// 3. The token exchange. No client secret: the brand's app may make this call itself.
const res = await fetch(`${baseUrl}/v1/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: assertion,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
}),
});
const body = await res.json();
console.log(`\nPOST /v1/oauth/token → ${res.status}\n` + JSON.stringify(body, null, 2));
if (!res.ok) process.exit(1);
// 4. Use the patient token on /v1/patient endpoints until it expires, then sign a new assertion.
const me = await fetch(`${baseUrl}/v1/patient`, { headers: { Authorization: `Bearer ${body.access_token}` } });
console.log(`\nGET /v1/patient → ${me.status}`);
if (!me.ok) process.exit(1);
"""Signs a patient assertion and exchanges it for a 15-minute patient token (docs/auth.md).
`python sign_patient_jwt.py`, against the local mock by default. Synthetic data only.
In production your server keeps the private key, publishes the public half as a JWKS at the
URL you registered with TeraRx, and signs one assertion each time a patient signs in to your app.
"""
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
import jwt
from cryptography.hazmat.primitives.asymmetric import ec
BASE_URL = os.environ.get("TERARX_BASE_URL", "http://localhost:4010")
ISSUER = os.environ.get("TERARX_ISSUER", "https://brand.example.com")
AUDIENCE = os.environ.get("TERARX_AUDIENCE", "https://sandbox.rx.tera.health")
PATIENT_ID = os.environ.get("TERARX_PATIENT_ID", "brand-pt-48213") # your external_patient_id
TENANT_ID = os.environ.get("TERARX_TENANT_ID") # platform partners only: the patient's brand
KID = "demo-2026-10"
def request(method, path, data=None, headers=None):
req = urllib.request.Request(BASE_URL + path, data=data, headers=headers or {}, method=method)
try:
with urllib.request.urlopen(req) as res:
return res.status, json.loads(res.read() or b"null")
except urllib.error.HTTPError as err:
return err.code, json.loads(err.read() or b"null")
def main():
# 1. A key pair. Generate it once and keep it; rotate by publishing a new key in your JWKS.
private_key = ec.generate_private_key(ec.SECP256R1())
public_jwk = json.loads(jwt.algorithms.ECAlgorithm.to_jwk(private_key.public_key()))
jwks = {"keys": [{**public_jwk, "kid": KID, "alg": "ES256", "use": "sig"}]}
print("JWKS to publish at your registered URL:\n" + json.dumps(jwks, indent=2))
# 2. The assertion: the six required claims, at most 5 minutes of lifetime, no names or health data.
# A platform partner also names the brand in `tenant_id`.
now = int(time.time())
claims = {"iss": ISSUER, "sub": PATIENT_ID, "aud": AUDIENCE, "iat": now, "exp": now + 300, "jti": str(uuid.uuid4())}
if TENANT_ID:
claims["tenant_id"] = TENANT_ID
assertion = jwt.encode(claims, private_key, algorithm="ES256", headers={"kid": KID, "typ": "JWT"})
print("\nPatient assertion:\n" + assertion)
# 3. The token exchange. No client secret: the brand's app may make this call itself.
form = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": assertion,
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
}).encode()
status, body = request("POST", "/v1/oauth/token", form, {"Content-Type": "application/x-www-form-urlencoded"})
print(f"\nPOST /v1/oauth/token -> {status}\n" + json.dumps(body, indent=2))
if status != 200:
raise SystemExit(1)
# 4. Use the patient token on /v1/patient endpoints until it expires, then sign a new assertion.
status, _ = request("GET", "/v1/patient", headers={"Authorization": f"Bearer {body['access_token']}"})
print(f"\nGET /v1/patient -> {status}")
if status != 200:
raise SystemExit(1)
if __name__ == "__main__":
main()
npm install jose, then node sign-patient-jwt.mjs. Python 3.9+: pip install "PyJWT[crypto]>=2.8,<3", then python sign_patient_jwt.py.
Webhook verification
CheckTeraRx-Signature on the raw body, accept either signature during a rotation and dedupe on TeraRx-Event-Id. Run it with no arguments to check the worked example, or with --serve for a receiver on port 4030. See Webhooks.
// Verifies TeraRx webhook signatures (docs/webhooks.md).
// `node verify-webhook.mjs` checks the worked example from the docs.
// `node verify-webhook.mjs --serve` starts a receiver on http://localhost:4030 that verifies each delivery
// with TERARX_WEBHOOK_SECRET.
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';
const TOLERANCE_SECONDS = 300;
// rawBody is the exact bytes received, never re-serialized JSON. headers use lowercase names, as Node gives them.
export function verifyTeraRx(rawBody, headers, secret, now = Date.now()) {
const timestamp = headers['terarx-timestamp'];
if (!/^\d+$/.test(timestamp ?? '') || Math.abs(now / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
// During a secret rotation the header carries two signatures; either one may match.
return (headers['terarx-signature'] ?? '').split(',').some((part) => {
const [version, hex] = part.trim().split('=');
const given = Buffer.from(hex ?? '', 'hex');
return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
});
}
// The worked example in docs/webhooks.md.
export const workedExample = {
secret: 'example_webhook_secret_new',
oldSecret: 'example_webhook_secret_old',
timestamp: '1793023500',
body: '{"id":"0190f5d8-4b2d-7e6f-8a21-c2d3e4f5a6b7","type":"clinical.flag_raised","created_at":"2026-10-26T14:05:00Z","tenant_id":"a0000000-0000-4000-8000-000000000002","data":{"enrollment_id":"0190f5c2-3a1b-7c4d-8e9f-a0b1c2d3e4f5","external_patient_id":"brand-pt-48213","flag_id":"0190f5d8-4b2c-7d5e-9f10-b1c2d3e4f5a6","severity":"critical","raised_risk_to":"high","ack_due_at":"2026-10-26T16:05:00Z"}}',
signature: 'v1=5e9f492e2cee549f0d1688a9d4a6d0505ea1687e5c293eb28a01bad16ee7aa2a',
rotatedSignature: 'v1=5e9f492e2cee549f0d1688a9d4a6d0505ea1687e5c293eb28a01bad16ee7aa2a,v1=2e8d962ded17a921cbc548b968abc024e9fcae75fd8ff8a0f2044999b0898416',
};
function checkWorkedExample() {
const { secret, oldSecret, timestamp, body, signature, rotatedSignature } = workedExample;
const at = Number(timestamp) * 1000;
const cases = [
['valid signature', verifyTeraRx(body, { 'terarx-timestamp': timestamp, 'terarx-signature': signature }, secret, at), true],
['rotation: new secret', verifyTeraRx(body, { 'terarx-timestamp': timestamp, 'terarx-signature': rotatedSignature }, secret, at), true],
['rotation: old secret', verifyTeraRx(body, { 'terarx-timestamp': timestamp, 'terarx-signature': rotatedSignature }, oldSecret, at), true],
['changed body', verifyTeraRx(body.replace('critical', 'warning'), { 'terarx-timestamp': timestamp, 'terarx-signature': signature }, secret, at), false],
['wrong secret', verifyTeraRx(body, { 'terarx-timestamp': timestamp, 'terarx-signature': signature }, 'not_the_secret', at), false],
['timestamp 6 minutes old', verifyTeraRx(body, { 'terarx-timestamp': timestamp, 'terarx-signature': signature }, secret, at + 360_000), false],
];
let failed = 0;
for (const [name, got, want] of cases) {
if (got !== want) failed++;
console.log(`${got === want ? '✔' : '✖'} ${name}: ${got ? 'accepted' : 'rejected'}`);
}
if (failed) process.exit(1);
}
function serve() {
const secret = process.env.TERARX_WEBHOOK_SECRET;
if (!secret) throw new Error('Set TERARX_WEBHOOK_SECRET to your webhook secret.');
const seen = new Map(); // event id → time received. Keep ids for at least 24 hours (use Redis or your database).
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks);
if (!verifyTeraRx(raw, req.headers, secret)) {
res.writeHead(401).end();
return;
}
const eventId = req.headers['terarx-event-id'];
if (seen.has(eventId)) {
res.writeHead(200).end(); // a retry: answer 200 without processing it again
return;
}
seen.set(eventId, Date.now());
res.writeHead(200).end(); // answer fast, then process (or queue) the event
const event = JSON.parse(raw);
console.log(`${event.type} ${event.id}`, event.data);
});
}).listen(4030, () => console.log('Receiving webhooks at http://localhost:4030'));
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
if (process.argv.includes('--serve')) serve();
else checkWorkedExample();
}
"""Verifies TeraRx webhook signatures (docs/webhooks.md).
`python verify_webhook.py` checks the worked example from the docs.
`python verify_webhook.py --serve` starts a receiver on http://localhost:4030 that verifies each delivery
with TERARX_WEBHOOK_SECRET.
"""
from __future__ import annotations
import hashlib
import hmac
import json
import os
import sys
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
TOLERANCE_SECONDS = 300
def verify_terarx(raw_body: bytes, headers, secret: str, now: float | None = None) -> bool:
"""raw_body is the exact bytes received, never re-serialized JSON. headers is any case-insensitive mapping."""
timestamp = headers.get("TeraRx-Timestamp") or ""
now = time.time() if now is None else now
if not timestamp.isdigit() or abs(now - int(timestamp)) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
# During a secret rotation the header carries two signatures; either one may match.
return any(
part.strip().startswith("v1=") and hmac.compare_digest(part.strip()[3:], expected)
for part in (headers.get("TeraRx-Signature") or "").split(",")
)
# The worked example in docs/webhooks.md.
SECRET = "example_webhook_secret_new"
OLD_SECRET = "example_webhook_secret_old"
TIMESTAMP = "1793023500"
BODY = b'{"id":"0190f5d8-4b2d-7e6f-8a21-c2d3e4f5a6b7","type":"clinical.flag_raised","created_at":"2026-10-26T14:05:00Z","tenant_id":"a0000000-0000-4000-8000-000000000002","data":{"enrollment_id":"0190f5c2-3a1b-7c4d-8e9f-a0b1c2d3e4f5","external_patient_id":"brand-pt-48213","flag_id":"0190f5d8-4b2c-7d5e-9f10-b1c2d3e4f5a6","severity":"critical","raised_risk_to":"high","ack_due_at":"2026-10-26T16:05:00Z"}}'
SIGNATURE = "v1=5e9f492e2cee549f0d1688a9d4a6d0505ea1687e5c293eb28a01bad16ee7aa2a"
ROTATED = SIGNATURE + ",v1=2e8d962ded17a921cbc548b968abc024e9fcae75fd8ff8a0f2044999b0898416"
def check_worked_example():
at = int(TIMESTAMP)
signed = {"TeraRx-Timestamp": TIMESTAMP, "TeraRx-Signature": SIGNATURE}
rotated = {"TeraRx-Timestamp": TIMESTAMP, "TeraRx-Signature": ROTATED}
cases = [
("valid signature", verify_terarx(BODY, signed, SECRET, at), True),
("rotation: new secret", verify_terarx(BODY, rotated, SECRET, at), True),
("rotation: old secret", verify_terarx(BODY, rotated, OLD_SECRET, at), True),
("changed body", verify_terarx(BODY.replace(b"critical", b"warning"), signed, SECRET, at), False),
("wrong secret", verify_terarx(BODY, signed, "not_the_secret", at), False),
("timestamp 6 minutes old", verify_terarx(BODY, signed, SECRET, at + 360), False),
]
failed = 0
for name, got, want in cases:
failed += got != want
print(f"{'ok ' if got == want else 'FAIL'} {name}: {'accepted' if got else 'rejected'}")
if failed:
raise SystemExit(1)
def serve():
secret = os.environ.get("TERARX_WEBHOOK_SECRET")
if not secret:
raise SystemExit("Set TERARX_WEBHOOK_SECRET to your webhook secret.")
seen = {} # event id -> time received. Keep ids for at least 24 hours (use Redis or your database).
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
if not verify_terarx(raw, self.headers, secret):
self.send_response(401)
self.end_headers()
return
event_id = self.headers.get("TeraRx-Event-Id")
repeat = event_id in seen
seen.setdefault(event_id, time.time())
self.send_response(200) # a retry gets 200 without processing it again
self.end_headers()
if not repeat: # answer fast, then process (or queue) the event
event = json.loads(raw)
print(event["type"], event["id"], event["data"])
print("Receiving webhooks at http://localhost:4030")
HTTPServer(("127.0.0.1", 4030), Receiver).serve_forever()
if __name__ == "__main__":
serve() if "--serve" in sys.argv else check_worked_example()
Enrollment
Client credentials, thenPOST /v1/enrollments with an Idempotency-Key you reuse on every retry.
enroll-patient.mjs
// A brand server gets a token with client credentials and enrolls a patient at checkout (docs/auth.md).
// `node enroll-patient.mjs`, against the local mock by default. Synthetic data only.
import { randomUUID } from 'node:crypto';
const baseUrl = process.env.TERARX_BASE_URL ?? 'http://localhost:4010';
const clientId = process.env.TERARX_CLIENT_ID ?? 'demo';
const clientSecret = process.env.TERARX_CLIENT_SECRET ?? 'demo-secret';
// 1. Client credentials. Cache the token for its expires_in (1 hour) instead of fetching one per call.
const tokenRes = await fetch(`${baseUrl}/v1/oauth/token`, {
method: 'POST',
headers: {
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'enrollments:write' }),
});
const token = await tokenRes.json();
console.log(`POST /v1/oauth/token → ${tokenRes.status}`);
if (!tokenRes.ok) process.exit(1);
// 2. The enrollment. Generate the Idempotency-Key once per checkout and reuse it on every retry,
// so a timeout followed by a retry cannot enroll the patient twice.
const idempotencyKey = randomUUID();
const enrollment = {
plan_code: 'ai_health_fitness',
external_patient_id: 'brand-pt-48213',
external_order_id: 'ord-2026-10-000123',
purchased_at: '2026-10-21T14:58:00Z',
patient: {
first_name: 'Jordan',
last_name: 'Rivera',
email: 'jordan.rivera@example.com',
phone: '+12025550143',
date_of_birth: '1986-04-12',
sex: 'female',
height_cm: 165.1,
state_code: 'NY',
timezone: 'America/New_York',
units: 'imperial',
},
consents: [{ document_code: 'coaching_consent', version: '2026-01', accepted_at: '2026-10-21T14:57:30Z' }],
treatment: {
medication_id: '6f1d2c3b-4a59-4e8f-9d7c-1b2a3c4d5e6f',
dose_mg: 0.25,
phase: 'titration',
injection_weekday: 1,
started_on: '2026-09-28',
},
};
async function enroll() {
const res = await fetch(`${baseUrl}/v1/enrollments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token.access_token}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(enrollment),
});
const body = await res.json();
console.log(`POST /v1/enrollments → ${res.status}${res.headers.get('idempotent-replayed') === 'true' ? ' (replayed)' : ''}`);
if (!res.ok) {
console.error(body);
process.exit(1);
}
return body;
}
const created = await enroll();
console.log(JSON.stringify({ id: created.id, status: created.status, stage: created.stage }, null, 2));
// 3. A retry with the same key returns the first response (Idempotent-Replayed: true) on the real API.
// The mock does not store anything, so here it answers like a first call.
await enroll();

