Skip to content

App routes

Mounted at /app. App routes use JWT audience snr_usr (set on the server). Same Authorization header pattern as admin (encrypted token), with a user-scoped JWT.

Returns static bilingual “how it works” copy. Language is selected by the lang header:

langLanguage
0, missing, or other numeric (except 1)English
1Arabic
en / ar (case-insensitive)English / Arabic
HeaderRequiredNotes
AuthorizationyesEncrypted token (hex ciphertext)
langno0 / 1 or en / ar
Content-TypenoBody is ignored; application/json is fine
FieldTypeNotes
lang0 | 1Echo of effective language used
titlestringScreen title
subtitlestringShort intro
stepsarrayOrdered steps: seq, title, icon
cta.submit_nowstringPrimary CTA label
{
"status": 1000,
"message": "ok",
"data": {
"lang": 0,
"title": "How second opinion works",
"subtitle": "A few simple steps to get an expert medical review of your case.",
"steps": [
{ "seq": 1, "title": "Share your medical documents and details securely.", "icon": "document" },
{ "seq": 2, "title": "Our coordinators match you with the right specialists.", "icon": "users" },
{ "seq": 3, "title": "Expert doctors review your case and records.", "icon": "stethoscope" },
{ "seq": 4, "title": "Receive your second opinion summary and next steps.", "icon": "check_circle" }
],
"cta": { "submit_now": "Submit your case now" }
}
}

Returns localized screen copy plus all active review types for the picker UI (not paginated; capped at 100 rows). Same lang header rules as POST /app/how. Requires D1 migration that adds review_types.icon (see migrations/0004_review_types_add_icon.sql).

Request body may be {}. Headers: Authorization (required); lang selects English vs Arabic strings for title, subtitle, and each item’s title, description, and duration.

FieldTypeNotes
lang0 | 1Effective language
titlestringScreen title
subtitlestringScreen subtitle
itemsarrayActive Status rows only, newest first

Each element of items:

FieldTypeNotes
rt_idstringreview_type_id
titlestringLocalized name
descriptionstringLocalized description
durationstringLocalized duration from min/max review days
doctor_countnumberReviewedDoctorsCount
pricenumberSame as admin
iconstringFrom DB, or "document" if unset
{
"status": 1000,
"message": "ok",
"data": {
"lang": 0,
"title": "Select review type",
"subtitle": "Get expert medical opinion from our panel",
"items": [
{
"rt_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Standard Review",
"description": "Reviewed by 3 specialist oncologists",
"doctor_count": 3,
"duration": "8 - 12 days",
"price": 900,
"icon": "document"
}
]
}
}

Lists active disease categories for pickers (e.g. “Select Disease Category”). Uses the same lang header rules as POST /app/how (0 / en → English speciality label, 1 / ar → Arabic).

FieldTypeRequiredNotes
qstringnoOptional filter on English / Arabic specialty (similar to admin list)
FieldTypeNotes
lang0 | 1Effective language
itemsarrayEach element: { "key", "value" }key = disease id (dc_id), value = localized speciality label

Up to 300 active rows.

{
"status": 1000,
"message": "ok",
"data": {
"lang": 1,
"items": [
{ "key": "501df25c-1cff-4e46-951c-6be32ed0159b", "value": "أمراض القلب" }
]
}
}

Stored in D1 table review_cases (see migrations/0005_create_review_cases.sql; rejected / reject_reason: 0007_review_cases_reject.sql; case_coordinator_id: 0008_review_case_coordinator_second_opinions.sql). user_id is taken from the JWT (AuthUser.id); do not send it in the body.

Column / fieldNotes
case_idUUID primary key (returned on create)
rt_idActive review type (review_types.review_type_id)
dc_idActive disease category (disease_categories.dc_id)
patient_ageInteger 0–150
patient_gendermale | female
statusdraft | review | rejected (review = submitted for processing; rejected set by admin)
medical_historyOptional string
medical_documentsJSON array of objects (e.g. { "url", "key", "name" }), max 50 items
reject_reasonSet when status is rejected; otherwise null
case_coordinator_idProvider / employee id of assigned coordinator; set by POST /admin/assign-coordinator; otherwise null
is_notified0 until a 24-hour draft reminder is sent, then 1 (migration 0035_review_cases_is_notified.sql)

Table second_opinions (migrations 0008 / 0009 / 0010): assignment history — sop_id, case_id, doctor_id, requested_documents (JSON array of strings), created_date, active, is_coordinator (integer 0 | 1, D1 boolean).


Creates a new case. JWT audience snr_usr.

FieldTypeRequired
rt_idstringyes
dc_idstringyes
patient_agenumberyes
patient_genderstringyes (male / female)
statusstringyes (draft / review)
medical_historystringno
medical_documentsarrayno (default [])
{
"status": 1000,
"message": "Case saved",
"data": { "case_id": "uuid" }
}

Invalid rt_id / dc_id400 (status 1002). Server error → 500 (1001).


Processes reminders for cases still in draft status at least 24 hours after creation. Each eligible case receives the UCP98 draft notification once; review_cases.is_notified prevents duplicate sends. Failed notification calls are released for retry on a later invocation.

{
"status": 1000,
"message": "Draft notifications processed successfully",
"data": {
"eligible": 1,
"notified": 1
}
}

Requires D1 migration 0035_review_cases_is_notified.sql.


Updates an existing case only while status on the row is draft. After the case is review, edits return 409. JWT audience snr_usr.

Same fields as save, plus:

FieldTypeRequired
case_idstringyes

Lists the authenticated user’s cases, newest first.

FieldTypeRequiredNotes
pagenumbernodefault 1
page_sizenumbernodefault 15, max 100

data is an array of case objects (same shape as detail). Includes pagination: total, page, page_size.


Appends med_docs into existing medical_documents for a draft case.

FieldTypeRequiredNotes
case_idstringyes
med_docsobject[]yesArray entries must include non-empty url, name, key. Max 50 total docs per case after append.

Stored in D1 as JSON object array and appended in-place.

{
"status": 1000,
"message": "Medical documents updated",
"data": {}
}

404 if the case is not yours; 409 if the case is not draft.

{
"case_id": "550e8400-e29b-41d4-a716-446655440000",
"med_docs": [
{
"url": "https://example.com/storage/report.pdf",
"name": "primary-physician-report.pdf",
"key": "uploads/user123/report.pdf"
},
{
"url": "https://example.com/storage/lab.jpg",
"name": "lab.jpg",
"key": "uploads/user123/lab.jpg"
}
]
}

Single case for the authenticated user.

FieldTypeRequired
case_idstringyes

Not found → 404 (status 1002 in body per common envelope).

List and detail responses include reject_reason and case_coordinator_id (string or null) when present in D1.