For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Parameter
Type
Default
Description

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 with pilot=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.

Parameter
Type
Description

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 with user_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 not free and billing is enabled. They are flattened into UserDetail.billing_balance and UserDetail.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_id defaults to 190 if omitted; values below 150 are rejected.

  • User.company_id is forced from the session; do not send.

  • UserDetail.timezone_id defaults to the requesting user's timezone.

  • If User.email is provided, email_status is computed via User.checkConfirmedEmail. When the email is set, the user is active, and send_email is true, a newstaff confirmation mail is sent.

  • UserDetail.name and UserDetail.surname are 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 = 3 is 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

Parameter
Type
Description

id

string

User ID

Permissions & Field Visibility

  • Viewer user_group_id > 150 viewing another pilot: User.email, User.user_group_id, User.user_login_count, User.email_status, User.expiration, UserDetail.phone, and the entire UserCertificate array are stripped.

  • Viewer user_group_id > 150: UserDetail.notes and UserDetail.billing_balance are always stripped.

  • Viewer user_group_id < 151: gets UserDetail.notes, address, pc, city, emergency_contact, latest UserLogin, and (when billing is enabled) billing_balance / package_balance.

  • Viewer user_group_id < 151: also gets the notification preferences User.alerts, User.message_alerts, User.newsletter and User.whatsapp, read from the shared user_credentials row. 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.id is required.

  • AttributedAircraft, FlightType, and PilotGroup are 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_id change to < 150 requires the editor to have user_group_id <= 120.

  • Self-edits cannot demote yourself away from user_group_id = 100 or below.

  • When email changes, email_status is recomputed and a confirm mail is sent if the user is active and send_email is true.

  • When user_group_id actually 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 cached Auth.User('user_group_id') from continuing to grant the previous role until the session naturally expires.

  • User.alerts, User.message_alerts and User.newsletter are notification preferences. They are not columns of users: they are saved on the user_credentials row 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.created is the account creation date, as a unix timestamp in seconds. It is only writable by editors with user_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 the created field.

  • User.whatsapp is only honoured when false, 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 from POST /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

Parameter
Type
Description

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

Parameter
Type
Description

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

Field
Type
Required
Description

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, 1366. 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 walking from..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_time summed for flights where the user is PIC or SIC, while totals runs through Flight::__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

Parameter
Type
Description

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 days ranges are proportionally slower; prefer get_time_limits when 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]true if record i was persisted, false if 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

Field
Type
Description

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.

Callers with user_group_id 250 (External Auditor) get the full certificate list, but each Uploads entry has url (and thumb_url for photos) omitted — the file itself cannot be fetched, only its metadata (filename, mime, size). This applies to this endpoint and to GET /uploads/index/UserCertificate/{certId}.json.

Response

Validity Check

valid is produced by UserCertificate::checkValidLicence($userGroupId, $UserCertificate):

Field
Type
Description

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): result only requires medical.

  • user_group_id == 300 (admin / non-pilot): result only requires licence.

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 (id sent) the owner is always the one stored on the certificate: the posted user_id is ignored, so a certificate can never be moved between users. On a create, callers with user_group_id > 170 (Captain, Pilot, Student Pilot, Cabin Crew, Auditor, Mechanic) always file on their own profile — the posted user_id is 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 gets 403 when editing a certificate that is not theirs, and 404 when the certificate belongs to another company.

  • issue and expiration must parse as Y-m-d or they are silently dropped. issue may be left empty for document types that do not require one.

  • name (the free-text description) is optional — type already identifies the document. When omitted it stores as an empty string, and the apps show the certificate type instead.

  • If photo is present and uploads cleanly, an Upload record is created (type is photo/video/document based 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

Field
Type
Required
Description

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

Parameter
Description

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.

Parameter
Description

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