/api/v1/diver/*. Documented once here; endpoint files link to the relevant section instead of repeating the field list.App\Http\Resources\Api\V1\CourseLogResource. Returned as a collection by GET /api/v1/diver/courses and GET /api/v1/diver/free-learnings (see findings.md — the courses endpoint itself is currently broken; this shape was captured live through free-learnings, which renders the same resource class against the same course_logs-style data).{
"log_code": "9be9084934f36b03bbbb70c38acb9d50",
"course": {
"id": 165,
"name": "Ice",
"group": "recreational",
"need_quiz": true,
"need_exam": true,
"need_skills": true
},
"instructor": null,
"dive_center": null,
"purchase_date": "2022-08-08",
"expire_date": null,
"quiz_lock": false,
"admin_lock": false,
"exam_enable": false,
"is_completed": false
}instructor / dive_center are assigned, captured live elsewhere:"instructor": { "id": 421, "full_name": "Example Instructor" },
"dive_center": { "id": 1281, "name": "Example Dive Center" }| Field | Type | Nullable | Notes |
|---|---|---|---|
log_code | string(32) | No | Opaque code, not a primary key. See _endpoint-template.md conventions |
course.id | integer | Yes | null only if the course record was deleted |
course.name | string | Yes | |
course.group | string | Yes | Enum value (not label), e.g. recreational, trainer |
course.need_quiz | boolean | No | |
course.need_exam | boolean | No | |
course.need_skills | boolean | No | |
instructor | object | Yes | null when no instructor is assigned to the log |
instructor.id | integer | No | Present only inside a non-null instructor |
instructor.full_name | string | No | Present only inside a non-null instructor |
dive_center | object | Yes | null when no dive centre is assigned |
dive_center.id | integer | No | Present only inside a non-null dive_center |
dive_center.name | string | Yes | Reads from dive_center->company->name; null if the centre has no company record |
purchase_date | string | Yes | Y-m-d |
expire_date | string | Yes | Y-m-d |
quiz_lock | boolean | No | Set by the anti-cheat check in CourseController::submitQuiz() |
admin_lock | boolean | No | Set by an administrator, or by the same anti-cheat check |
exam_enable | boolean | No | |
is_completed | boolean | No |
CourseLogDetailResource)App\Http\Resources\Api\V1\CourseLogDetailResource. Intended for GET /api/v1/diver/courses/{log_code} and GET /api/v1/diver/free-learnings/{log_code}.findings.md — skills_completed collides between a hasMany relation and a boolean accessor, and the resource's property access always resolves to the boolean, so ->sum() throws. The shape below is reconstructed field-by-field from the resource source, not from a live response; treat the field list as accurate but the example as illustrative, not captured.{
"log_code": "a6b952760c2b672d2f45da6989b1a8c1",
"course": {
"id": 12,
"name": "Open Water",
"group": "recreational",
"need_quiz": true,
"need_exam": true,
"need_skills": true
},
"instructor": { "id": 421, "full_name": "Example Instructor" },
"dive_center": { "id": 1281, "name": "Example Dive Center" },
"purchase_date": "2023-01-10",
"expire_date": null,
"quiz_lock": false,
"admin_lock": false,
"exam_enable": true,
"modules": [
{
"id": 41,
"name": "Module 1",
"quiz_questions": 10,
"quiz_minimum": 8,
"quiz_status": {
"passed": true,
"score": 100,
"date": "2023-01-12"
}
}
],
"exam_status": {
"passed": true,
"score": 96.5,
"date": "2023-01-20"
},
"skills_count": {
"total": 12,
"signed": 12
},
"is_completed": true
}| Field | Type | Nullable | Notes |
|---|---|---|---|
log_code | string(32) | No | |
course.* | Same shape as the course log's course object above | ||
instructor | object | Yes | Same shape as the course log's instructor |
dive_center | object | Yes | Same shape as the course log's dive_center |
purchase_date | string | Yes | Y-m-d |
expire_date | string | Yes | Y-m-d |
quiz_lock | boolean | No | |
admin_lock | boolean | No | |
exam_enable | boolean | No | |
modules | array | No | One entry per module in the course's version, [] when the course has no modules |
modules[].id | integer | No | |
modules[].name | string | No | |
modules[].quiz_questions | integer | No | Configured question count for the module's quiz |
modules[].quiz_minimum | integer | No | Passing threshold |
modules[].quiz_status | object | Yes | null when the module's quiz has never been attempted |
modules[].quiz_status.passed | boolean | No | Present only inside a non-null quiz_status |
modules[].quiz_status.score | number | No | Present only inside a non-null quiz_status |
modules[].quiz_status.date | string | No | Y-m-d, present only inside a non-null quiz_status |
exam_status | object | Yes | null when the exam has never been attempted |
exam_status.passed / .score / .date | Same shape as modules[].quiz_status | ||
skills_count.total | integer | No | Sum of skill counts across every skill log for this course log |
skills_count.signed | integer | No | Sum of diver-signed skill counts |
is_completed | boolean | No |
App\Http\Resources\Backend\User\CertificationResource, a backend resource used only by API controllers. Wraps every certification in an extra resource / data envelope in addition to the standard API envelope. Returned singly (as certification) by GET /api/v1/diver/certifications/{certification}/history, and by the /api/v1/professional/certifications endpoints.GET /api/v1/diver/certifications uses App\Http\Resources\Api\V1\DiverCertificationResource instead: the same keys with the same values, plus certification_date, course_group and is_specialty, a previous_certification that resolves to the previous course name, and null-safe distributor and leveling_up_from. See certifications/GET_list.md.{
"resource": "Certification",
"data": {
"log_code": "b737d1ed6e52d5d0daa7794889f9d61c",
"course": "Advanced Deco Trimix Instructor",
"course_level": null,
"distributor": "Example Distributor",
"dive_center": "Example Dive Center",
"instructor": "Example Instructor",
"order_id": null,
"is_associated": false,
"associated_log": null,
"is_renewed": 0,
"will_expire": true,
"expire_date": "2027-10-20",
"is_crossover": false,
"leveling_up": false,
"leveling_up_from": null,
"leveling_id": null,
"is_upgrade": false,
"upgrade_course": null,
"previous_certification": null
}
}resource is a fixed literal, always "Certification".| Field | Type | Nullable | Notes |
|---|---|---|---|
resource | string | No | Always the literal "Certification" |
data.log_code | string(32) | No | |
data.course | string | No | Course name, plain text, not an object |
data.course_level | string | Yes | null for courses with no level system |
data.distributor | string | No | Reads distributor->name directly: a certification without a distributor makes this resource fail. The GET /diver/certifications resource returns null instead |
data.dive_center | string | No | dive_center->name ?? 'N/A' — the literal string N/A is a sentinel, not null |
data.instructor | string | No | instructor->full_name ?? 'N/A' — same N/A sentinel |
data.order_id | integer | Yes | null for certifications not tied to a store order (legacy/migrated records) |
data.is_associated | boolean | No | |
data.associated_log | Yes | Not observed populated in this dataset | |
data.is_renewed | integer | No | 0/1, not cast to boolean — document as returned |
data.will_expire | boolean | No | |
data.expire_date | string | Yes | Y-m-d, present only when will_expire is true in every observed record, but not enforced by the resource — treat as always-present-but-nullable |
data.is_crossover | boolean | No | |
data.leveling_up | boolean | No | |
data.leveling_up_from | string | Yes | Course name of the certification leveling_id points to |
data.leveling_id | integer | Yes | |
data.is_upgrade | boolean | No | |
data.upgrade_course | string | Yes | |
data.previous_certification | string | Yes |
App\Http\Resources\Api\V1\DiveLogResource. Returned by GET /api/v1/diver/dive-logs (paginated) and GET /api/v1/diver/dive-logs/{diveLog}.{
"id": 384110,
"uuid": "6560b3f5-6d71-4f25-a769-6d593c024844",
"number": 400,
"date": "2022-09-05 00:00:00",
"dive_type": "shore",
"purpose_type": "fun_dive",
"location": "Example Site",
"country": null,
"lat": null,
"lon": null,
"dive_config": "open_circuit",
"gas": "air",
"gas_mix": { "oxygen": 21, "nitrogen": 79 },
"max_depth": 20,
"average_depth": 12,
"depth_unit": "meters",
"dive_time": 52,
"abt": null,
"rnt": 0,
"tbt": null,
"tts": 0,
"pressure_start": null,
"pressure_end": null,
"pressure_unit": "bar",
"water_temperature": 13,
"weather_temperature": 13,
"temperature_unit": "celsius",
"weather": "sunny",
"water": "calm",
"current": "no_current",
"visibility": "poor",
"weights": null,
"weights_unit": "kilogram",
"buddy": "Example Buddy",
"verified_by": "EB",
"wetsuit": "Sidemount",
"sac_rate": null,
"safety_stop": false,
"has_decompression": false,
"decompression_note": null,
"mod": null,
"end": null,
"group_in": null,
"group_out": null,
"comments": "Example dive notes.",
"log_code": null,
"course_id": null,
"skill_log_id": null,
"skill_id": null,
"instructor_id": null,
"is_signed": false,
"signed_at": null,
"cylinder_count": 1,
"bottom_cylinder_volume": 15,
"dive_condition_notes": "Good"
}*_type, *_config, gas, weather, water, current, visibility, *_unit fields are enum values (snake_case), not labels — unlike the user resource, which returns labels. Free-text fields (buddy, verified_by, location, comments, dive_condition_notes) are diver-entered and were redacted for this document; they are not sanitised or length-limited server-side beyond the column definition.gas_mix is stored internally as {o, n, h} and translated to {oxygen, nitrogen, helium} by App\Support\DiveLog\GasMix on the way out (and back on the way in), so every client sees one shape whichever way the dive log was created.*_type, *_config, gas, weather, water, current and visibility fields can take, and which a client may send, are listed in dive-logs/POST_create.md → "Enum values".| Field | Type | Nullable |
|---|---|---|
id | integer | No |
uuid | string | No |
number | integer | No |
date | string (Y-m-d H:i:s) | Yes |
dive_type | string (enum value) | Yes |
purpose_type | string (enum value) | Yes |
location | string | Yes |
country | string | Yes |
lat / lon | number | Yes |
dive_config | string (enum value) | Yes |
gas | string (enum value) | Yes |
gas_mix | object ({oxygen, nitrogen, helium}), null when the dive has no recorded mix. Always these three API keys | Yes |
max_depth / average_depth | number | Yes |
depth_unit | string (enum value) | Yes |
dive_time | integer | Yes |
abt / rnt / tbt / tts | number | Yes |
pressure_start / pressure_end | number | Yes |
pressure_unit | string (enum value) | Yes |
water_temperature / weather_temperature | number | Yes |
temperature_unit | string (enum value) | Yes |
weather / water / current / visibility | string (enum value) | Yes |
weights | number | Yes |
weights_unit | string (enum value) | Yes |
buddy / verified_by | string | Yes |
wetsuit | string | Yes |
sac_rate | number | Yes |
safety_stop / has_decompression | boolean | No |
decompression_note | string | Yes |
mod / end | number | Yes |
group_in / group_out | string (one letter A to Z) | Yes |
comments | string | Yes |
log_code | string(32) | Yes |
course_id / skill_log_id / skill_id / instructor_id | integer | Yes |
is_signed | boolean | No |
signed_at | string (Y-m-d H:i:s) | Yes |
cylinder_count | integer | Yes |
bottom_cylinder_volume | number | Yes |
dive_condition_notes | string | Yes |
GET /api/v1/diver/dive-logs only){
"success": true,
"status": "success",
"message": "OK",
"data": {
"data": [ "...dive log objects above..." ],
"meta": {
"current_page": 1,
"last_page": 20,
"per_page": 20,
"total": 400
}
}
}data.data, page metadata under data.meta. There is no links key. per_page is hardcoded to 20 server-side and not yet accepted as a query parameter — see docs/plans/ApiDocsDiver/plan.md's out-of-scope note.App\Http\Resources\Api\V1\QuizResultResource. Returned by POST /api/v1/diver/courses/{log_code}/modules/{module}/quiz (on submission), GET /api/v1/diver/courses/{log_code}/quiz/{quiz}, the free-learnings equivalents, and GET /api/v1/diver/certifications/{certification}/quiz/{quiz}.{
"id": 61222,
"log_code": "6fc53117a24f6bbc76fa8f4bbcaa02dd",
"module_id": 223,
"total": 10,
"correct": 10,
"wrong": 0,
"score": 100,
"passed": true,
"date": "2014-11-11",
"start": "2014-11-11 05:54:36",
"end": "2014-11-11 06:04:19",
"questions": [
{
"id": 319,
"text": "Who discovered Buoyancy?",
"answers": [
{ "id": 1261, "text": "Boyle", "is_correct": false, "selected": false },
{ "id": 1263, "text": "Archimedes", "is_correct": true, "selected": true }
]
}
]
}| Field | Type | Nullable | Notes |
|---|---|---|---|
id | integer | No | |
log_code | string(32) | No | |
module_id | integer | No | |
total | integer | No | Question count used for scoring, not necessarily count(questions) |
correct / wrong | integer | No | |
score | number | No | Percentage, rounded to 2 decimals server-side |
passed | boolean | No | |
date | string | No | Y-m-d |
start / end | string | No | Y-m-d H:i:s, client-supplied at submission time, not server timestamps |
questions | array | No | Every question attempted, with every answer choice and the correct one revealed |
questions[].id / .text | No | ||
questions[].answers[].id / .text | No | ||
questions[].answers[].is_correct | boolean | No | Reveals the correct answer regardless of what the diver selected |
questions[].answers[].selected | boolean | No | What this diver chose |
App\Http\Resources\Api\V1\ExamResultResource. Identical shape to the quiz result minus module_id (an exam is not scoped to a module). Returned by POST /api/v1/diver/courses/{log_code}/exam (on submission), GET /api/v1/diver/courses/{log_code}/exam/{exam}, and GET /api/v1/diver/certifications/{certification}/exam/{exam}.{
"id": 7063,
"log_code": "f7efa55017629461ccd0b8bf303bfb91",
"total": 45,
"correct": 45,
"wrong": 0,
"score": 100,
"passed": true,
"date": "2014-11-28",
"start": "2014-11-28 03:56:06",
"end": "2014-11-28 04:09:25",
"questions": [
{
"id": 1418,
"text": "Can you hold your breath at any time while diving or hovering?",
"answers": [
{ "id": 5641, "text": "Yes", "is_correct": false, "selected": false },
{ "id": 5642, "text": "No, never hold your breath while diving", "is_correct": true, "selected": true }
]
}
]
}module_id.App\Http\Resources\Api\V1\SkillProgressResource. Intended for GET /api/v1/diver/courses/{log_code}/skills, GET /api/v1/diver/certifications/{certification}/skills, GET /api/v1/diver/certifications/{certification}/history, and the response of POST /api/v1/diver/courses/{log_code}/skills/sign.findings.md: SkillLog::with('skillType.skills') references a relation, skills, that does not exist on SkillType (the real relation is skill, singular). The resource itself also reads $this->skillType?->skills, so even a request that avoided the eager-load bug would fail inside the resource. The shape below is reconstructed from SkillProgressResource source only; no example is a captured live response — do not treat the example values as observed.{
"id": 7757,
"log_code": "f7efa55017629461ccd0b8bf303bfb91",
"skill_type_id": 14,
"skill_type_name": "Example skill group",
"user_completed": null,
"start": "2023-01-15 10:00:00",
"end": null,
"skills": [
{
"id": 88,
"name": "Example skill",
"description": "Example skill description.",
"diver_signed": false,
"instructor_signed": false
}
]
}| Field | Type | Nullable | Notes |
|---|---|---|---|
id | integer | No | TO VERIFY: not captured live |
log_code | string(32) | No | TO VERIFY: not captured live |
skill_type_id | integer | No | TO VERIFY: not captured live |
skill_type_name | string | Yes | null if the skill type relation fails to load |
user_completed | Yes | Type and semantics TO VERIFY — not clarified by the resource source alone | |
start / end | string | Yes | Y-m-d H:i:s, TO VERIFY: not captured live |
skills | array | No | Empty when the skill type has no skills defined, or when skillType failed to load (currently always, given finding 2) |
skills[].id / .name / .description | No | TO VERIFY: not captured live | |
skills[].diver_signed | boolean | No | From the log's user_skills JSON map, keyed by skill id |
skills[].instructor_signed | boolean | No | From the log's instructor_skills JSON map |
docs/api/diver/_resources.md