SCL

SCL Onboarding — API Reference

Base URL: http://localhost:3000  ·  All endpoints return JSON

Authentication
/api/auth
No authentication required on these endpoints. Successful login/register returns a JWT token — include it as Authorization: Bearer <token> on all protected routes. Tokens expire after 7 days.
POST /api/auth/register Create a new learner account
Request Body
FieldTypeRequiredNotes
emailstringrequiredStored lowercase
passwordstringrequiredMin 8 characters
Responses

201 Returns JWT token and email. Account is immediately active.

{ "token": "eyJ...", "email": "user@example.com" }

400 Missing fields or password too short  409 Email already registered

POST /api/auth/login Log in with email and password
Request Body
FieldTypeRequiredNotes
emailstringrequired
passwordstringrequired
Responses

200 JWT token and email

{ "token": "eyJ...", "email": "user@example.com" }

401 Invalid email or password

POST /api/auth/forgot-password Request a password reset link
Request Body
FieldTypeRequiredNotes
emailstringrequired
Notes

Always returns 200 to prevent email enumeration. Sends a reset link to the email fire-and-forget. Reset token is valid for 1 hour.

{ "message": "If that email exists, a reset link has been sent" }
POST /api/auth/reset-password Set a new password using a reset token
Request Body
FieldTypeRequiredNotes
tokenstringrequiredFrom reset email link
passwordstringrequiredMin 8 characters
Responses

200

{ "message": "Password updated successfully" }

400 Invalid/expired token, or password too short

Learners
/api/learners
🔒 Requires Bearer JWT token
GET /api/learners/me Load saved form progress

Returns the learner row for the authenticated user, or null if no data saved yet.

Response
{
  "user_id": 1,
  "first_name": "Ian",
  "surname": "Stockley",
  "progress_step": 3,
  "submitted": 0,
  "completed": 0,
  ...all learner fields
}
POST /api/learners/save Upsert partial or full form data

Called after each step to persist progress. All fields are optional — only provided fields overwrite existing values.

Key Fields
FieldTypeNotes
progress_stepintegerCurrent step number (1–11)
first_name, surname, middle_name, known_asstringPersonal details
date_of_birthstringDD/MM/YYYY
legal_sex, pronoun, ethnicity, titlestring
address_line1, address_line2, address_town, address_postcodestring
years_at_addressstring
lived_3_yearsinteger0 or 1
prev_address_line1, prev_address_line2, prev_address_town, prev_address_postcodestringIf lived_3_years = 0
phone_mobile, phone_landlinestring
euro_resident_idinteger0 or 1
country_of_birthstring
has_disabilityinteger0 or 1
disability_category_1, disability_category_2stringIf has_disability = 1
has_ehcp, learning_barrier, exam_arrangementsinteger0 or 1
disability_notesstring
ec1_first_name, ec1_surname, ec1_phone, ec1_mobile, ec1_email, ec1_relationship, ec1_living_withstring/intEmergency Contact 1
ec2_first_name, ec2_surname, ec2_phone, ec2_mobile, ec2_email, ec2_relationship, ec2_living_withstring/intEmergency Contact 2 (optional)
employment_status, benefit_typestring
residency_status, right_to_workstring
prior_attainment_levelstring
quals_on_entrystringJSON array of qualification objects
id_document_typestring"Passport" or "Driving Licence"
id_numberstring
dl_surname, dl_first_names, dl_dobstringDriving licence only
heard_about_sclstring
contact_by_sms, contact_by_email, contact_by_social_media, contact_by_websiteinteger0 or 1
declaration_confirmedinteger0 or 1
Response
{ "ok": true }
POST /api/learners/submit Mark application as submitted

Sets submitted = 1 and completed = 1 on the learner record. Fires a confirmation email (non-fatal if it fails). Call after /save with progress_step: 11.

Response
{ "ok": true }
Notifications
/api/notifications
🔒 Requires Bearer JWT token
POST /api/notifications/token Save device push token
Request Body
FieldTypeRequiredNotes
tokenstringrequiredExpo push token (iOS only)
Response
{ "ok": true }
GET /api/notifications Get all notifications for current user

