Quickstart
This guide creates a test eConsult in the sandbox: find a specialty and a provider, submit a case for a new patient, attach a file, and get notified when the specialist replies.
You need a sandbox partner key (thea_sk_test_…), plus the practice key
(thea_pk_test_…) and practice ID for your test practice. Thea sends you all
three when you sign up. If you don’t have them yet, see
Environments and keys. The curl examples use
jq to keep IDs from one step to the next.
-
Set your keys and base URL
Keep the keys in environment variables, never in source code.
Terminal window export THEA_PARTNER_KEY="thea_sk_test_…"export THEA_PRACTICE_KEY="thea_pk_test_…"export THEA_PRACTICE_ID="152"export THEA_API="https://dev.theahealth.com/api/partner/v1"Every request sends the partner key. Requests about the practice’s data also send its practice key. See Authentication.
In Node.js or Python, set up a client that sends both keys on every request. Requests that don’t need the practice key ignore it.
const THEA_API = process.env.THEA_APIconst practiceId = process.env.THEA_PRACTICE_IDconst headers = {Authorization: `Bearer ${process.env.THEA_PARTNER_KEY}`,'Thea-Practice-Key': process.env.THEA_PRACTICE_KEY,'Content-Type': 'application/json',}import os, requestsTHEA_API = os.environ["THEA_API"]practice_id = os.environ["THEA_PRACTICE_ID"]session = requests.Session()session.headers["Authorization"] = f"Bearer {os.environ['THEA_PARTNER_KEY']}"session.headers["Thea-Practice-Key"] = os.environ["THEA_PRACTICE_KEY"] -
Pick a specialty
Terminal window SPECIALTY_ID=$(curl -s "$THEA_API/specialties" -H "Authorization: Bearer $THEA_PARTNER_KEY" \| jq -r '.data[] | select(.name == "Cardiology") | .id')const { data: specialties } = await fetch(`${THEA_API}/specialties`, { headers }).then((r) => r.json())const cardiology = specialties.find((s) => s.name === 'Cardiology')specialties = session.get(f"{THEA_API}/specialties").json()["data"]cardiology = next(s for s in specialties if s["name"] == "Cardiology") -
Find the provider
Every case is filed for an
activeprovider on the practice’s roster. Your test practice comes with one.Terminal window PROVIDER_ID=$(curl -s "$THEA_API/practices/$THEA_PRACTICE_ID/providers?status=active" \-H "Authorization: Bearer $THEA_PARTNER_KEY" \-H "Thea-Practice-Key: $THEA_PRACTICE_KEY" \| jq -r '[.data[] | select(.role == "provider")][0].id')const { data: roster } = await fetch(`${THEA_API}/practices/${practiceId}/providers?status=active`, { headers }).then((r) => r.json())const provider = roster.find((p) => p.role === 'provider')roster = session.get(f"{THEA_API}/practices/{practice_id}/providers", params={"status": "active"}).json()["data"]provider = next(p for p in roster if p["role"] == "provider")In production you match your own clinicians to this roster. See Providers and seats.
-
Create the eConsult
Send the patient’s demographics with the case. Thea matches them to the practice’s existing patient by your
external_id(or name and date of birth), or creates one, so a patient only exists in Thea once you send a consult. Name the provider by rosterid,npi, oremail, and put your own referral ID inexternal_ids; Thea echoes it on the case and its webhooks.Terminal window CASE_ID=$(jq -n --arg provider "$PROVIDER_ID" --arg specialty "$SPECIALTY_ID" '{patient: {external_id: "acme-pt-99812",first_name: "Avery",last_name: "Johnson",date_of_birth: "1984-03-12",gender: "female"},provider: { id: $provider },specialty_ids: [$specialty],question: "55F with new-onset palpitations; Holter shows 8% PVC burden, echo normal. Is a beta-blocker reasonable first-line?",patient_consent_given: true,external_ids: { referral_id: "acme-ref-5521" }}' \| curl -s -X POST "$THEA_API/cases" \-H "Authorization: Bearer $THEA_PARTNER_KEY" \-H "Thea-Practice-Key: $THEA_PRACTICE_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: acme-ref-5521" \-d @- \| jq -r '.id')echo "$CASE_ID"const econsult = await fetch(`${THEA_API}/cases`, {method: 'POST',headers: { ...headers, 'Idempotency-Key': 'acme-ref-5521' },body: JSON.stringify({patient: {external_id: 'acme-pt-99812',first_name: 'Avery',last_name: 'Johnson',date_of_birth: '1984-03-12',gender: 'female',},provider: { id: provider.id },specialty_ids: [cardiology.id],question: '55F with new-onset palpitations; Holter shows 8% PVC burden, echo normal. Is a beta-blocker reasonable first-line?',patient_consent_given: true,external_ids: { referral_id: 'acme-ref-5521' },}),}).then((r) => r.json())console.log(econsult.id, econsult.status, econsult.url)econsult = session.post(f"{THEA_API}/cases",headers={"Idempotency-Key": "acme-ref-5521"},json={"patient": {"external_id": "acme-pt-99812","first_name": "Avery","last_name": "Johnson","date_of_birth": "1984-03-12","gender": "female",},"provider": {"id": provider["id"]},"specialty_ids": [cardiology["id"]],"question": "55F with new-onset palpitations; Holter shows 8% PVC burden, echo normal. Is a beta-blocker reasonable first-line?","patient_consent_given": True,"external_ids": {"referral_id": "acme-ref-5521"},},).json()print(econsult["id"], econsult["status"], econsult["url"])You get
201with the case. It startsopen, and Thea assigns a specialist and notifies them, just like a consult created in the Thea app.responding_specialistsstays empty until a specialist replies. Using your referral ID as theIdempotency-Keymeans a retry can never create a second case. -
Attach the referral
Upload each file to the case in one request, as
multipart/form-data.Terminal window curl -X POST "$THEA_API/cases/$CASE_ID/files" \-H "Authorization: Bearer $THEA_PARTNER_KEY" \-H "Thea-Practice-Key: $THEA_PRACTICE_KEY" \-F "file=@referral-5521.pdf;type=application/pdf"import { readFile } from 'node:fs/promises'const form = new FormData()form.append('file', new Blob([await readFile('referral-5521.pdf')], { type: 'application/pdf' }), 'referral-5521.pdf')// Let fetch set the multipart Content-Type, so leave it out of the headers.const { 'Content-Type': _, ...authHeaders } = headersconst file = await fetch(`${THEA_API}/cases/${econsult.id}/files`, {method: 'POST',headers: authHeaders,body: form,}).then((r) => r.json())with open("referral-5521.pdf", "rb") as f:file = session.post(f"{THEA_API}/cases/{econsult['id']}/files",files={"file": ("referral-5521.pdf", f, "application/pdf")},).json() -
Get notified when things change
Register a webhook endpoint so you hear about replies and closure without polling. Webhook endpoints belong to your organization, so this only needs the partner key. The response includes the endpoint’s signing
secret, shown only once, so this saves the whole response toendpoint.json.Terminal window curl -s -X POST "$THEA_API/webhook-endpoints" \-H "Authorization: Bearer $THEA_PARTNER_KEY" \-H "Content-Type: application/json" \-d '{"url": "https://hooks.example.com/thea","events": ["case.message.created", "case.closed"]}' > endpoint.jsonENDPOINT_ID=$(jq -r '.id' endpoint.json)Move the
secretfromendpoint.jsoninto your secrets manager, then send yourself a signed test event:Terminal window curl -X POST "$THEA_API/webhook-endpoints/$ENDPOINT_ID/test" \-H "Authorization: Bearer $THEA_PARTNER_KEY" -
Read the specialist’s reply
When
case.message.createdarrives, fetch the thread with that practice’s key:Terminal window curl "$THEA_API/cases/$CASE_ID/messages" \-H "Authorization: Bearer $THEA_PARTNER_KEY" \-H "Thea-Practice-Key: $THEA_PRACTICE_KEY"

