RAID API
    • DiveRAID frontend public API
      • Search divers
        POST
      • Search instructors
        POST
      • Search dive centers
        POST
      • Search dive centers by country
        GET
    • DiveRAID rest API V1
      • Auth
        • Register a new user
        • Login and get Sanctum token
        • Send password reset email
        • Reset password using token
        • Logout and invalidate JWT token
        • Verify email address (signed URL)
        • Resend email verification
      • Profile
        • Get authenticated user profile
        • Update profile
        • Delete account
        • Update password
        • Update dive center association
      • Diver
        • Courses
          • List active courses
          • List expired courses
          • Get course detail
          • Submit module quiz
          • Get quiz result
          • Submit course exam
          • Get exam result
          • Get skills progress
          • Sign skills (diver)
        • Free Learnings
          • List enrolled free learnings
          • List available free learning courses
          • Enroll in a free learning course
          • Get free learning detail
          • Submit free learning module quiz
          • Get free learning quiz result
        • Certifications
          • List diver certifications
          • Get certification history
          • Get certification quiz result
          • Get certification exam result
          • Get certification skills
        • Dive Logs
          • List dive logs (paginated)
          • Create dive log
          • Get dive log
          • Update dive log
        • Awards
          • List award cards
        • Documents
          • List diver documents
        • Forms
          • List diver forms
        • Medical
          • Get medical questionnaire structure
          • Submit medical questionnaire
        • Store
          • List courses available for purchase
          • Get order status
      • Professional
        • Students
          • List students
          • Get student progress
          • Get student quiz result
          • Get student exam result
          • Get student skills progress
          • Sign student skills
        • Certifications
          • List all professional certifications
          • List diver-level certifications
          • List specialty certifications
          • List professional certifications
          • List trainer certifications
          • List examiner certifications
          • Get certification history
          • Get certification quiz result
          • Get certification exam result
          • Get certification skills
        • Classroom
          • List classrooms
          • Get classroom progress (all students)
          • Get classroom student progress
          • Get classroom student quiz result
          • Get classroom student exam result
          • Get classroom student skills
          • Sign classroom student skills
        • Renewals
          • List renewal courses
          • Get renewal progress
          • Submit renewal module quiz
          • Get renewal quiz result
          • Submit renewal exam
          • Get renewal exam result
          • Get renewal skills progress
          • Sign renewal skills
        • Recognitions
          • List recognition courses
        • Dive Logs
          • List student dive logs for a course
          • Get student dive log
          • Sign student dive log
        • Store
      • Sync
        • Get sync status
        • Download full course data for offline use
        • Upload offline operations
      • Utility
        • List all countries
        • Get country data
        • Get country divisions (states/provinces)
        • List active dive centers
        • Get dive center details
      • Dive Center
    • Schemas
      • Frontend API Schema
        • DiverSearch
      • SuccessResponse
      • ErrorResponse
      • ValidationErrorResponse
      • TokenResponse
      • UserProfile
      • CourseLog
      • CourseLogDetail
      • QuizResult
      • ExamResult
      • SkillProgress
      • DiveLog
      • Certification
      • PaginatedDiveLogs
      • StoreItem
      • PaymentIntentResponse
      • OrderConfirmResponse
      • OrderStatusResponse
      • InstructorSearch
      • DiveCenterSearch

    DiveRAID rest API V1

    DiveRAID REST API V1#

    Everything under /api/v1/*: the interface the RAID mobile client and any authenticated integration use. 86 operations across six areas.
    This page describes what is true of every endpoint. The per-endpoint files describe what is specific to each. Where the two disagree, the per-endpoint file wins, because it was written against a captured live response.

    1. Base URL and versioning#

    EnvironmentBase URL
    Productionhttps://user.diveraid.com
    Staginghttps://test.diveraid.com
    Every path in this documentation is relative to that host and already includes /api/v1.
    v1 is the only version. It is expected to change in place rather than be replaced: breaking changes are announced, not versioned away. Anything described here as decided-but-not-implemented will arrive inside v1.

    2. Authentication#

    Sanctum personal access tokens. Send the token on every authenticated call:
    Authorization: Bearer 20|raid_ydVCW8OH9aAkZIKDuWAIOmNA6BFdfZrbAPZtpzFjebcf
    The token is an opaque string of the form {id}|raid_{random}. It carries no readable payload: do not attempt to decode a user id or an expiry out of it. It is stored server-side, which is what makes individual revocation possible.

    Obtaining a token#

    POST /api/v1/auth/login with email and password. The response carries token, token_type and expires_in.
    Send device_name as well. It is optional, but it is the label the account holder sees on their privacy screen when deciding which device to sign out. Without it the server derives something generic from the User-Agent, which on a mobile client is rarely useful.

    Lifetime#

    90 days, non-rotating. There is no refresh endpoint and no refresh token: when a token expires the user logs in again. expires_in is returned at login, around 7776000 seconds; treat it as approximate, it is computed at response time and truncates to the second.

    Abilities#

    Tokens are issued with the * ability. Abilities are in place so that scoped tokens can be introduced without a schema change; no endpoint restricts on them today.

    Authorization is separate from authentication#

    A valid token proves who you are. What you may do is decided by the user's role. Endpoints under /api/v1/professional/* require the professional role and return 403 otherwise, regardless of the token.

    Multiple devices#

    Every login issues an independent token. Signing in on a second device does not disturb the first. The account holder can list and revoke individual devices from the web application.

    Token lifecycle#

    EventEffect
    LoginIssues a new token, leaves others alone
    LogoutRevokes only the token used for the call
    Password change, authenticatedRevokes all others, keeps yours valid
    Password reset, forgot-password flowRevokes all tokens, including yours
    Account suspended, banned or deletedRevokes all tokens
    Revoke from the privacy screenRevokes that one device
    Note the asymmetry between the last two password rows: a password change is performed by someone who proved knowledge of the current password, so their own session survives. A reset is recovery from lost or compromised credentials, where the caller has proved nothing, so nothing is spared.

    Public endpoints#

    Four endpoints need no token: POST /api/v1/auth/register, /login, /password/forgot, /password/reset. All four are rate limited.

    3. Response envelope#

    Every successful response carries the same four keys.
    {
      "success": true,
      "status": "success",
      "message": "OK",
      "data": { }
    }
    KeyNotes
    successBoolean. Always present
    status"success" or "error". Carries the same information as success; both are kept for compatibility
    messageHuman-readable. "OK" on a plain read, descriptive after a write
    dataThe payload. Omitted entirely when there is nothing to return, for example after a logout or a delete. Do not expect data: null
    201 Created uses the same shape.

    4. Errors#

    Status codes#

    CodeMeaning
    401No token, malformed token, or expired token
    403Authenticated, but the role or ownership check refused
    404Resource not found
    422Validation failure, or a business rule refusing an otherwise valid request
    429Rate limit exceeded
    500Server error. Several endpoints currently return this unconditionally; see section 12

    The standard error shape#

    {
      "success": false,
      "status": "error",
      "message": "Token expired",
      "errors": { }
    }
    errors is present only on validation failures.

    401 in detail#

    All three cases share the shape above; the case is distinguished by message alone:
    messageMeaning
    UnauthenticatedNo Authorization header
    Invalid tokenMalformed, unknown or already revoked
    Token expiredPast its 90 days
    The underlying reason is logged server-side and deliberately not returned.

    422 has two meanings#

    Validation failure carries errors, an object keyed by field name with an array of messages:
    {
      "success": false,
      "status": "error",
      "message": "Validation failed",
      "errors": { "email": ["The email field must be a valid email address."] }
    }
    Business rule refusal carries no errors. The request was well-formed; the domain refused it. For example, a professional cannot change dive centre while a renewal is open:
    {
      "success": false,
      "status": "error",
      "message": "Dive center cannot be changed while a professional renewal is active"
    }
    Distinguish the two by the presence of errors, not by the status code.

    Known deviation: eight controllers return Laravel's default validation shape#

    Most endpoints validate through a Form Request and produce the standard envelope. Eight controllers still validate inline and produce Laravel's bare default instead, with no success and no status:
    {
      "message": "The country field must be 2 characters.",
      "errors": { "country": ["The country field must be 2 characters."] }
    }
    The controllers concerned are Diver\MedicalController, Diver\FreeLearningController, Professional\ClassroomController, Professional\RenewalController, Professional\StoreController, Professional\StudentController, Sync\SyncController, Utility\UtilityController. Their endpoint files say so individually.
    Alignment is planned. Until it lands, parse a 422 by looking for errors, never by looking for success.

    Known deviation: ownership refused by a Form Request#

    PATCH /api/v1/diver/dive-logs/{diveLog} enforces ownership in the Form Request's authorize() rather than in the controller, so its refusal is Laravel's default:
    { "message": "This action is unauthorized." }
    Every other ownership check in the API refuses inside the controller and produces the standard envelope.

    404 message shape#

    Two kinds, worth knowing when matching on the message:
    Endpoints whose path parameter is a numeric key resolved by route-model binding ({diveLog}, {quiz}, {exam}, {module}, {certification}) include the looked-up id in the message.
    Endpoints resolving an opaque {log_code} manually do not.

    5. Rate limiting#

    ScopeLimit
    register, login, password/forgot, password/reset10 requests per minute per IP, counting successes and failures alike
    auth/email/resend6 per minute per authenticated user
    All other authenticated endpointsThe default api limiter
    Exceeding a limit returns 429.

    6. Pagination#

    Only the dive log listings paginate today: GET /api/v1/diver/dive-logs and GET /api/v1/professional/dive-logs/{user}/{log_code}.
    {
      "success": true, "status": "success", "message": "OK",
      "data": {
        "data": [],
        "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 0 }
      }
    }
    The collection sits at data.data and the pagination at data.meta. There is no links key. per_page is fixed at 20 and is not yet accepted as a parameter.
    Every other list endpoint returns its full collection unpaginated. Two are large enough to matter: GET /api/v1/utility/dive-centers returns every active dive centre in one response, currently 836, and GET /api/v1/diver/certifications returns every certification a diver holds, with no cap. Fetch them once and filter client-side rather than calling them per keystroke.
    Adding per_page, with a default of 20 and a maximum of 100, plus a search filter on the dive centre list, is decided and not yet implemented.

    7. Identifiers#

    Three kinds, and they are not interchangeable.
    PlaceholderKind
    {log_code}Opaque char(32) string identifying a course log or free learning. Not a primary key. Resolved manually by the controller, which performs its own ownership check
    {certification}, {diveLog}, {quiz}, {exam}, {module}, {order}, {classroom}Numeric primary key, resolved by route-model binding
    {user}Numeric primary key, on professional endpoints acting on a student
    One inconsistency worth knowing: the dive centre identifier is called id in POST /api/v1/utility/dive-center and dive_center_id in PATCH /api/v1/profile/dive-center. Same value, two names.

    8. Dates and time#

    Two formats coexist and must be parsed differently:
    Domain dates are Y-m-d strings: dob, expire_date, renewal_date, gdpr, terms, email_verified_at.
    Record timestamps are full ISO-8601 with microseconds and a UTC offset: created_at, updated_at, for example 2026-09-10T09:21:23.000000Z.
    The user's timezone field is a display preference. The API does not localise timestamps to it; conversion is the client's responsibility.

    9. Money#

    Money is carried in major units as a JSON number with at most two decimals. A course priced at 135 dollars is 135, not 13500.
    This is worth stating because the database stores minor units as integers and the model accessor converts. Anything reading the database directly sees different numbers from anything reading the API.
    currency is USD on store endpoints and is not negotiable: the diver and professional stores price in US dollars only. The currency field on the user profile is derived from their country and is a display preference, not the currency they will be charged in.

    10. Enum fields and localisation#

    Enum-backed fields return the English display label, not a machine value: qualification_cast returns Recreational, status returns Account Active, preferred_system returns Metric.
    Three consequences:
    1.
    Do not branch on these strings. They are display text and can change without being a breaking change in anyone's intent.
    2.
    They are English regardless of Accept-Language. No endpoint reads that header.
    3.
    Writing is asymmetric with reading. PATCH /api/v1/profile accepts gender as the numeric enum value while the response returns the label. What you send is not what you get back.
    Returning both parts, {"value": "active", "label": "Active"}, is decided and not yet implemented. GET /api/v1/utility/countries already returns that shape and is the model for it.
    One more sentinel to watch for: when no dive centre or distributor is set, dive_center and distributor contain the literal English string Not assigned rather than null.

    11. Conditional fields#

    Some resources add and remove keys depending on the record. The user resource is the clearest case: a recreational diver receives 50 keys; a suspended professional receives up to 15 more, including the professional block and the account status block.
    Check for a key's presence rather than assuming it. The per-endpoint field tables mark which keys are conditional and on what.
    Emitting every key always, with null where not applicable, is decided and not yet implemented.

    12. Endpoints that do not work today#

    Documented honestly rather than omitted, because a client needs to know what not to call. Each has its own file describing the failure.
    EndpointFailure
    GET /api/v1/diver/courses500, unconditionally
    GET /api/v1/diver/courses/expired500, unconditionally
    GET /api/v1/diver/courses/{log_code}500, unconditionally
    GET /api/v1/diver/free-learnings/{log_code}500, unconditionally
    GET /api/v1/diver/store500, unconditionally
    GET /api/v1/diver/courses/{log_code}/skills500 once the log has skill records
    GET /api/v1/diver/certifications/{certification}/history500 once the certification has skill records
    GET /api/v1/diver/certifications/{certification}/history/skills500 once the certification has skill records
    POST /api/v1/diver/courses/{log_code}/skills/sign is a special case: the write succeeds and the response then fails with a 500. A client that retries on 500 will re-submit signatures that already landed. The update is idempotent per skill, so no data is corrupted, but the write and the response are not atomic from the client's point of view.
    All of these are being fixed. The endpoint files will be updated with captured successful responses once they are.

    13. Areas#

    AreaOperationsPath
    auth7/api/v1/auth/*
    profile5/api/v1/profile*
    utility5/api/v1/utility/*
    diver31/api/v1/diver/*
    professional35/api/v1/professional/*
    sync3/api/v1/sync/*
    professional endpoints require the professional role in addition to a valid token.

    14. What this API does not cover#

    Four search endpoints exist outside this documentation and are not part of V1 in any meaningful sense, despite sharing the /api/v1 prefix:
    POST /api/v1/diver
    POST /api/v1/instructor
    POST /api/v1/dive_center
    GET  /api/v1/dive_center/country
    They authenticate with a shared static key in a token header rather than a bearer token, serve the public website, and follow none of the conventions on this page: a different envelope, a different error shape, no pagination. They are documented separately under docs/api/search/ and are scheduled to move to their own prefix.
    If you are building against V1, ignore them.

    15. How this documentation is written#

    Every response shown in these files was captured from a running instance, not transcribed from the source code. That distinction is not pedantry: three earlier descriptions of this API were written by reading controllers, and all three were wrong in the same ways, including a nested data.data wrapper on the profile endpoint that has never existed.
    Where something could not be captured, because the endpoint fails or the data does not exist in any account, the file says so explicitly rather than presenting a reconstruction as observed fact.
    A file is only correct as of the commit that last touched it. When a schema and a live response disagree, the live response is right, and the file is a bug worth reporting.
    Modified at 2026-09-18 08:51:41
    Previous
    Search dive centers by country
    Next
    Register a new user
    Built with