Returns notifications ordered by unread first, then newest first.

Response
[
  {
    "id": 12,
    "title": "Application update",
    "body": "Your application has been reviewed.",
    "read": 0,
    "created_at": "2026-03-05 10:32:00"
  }
]
POST /api/notifications/:id/read Mark a notification as read

Only marks as read if the notification belongs to the authenticated user.

Response
{ "ok": true }
POST /api/notifications/read-all Mark all notifications as read
Response
{ "ok": true }
Admin
/api/admin
🔑 Requires x-admin-password header
Pass the admin password in the x-admin-password request header. The password is set via the ADMIN_PASSWORD environment variable in backend/.env. Returns 401 if missing or wrong.
GET /api/admin/learners List all learners

Ordered newest first. Includes message counts.

Response
[
  {
    "id": 1,
    "email": "ian@example.com",
    "created_at": "2026-03-01 09:00:00",
    "full_name": "Ian Stockley",
    "submitted": 0,
    "completed": 0,
    "progress_step": 4,
    "msg_count": 2,
    "unread_count": 1
  }
]
GET /api/admin/learners/:id/details Get full details for a learner

Returns all learner form fields plus account metadata.

Response
{
  "email": "ian@example.com",
  "registered_at": "2026-03-01 09:00:00",
  "submitted_at": null,
  "progress_step": 4,
  "submitted": 0,
  "completed": 0,
  "first_name": "Ian",
  "surname": "Stockley",
  ...all form fields
}

404 User not found

GET /api/admin/learners/:id/messages Get messages sent to a learner

Returns notifications sent to this learner, newest first.

Response
[
  {
    "id": 5,
    "title": "Next steps",
    "body": "Please bring your ID to your first session.",
    "read": 1,
    "created_at": "2026-03-04 14:00:00"
  }
]
POST /api/admin/learners/:id/unlock Unlock a submitted learner for re-editing

Resets submitted and completed to 0, allowing the learner to edit and resubmit their application.

Response
{ "ok": true }
POST /api/admin/notify Send a message to one or more learners

Inserts a notification row per user. Push notification sent fire-and-forget to any iOS device tokens registered.

Request Body
FieldTypeRequiredNotes
userIdsstring | integer[]required"all" or array of user IDs (max 1000)
titlestringrequiredMax 80 characters
bodystringrequiredMax 500 characters
Example
{
  "userIds": [1, 3, 7],
  "title": "Reminder",
  "body": "Don't forget to complete step 5 of your application."
}
Response
{ "sent": 3 }
GET /api/admin/workflows List all workflows
Response
[
  {
    "id": 1,
    "name": "Day 3 nudge",
    "trigger_type": "stuck_step",
    "trigger_step": 3,
    "trigger_field": null,
    "trigger_days": 3,
    "message_title": "Still there?",
    "message_body": "You left off at step 3 — finish now!",
    "active": 1,
    "created_at": "2026-03-01 09:00:00",
    "send_count": 12
  }
]
POST /api/admin/workflows Create a new workflow
Request Body
FieldTypeRequiredNotes
namestringrequiredMax 100 characters
trigger_typestringrequirednot_started · stuck_step · awaiting · blank_field
trigger_stepintegeroptionalRequired if trigger_type = stuck_step (1–11)
trigger_fieldstringoptionalRequired if trigger_type = blank_field
trigger_daysintegerrequiredDays to wait before firing (1–365)
message_titlestringrequiredMax 80 characters
message_bodystringrequiredMax 500 characters
Trigger Types
ValueDescription
not_startedRegistered but progress_step is 0 for N days
stuck_stepStuck on a specific step for N days (requires trigger_step)
awaitingSubmitted but not yet marked complete for N days
blank_fieldA specific learner field is still empty after N days (requires trigger_field)
Response
{ "id": 4 }
PATCH /api/admin/workflows/:id Enable or disable a workflow
Request Body
FieldTypeRequiredNotes
activebooleanrequiredtrue to enable, false to pause
Response
{ "ok": true }
DELETE /api/admin/workflows/:id Delete a workflow

Permanently deletes the workflow and its send history. Cannot be undone.

Response
{ "ok": true }