Pilots
Pilots List (Dropdown)
GET /pilots/list[/active:{1|0|all}][/pilot:{1|0|all}].json
Minimal, non-paginated list of users in the caller's company. Designed for dropdowns / selection fields. Soft-deleted users (deleted=1) are always excluded.
Named Parameters
active
1 | 0 | all
1
1 only active; 0 only inactive; all both
pilot
1 | 0 | all
1
1 only flying users; 0 only non-flying; all both
Response
{
"pilots": [
{ "id": "123", "name": "John", "surname": "Doe", "user_group_id": "150", "active": true, "pilot": true },
{ "id": "124", "name": "Jane", "surname": "Smith", "user_group_id": "190", "active": true, "pilot": true }
]
}Sorted by User.user_group_id ASC, UserDetail.name ASC, UserDetail.surname ASC — managers first, then instructors, then pilots, then students.
Use this in preference to /pilots/index when you only need {id, name, surname} for dropdowns: it is a single non-paginated query and is much cheaper than walking the paginated index. Examples:
/pilots/list.json— active flying users (audit-grade dropdown, default)/pilots/list/active:all.json— all flying users including inactive (logbook audit pilot filter)/pilots/list/pilot:all.json— all active users including non-flying (e.g. crew pickers that must include instructors withpilot=0)
Pilots Index (Paginated)
GET /pilots/index/page:{page}/search:{search}/user_group_id:{groupId}/pilot_group:{groupId}/base_id:{baseId}/active:{active}/pilot:{pilot}/limit:{limit}.json
Retrieve a paginated, filterable list of pilots with full details. Must be a JSON or AJAX request unless excel:1 is set.
Path Parameters
All filter parameters are optional — use empty string to skip.
page
number
Page number (starts at 1)
search
string
Matches name, surname, name+surname, surname+name, companyid, passport, email prefix, or exact User.id
user_group_id
string
Filter by user group/role. 150 is treated as <= 150 (all managers)
pilot_group
string
Filter by pilot group ID
base_id
string
Filter by base
active
boolean
Filter active/inactive users (active:false to include only inactive)
pilot
boolean
Filter pilot vs non-pilot accounts
limit
number
Page size (default 50, max 100000)
excel
boolean
Render the XLS export view (limit forced to 100000)
Permissions & Field Visibility
Viewers with
user_group_id > 170(e.g. students) get a reduced field set and the result is restricted to users withuser_group_id <= 170(i.e. they cannot see other students/clients).Billing fields (
User.billing,UserBill,UserBillPackage) are only included when the company plan is notfreeand billing is enabled. They are flattened intoUserDetail.billing_balanceandUserDetail.package_balance.
Response
PilotGroup is reshaped to { color: name }. email_status reflects User.checkConfirmedEmail outcomes (e.g. confirmed, pending).
Create Pilot
POST /pilots/create.json
Create a new pilot account. Restricted to user_group_id ∈ {1, 100, 105, 110, 120, 150}.
The free plan is capped at 100 pilots — additional pilots return 400.
Request Body
User.user_group_iddefaults to190if omitted; values below150are rejected.User.company_idis forced from the session; do not send.UserDetail.timezone_iddefaults to the requesting user's timezone.If
User.emailis provided,email_statusis computed viaUser.checkConfirmedEmail. When the email is set, the user isactive, andsend_emailis true, anewstaffconfirmation mail is sent.UserDetail.nameandUserDetail.surnameare required.
Response
On failure: result = false, validation contains User.invalidFields().
My Pilot View
GET /pilots/view.json
Retrieve the authenticated user's own pilot profile. Same payload as View Pilot below.
Note: passing
userId = 3is treated as "view self" for legacy reasons.
View Pilot
GET /pilots/view/{id}.json
Retrieve full pilot details including certificates, pilot groups, attributed aircraft, and flight types. Scoped to the viewer's company.
Path Parameters
id
string
User ID
Permissions & Field Visibility
Viewer
user_group_id > 150viewing another pilot:User.email,User.user_group_id,User.user_login_count,User.email_status,User.expiration,UserDetail.phone, and the entireUserCertificatearray are stripped.Viewer
user_group_id > 150:UserDetail.notesandUserDetail.billing_balanceare always stripped.Viewer
user_group_id < 151: getsUserDetail.notes,address,pc,city,emergency_contact, latestUserLogin, and (when billing is enabled)billing_balance/package_balance.Viewer
user_group_id < 151: also gets the notification preferencesUser.alerts,User.message_alerts,User.newsletterandUser.whatsapp, read from the shareduser_credentialsrow. They are absent for any other viewer.
Response
Returns 404 if the pilot is not in the viewer's company.
Edit Pilot
POST /pilots/edit.json
Update pilot profile details. Restricted to user_group_id ∈ {1, 100, 105, 110, 120, 150}.
Request Body
User.idis required.AttributedAircraft,FlightType, andPilotGroupare passed as{ id: truthy }maps; only the keys are used. Existing pilot ↔ aircraft / flight-type / pilot-group joins are wiped before re-saving.A
user_group_idchange to< 150requires the editor to haveuser_group_id <= 120.Self-edits cannot demote yourself away from
user_group_id = 100or below.When
emailchanges,email_statusis recomputed and aconfirmmail is sent if the user is active andsend_emailis true.When
user_group_idactually changes, all of the edited user's active sessions are deleted server-side, forcing them to re-authenticate on their next request. This avoids the cachedAuth.User('user_group_id')from continuing to grant the previous role until the session naturally expires.User.alerts,User.message_alertsandUser.newsletterare notification preferences. They are not columns ofusers: they are saved on theuser_credentialsrow shared by every company account of that email address, so setting them here changes them for all of that person's accounts. Omit a key to leave it untouched.User.createdis the account creation date, as a unix timestamp in seconds. It is only writable by editors withuser_group_id <= 150; for anybody else, and for any non-numeric, non-positive or more-than-48h-in-the-future value, the key is silently dropped and the stored date is left untouched. The change is recorded in the account history (GET /pilots/changes/{userId}.json) under thecreatedfield.User.whatsappis only honoured whenfalse, which switches WhatsApp notifications off. It can never be enabled from here: turning it on requires the verification code sent to the pilot's phone, entered by the pilot fromPOST /users/whatsapp.json.
Response
On failure, errors is the flattened User.invalidFields().
Pilot Totals
GET /pilots/totals/{userId}.json
Cumulative flight hour totals (in seconds) for a pilot, broken down by function and rule.
Path Parameters
userId
string
User ID. Defaults to the authenticated user.
Response
other_companies aggregates time from other companies that share the same email. previous_time is UserDetail.flight_hours * 3600. All times are in seconds. flight_time = total_time − rules.SIM.time.
PIC total includes FI time plus all instructor/examiner/PICUS classifications: PICUS, CRI, IRI, FIFI, SFI, TRI, EXA. FI total includes the instructor/examiner classes (CRI, IRI, FIFI, SFI, TRI, EXA) — but not PICUS, which is PIC time only. Each class is also returned as its own line so the time worked in each function can be seen separately — these lines break down what is already inside PIC/FI, they are not additional totals.
SIC time is only credited on multipilot aircraft (legacy copic semantics; copic was migrated to sic). SUPERVISOR (sup) is reported separately only and is not rolled into PIC. The former COPILOT line is removed.
Pilot Currency
GET /pilots/get_currency/{userId}/{d1}/{d2}/{d3}.json
Check pilot landings/hours within rolling day windows against the company-configured currency requirements.
Path Parameters
userId
string
User ID. Forced to the authenticated user when null or when caller user_group_id > 170.
d1
number
First window in days (default 30, max 999).
d2
number
Second window in days (default 90, max 999).
d3
number
Third window in days (default null/disabled).
Response
Hours are in seconds. requirements mirrors CompanySetting.currency{1,2,3}_{flighttime,landings}.
Pilot Time Limits
POST /pilots/get_time_limits.json
Daily breakdown plus period/year totals of block flight time and duty time for a given user, anchored to a Unix timestamp.
Two modes: by default the endpoint reports the calendar month the timestamp falls in. Pass days to get a rolling window of the last N days instead — the window ends on the timestamp's own day and can span more than one month (e.g. days: 28 for the EASA FTL cumulative window).
Request Body
timestamp
number
Yes
Unix timestamp anchoring the period. In month mode it selects the month; in window mode it is the last day of the window
user_id
number
Yes
Target user ID
days
number
No
Rolling window length in days, 1–366. Omit for calendar-month mode
timestamp and user_id must be numeric and non-empty; days, when present, must be numeric and within range, or the request is rejected with 400 Invalid values.
Dates are resolved server-side in GMT (
date_default_timezone_set('GMT')), not the company timezone. A client that wants the window to end on the pilot's own local date should send a timestamp at midday UTC of that date, so no company offset can push it onto the neighbouring day.
Response — month mode (no days)
Response — window mode (days sent)
The daily dictionary and its total move from the month key to a period key, so the two modes can never be mistaken for one another. from/to give the exact window the server used.
Flight totals are in hours (formatted strings). Duty totals come from PilotDutyRecord::__total. Daily values are summed by date. year is always the calendar year of timestamp in both modes.
The daily dictionary is sparse — days with no flight/duty time are absent entirely, not present as
0. Clients drawing a per-day chart must fill the gaps themselves by walkingfrom..to(or the month) rather than iterating the dictionary's keys. An empty dictionary is serialised by PHP as[], not{}.
The daily dictionary and the totals come from different queries: the daily values are raw
block_timesummed for flights where the user is PIC or SIC, whiletotalsruns throughFlight::__getPilotFlightTime, which applies each flight type's per-seat flight-time rules. The daily values will therefore not always add up to the total — show the total, don't compute it client-side.
Duty Records
GET /pilots/duty/{days}/{user}.json
POST /pilots/duty/{days}/{user}.json
GET — last N days of duty records
days
number
Number of trailing days to return, counting back from today (default 3). No maximum — e.g. pass 31 for a full month.
user
string
Present in the route but ignored on GET — see note below.
Access control / behaviour notes (GET):
The record lookup is hardcoded to the authenticated user (
Auth->User('id')). The{user}path parameter is ignored on GET, so the endpoint always returns the caller's own duty records regardless of the value passed — cross-user reads are not supported here. For another pilot's totals use Pilot Time Limits (get_time_limits) or the manager duty endpoints.Records are built one day at a time, with one database query per day (N+1). Large
daysranges are proportionally slower; preferget_time_limitswhen you only need monthly/yearly totals.Days with no saved record are returned with empty-string time fields.
POST — bulk save duty records
Body is a numerically-indexed array of PilotDutyRecord objects (flat, no model alias). Each record needs date; in_duty/out_duty/in_work/out_work/in_flight/out_flight are optional time fields (HH:MM or HH:MM:SS). user_id and company_id are auto-filled from the session when omitted. Existing row for (company, user, date) is replaced.
Each record is validated and saved independently in a loop; an invalid record does not block the rest.
save[i]—trueif recordiwas persisted,falseif it failed validation.errors[i]— per-field validation errors for failed records.
Valid time strings only. Literal "null" is rejected; send an empty string or omit the field to clear/leave unchanged. Empty string overwrites the existing column with NULL.
Certificates
Certificate Types
GET /pilots/certificate_types.json
Returns the canonical certificate type catalogue used across the app.
Response
type
string
Category — one of licence, rating, medical, training, document
validity
number | null
Default validity window in months. null = no expiry.
name
string
Display name
abbr
string
Short code for compact tables
icon
string
Font Awesome class
requires_expiration
boolean
Whether an expiration date should be required at input
requires_issue
boolean
Whether an issue date should be required at input
mandatory
boolean
Counts toward the valid certificate result
order
number
Display sort order
group_role
string
pilot | all — restricts which user roles see this type
List Certificates
GET /pilots/certificates/{userId}.json
Retrieve all certificates for a pilot plus the validity summary. JSON-only.
Response
Validity Check
valid is produced by UserCertificate::checkValidLicence($userGroupId, $UserCertificate):
result
boolean
Overall validity. true iff licence + rating + medical + training are all valid (or, for ground roles, just the relevant subset — see below).
licence
boolean | null
At least one valid licence cert.
rating
boolean | null
All rating certs valid (any single invalid rating fails).
medical
boolean | null
At least one valid medical cert.
training
boolean | null
All training certs valid.
limit
number | null
Earliest expiry timestamp (epoch seconds) across all certs with an expiration.
Special cases:
user_group_id == 200(student):resultonly requiresmedical.user_group_id == 300(admin / non-pilot):resultonly requireslicence.
View Certificate
GET /pilots/certificate/{certId}.json
Retrieve a single certificate with up to 5 attached uploads.
Add Certificate
POST /pilots/add_certificate.json
Create a new certificate, or update an existing one by sending its id. Uses multipart/form-data for the optional file attachment.
Request Body
Ownership. On an update (
idsent) the owner is always the one stored on the certificate: the posteduser_idis ignored, so a certificate can never be moved between users. On a create, callers withuser_group_id > 170(Captain, Pilot, Student Pilot, Cabin Crew, Auditor, Mechanic) always file on their own profile — the posteduser_idis replaced with the authenticated user.Managers, Chief Pilots and Flight Instructors (
user_group_id <= 170) can file and edit certificates for any user in their own company. Everyone else gets403when editing a certificate that is not theirs, and404when the certificate belongs to another company.issueandexpirationmust parse asY-m-dor they are silently dropped.issuemay be left empty for document types that do not require one.name(the free-text description) is optional —typealready identifies the document. When omitted it stores as an empty string, and the apps show the certificate type instead.If
photois present and uploads cleanly, anUploadrecord is created (typeisphoto/video/documentbased on MIME) and the file is sent to S3.
Response
errors is populated on validation failure or upload errors.
Delete Certificate
POST /pilots/delete_certificate/{id}.json
Delete a certificate. Callers with user_group_id > 150 can only delete their own certificates; managers can delete any certificate within their company.
Returns 404 if the certificate is not found in the caller's scope.
Attributions
GET /pilots/attributions/{userId}.json
Returns the aircraft and flight types attributed to a pilot. {userId} is optional; when omitted, the authenticated user is used. Callers with user_group_id > 170 can only query themselves.
For each category, the response is an id => label map of the pilot's explicit attributions. If the pilot has no attribution set for a given category, the full active company list for that category is returned instead.
Returns 404 if the pilot is not in the caller's company and 403 when a non-manager queries another user.
Manager-only Endpoints
Save Pilot Notes
POST /manager/pilots/notes.json
Persist a private (manager-only) note onto a pilot's UserDetail.notes. Requires user_group_id <= 170.
Request Body
pilot
string
Yes
Pilot user ID
notes
string
Yes
Note body
Delete Pilot
POST /manager/pilots/delete/{id}.json
Soft-delete a pilot. The caller cannot delete themselves.
Recalculate Duties
GET /manager/pilots/recalculate_duties/{date}.json
Re-runs PilotDutyRecord::autoCalc for every PIC/SIC/Supervisor on flights logged on {date} (format YYYY-MM-DD). Existing duty rows for that date are cleared first. Up to 100 flights are processed per call.
FI Assignments Tree
GET /manager/pilots/fi_assignments.json
Returns instructor → supervised-pilot trees for the company in a tree-view friendly shape (icon, color, href, text, nodes). Branches with more than 6 supervised pilots are flagged with backColor = "#ff9900".
For caller user_group_id <= 150, also returns unAssignedStudents — students whose supervisor_id is empty or points to an inactive instructor.
Audit
GET /manager/pilots/audit/from:{from}/to:{to}/group:{group}/pilot_group:{pilot_group}/pilot:{pilot}/all_certs:{all_certs}.json
Paginated audit view of pilots and instructors with their latest flight, training status, certificates, and supervisor.
Path Parameters
from
Lower bound on latest_flight_date (YYYY-MM-DD or epoch seconds)
to
Upper bound on latest_flight_date
group
user_group_id (or 150 for <= 150)
pilot
Truthy to restrict to pilots only
pilot_group
Pilot group ID
all_certs
If absent or false, certificates are filtered to licence, rating, medical only
JSON or AJAX requests return:
The HTML version of this endpoint also returns userGroups and pilotGroups for filter dropdowns.
Monthly Duties Report
GET /manager/pilots/duties/month:{month}/year:{year}/group:{group}/data:{data}.json
Per-pilot daily breakdown of duty / FDP / work time for a single month. Premium and unlimited plans only; lower plans return the upgrade view.
month
1–12 (default current month)
year
YYYY (default current year)
group
user_group_id (150 for <= 150)
data
duty (default), flight, or work — selects which in_*/out_* columns drive the totals
Flight.block_time is total block time in seconds for the selected month, computed via Flight->__getPilotFlightTime() (respects company FlightType PIC/SIC rules).
/manager/pilots/duties/{export}.json with a truthy export segment renders the XLS view instead.
Manager — Pilot Duty PDF
GET /manager/pilots/duty/{user}/{year}/{month}
Renders a duty/flight report as a PDF (not JSON). Listed here for completeness.
Notes on Removed Endpoints
POST /pilots/reminder.json (resend activation email) was removed. Use the standard email-confirmation flow triggered automatically by create / edit when send_email = true and the email changes or the account is newly activated.
Last updated