Overview › API inventory

API Inventory — PADI Mobile, as built

Companion to Logged In Experience. This is the API-documentation half of the ask: what the existing mobile apps actually call today, grouped by feature area, with source citations.

How to read this document

Sections 1–9 are step 1 of the test: what the current mobile apps call. They are evidence that an endpoint works from a mobile client — nothing more. The apps themselves are being replaced by the unified Expo React Native app.

Wherever sections 1–9 say an endpoint is "absent", that means absent from the mobile apps. It does not mean PADI has no such endpoint. Several of those gaps are closed by a web front end — profile read, membership, preferences and recommendations among them.

§10 is step 2, and it supersedes any "absent" in the earlier sections. Read them together or you will reach the wrong conclusion.

Scope. Primarily the PADI app (kms-ios-padi, kms-android-padi), which is what the Logged In Experience card concerns. AWARE and Adventures are covered in §7 for contrast, because they are separate products on separate backends with separate identity.

Provenance. Every mobile endpoint below is grounded in the Kotlin or Swift source. The three OpenAPI files in kms-android-padi (padi-app-openapi.yaml, padi-kms-openapi.yaml, docs/openapi-android.yaml) are untracked and self-described as reverse-engineered from client code — useful as a cross-check, not cited here as contract.

The same caution applies to the web side. Rows marked "exists web-side" cite MyPADI — Unified Diver Profile.docx, which is Axelerant-authored discovery, not a PADI contract — it is titled "API Reference & Dashboard Gap Analysis" and states that "endpoints were captured from the browser Network tab and cross-checked against the PadiWW source." No PADI-published API documentation for MyPADI exists in the corpus; it was requested from Ralph on 2026-04-24 and asked for again on 2026-07-13. Nothing in this inventory is a contract PADI has committed to. Everything here is observed behaviour — reliable as a description of what runs today, unreliable as a guarantee of what will be there in Q1 2027.

⚠️ Some of these endpoints are being replaced, and nobody has told us which.

Ralph Lai (VP Product Engineering, PADI) and Kanikar Phan, in WS 26 — Minutes of Meeting, PADI Technical Workshop #3 (2026-03-31), state that:

  • "Existing Postman documentation currently reflects only a subset"
  • "APIs are undergoing an active transition"
  • "Several endpoints are being rebuilt due to the legacy system sunset"

This inventory is a snapshot of what the apps call today, not a forecast of what will exist in Q1 2027. An "exists" row means the endpoint is live and consumed right now. It does not mean the endpoint survives the legacy sunset, keeps its path, or keeps its response shape.

No list of which endpoints are being rebuilt has been shared. Until PADI provides one, every estimate built on this inventory carries unquantified rework risk — and the risk is largest exactly where mobile depends most: the auth facade, the eCard family, and the learning endpoints all sit on the legacy surface being transitioned.

This is the single highest-value thing to ask PADI for, ahead of any new API: which of these 34 endpoints survive, and on what timeline.


1. Where the contracts live

iOS (kms-ios-padi) Android (kms-android-padi)
Endpoint definitions PADI/data/api/api/*.swift (27 protocols), paths in PADI/data/api/impl/** app/src/main/java/com/duns/padiapp/data/api/*.kt (21 Retrofit interfaces) — authoritative
DI / index PADI/component/Components.swift, Components+APIs.swift di/AppModule.kt:193-429, di/PADITrainingAppModule.kt:53-78
HTTP client PADI/data/api/common/ApiClient.swift, impl/ApiClient/ApiClientImpl.swift (Alamofire) Retrofit + OkHttp, wired in AppModule.kt
Auth header PADI/data/api/common/HeaderBuilder.swift:65-78 data/api/interceptor/AuthenticatorInterceptorRefreshToken.kt:36-42
Refresh / retry PADI/data/api/common/RequestAdapter.swift:36-118 AuthenticatorInterceptorRefreshToken.kt:47-99
Host registry PADI/component/Configuration.swift (19-case EndPoint enum) domain/{Live,Staging}AppSettingIpm.kt:16-53

Android's Retrofit interfaces are the single best place to read the whole surface — 21 files, ~516 lines. iOS has no equivalent single view.


2. Hosts and environments

From LiveAppSettingIpm.kt / StagingAppSettingIpm.kt:16-53 and Configuration.swift:97-121.

Service Production Staging
Auth (Cognito facade) api.global-prod.padi.com/auth/api/ api-stage.global-np.padi.com/auth/api/
eCard ecard.global-prod.padi.com/api/ ecard-stage.global-np.padi.com/api/
eCard v2 api.global-prod.padi.com/c/cards-new/ api.global-np.padi.com/c/cards-new/
Cert photo api.padi.com/cp/certification-photo/ api.global-np.padi.com/cp/certification-photo/
Learning / LMS learning-prod.padi.com/learning/ learning-np.padi-staging.com/learning/
API gateway api.padi.com/ api.global-np.padi.com/
Logbook logbook.global-prod.padi.com/api/ logbook-stage.global-np.padi.com/api/
Pro api.pro-prod.padi.com/ api.pro-np.padi.com/
Checkpoint checkpoint-api.padi.com/api/ checkpoint-stage.global-np.padi.com/api/
Languages languages.global-prod.padi.com/api/ languages-stage.global-np.padi.com/api/
Analytics analytics-prod.padi.com/api/v1/ analytics-np.padi.com/api/v1/
Commerce api.padi.com/c/commerce/api/ api.global-np.padi.com/c/commerce/api/
eCommerce api-ecomm.ecomm-prod.padi.com/api/v3/ api-stage-ecomm.ecomm-np.padi.com/api/v3/
Identity (direct) cognito-idp.us-west-2.amazonaws.com, pool us-west-2_hGwJiwtcI same pool, different client id
Cloudinary api.cloudinary.com/v1_1/padi, res.cloudinary.com/padi same
Geolocation geolocation.padi-prod.padi.com same

Environment defects found while compiling this — all worth a ticket independent of the feasibility work:

  • LiveAppSettingIpm.kt:33 — messaging uses the staging URL in production
  • BaseAppSettingsIpm.kt:152getCognito2Url() returns the staging auth host in both flavours
  • ProChekApiImpl.swift:14 — production URL hardcoded, ignores environment
  • procheckUrl is the same production host in both Android flavours

3. Authentication and session

Base: .../auth/api/. A PADI-owned REST facade in front of AWS Cognito.

Method Path iOS Android
POST login AuthenticationApiImpl.swift:20,37 Swapi.kt:31-33
POST token (refresh) RefreshTokenApiImpl.swift:14,29 Swapi.kt:86-88
POST oauth/register RegisterApiImpl.swift:14,54,101 Swapi.kt:69-71
POST oauth/confirm RegisterConfirmationAPIImpl.swift:13,41 Swapi.kt:75-77
POST oauth/reset RegisterConfirmationAPIImpl.swift:14,60 Swapi.kt:58-59
POST password/reset/initiate SendConfirmationCodeApiImpl.swift:14,26 Swapi.kt:53-54
POST user/exist/legacy GetUserStatusApiImpl.swift:13,20 Swapi.kt:37-39
POST sso/exchange JumpPageSSO.swift:128 Swapi.kt:81-82
POST oauth2/token (client credentials) EmailTransactServiceImpl.swift:35-44 Cognito2Api.kt:9-12
POST Cognito GetUser UserAttribute/UserAttributeApi.swift:21-27 absent

Token model. Three tokens (idToken, accessToken, refreshToken). The idToken is sent as the bearer on both platforms. Endpoints opt out of auth with @Headers("isAuthorized: false") on Android (Swapi.kt:32,38,70,75,87) or the nonAuthKeyAPIs allow-list on iOS (HeaderBuilder.swift:57-63).

Refresh. Reactive, on 401/403, single retry, then forced logout. Android synchronises the refresh; iOS queues concurrent failures in requestsToRetry and replays them after one refresh.

Confirmed absent: server-side logout/revoke · change-password while authenticated (only the forgot-password reset flow) · biometrics · OAuth hosted-UI / PKCE / social login into the app.

user/exist/legacy returns a triage of m2User / cognitoUser / travelUser — evidence that cross-property identity was anticipated.


4. Profile

Read: no endpoint in the mobile apps. (§10: GET /p/profile/web/me is live-tested and returns the full profile.) In the current apps, profile is decoded from the Cognito ID token — see IDToken.swift:11-50 for the full claim set, including contact_id (Salesforce Contact Id), global_uuid, custom:affiliate_id, custom:affiliate_type_id, custom:language.

Write:

Method Path Base Source
PUT user/update/{username} — accepts firstName, middleName, lastName, birthDate; used only for guardian email auth Swapi.kt:63-64; SendGuardianPermissionMailApiImpl.swift:13,25
PUT user/update/language/{email} auth Swapi.kt:43-44; UpdateSelectedLanguageApiImpl.swift:21,24
POST profiles/{affiliateId}/MailingAddress learning portal LearningApi.kt:22-23; AddressApiImpl.swift:14-16,40

Address fields: addressLine1, addressLine2, city, countryId, postalCode, stateId.

Reference data:

Method Path Source
GET globals/Countries LearningApi.kt:14-15
GET globals/Countries/{countryId}/States LearningApi.kt:17-18
GET globals/SupportedLanguages LearningApi.kt:28-29
GET api/v1/analytics/{globalUUID} — segmentation: gender, is_pro, is_freediver, is_tec, preferred_language, diver_core_level, home_country, age, user_type, level AnalyticsPadiApi.kt:8-11; AnalyticsAPIImpl.swift:14,19

The analytics endpoint is the only source of home_country and diver_core_level.

Photos:

Method Path Source
POST certifications/photos/upload (multipart) ProApi.kt:14-16; UploadPhotoApiImpl.swift:14,21-22
GET cp/certification-photo/api/CertificationPhoto/GetCertificationPhoto CertificationPhotoApi.kt:7-8
GET p/photos/web/certification (iOS only — exposes moderation state) GetCertificationPhotoApilmpl.swift:24,34

No delete endpoint. Android's upload returns only {success: Boolean}.


5. Certifications and eCards

Base ecard.global-prod.padi.com/api/, path constants at ECardsApi.kt:9-13.

Method Path Android iOS
GET eCard/GetCardListConsumer ECardsApi.kt:10,21-22 GeteCardsApiV2Impl.swift:61,103
GET eCard/GeteCardListConsumer ECardsApi.kt:9,18-19 GeteCardsApiV2Impl.swift:63,93
GET eCard/HaseCardConsumer ECardsApi.kt:11,24-25 GeteCardsApiV2Impl.swift:68
GET eCard/HaseCardPro ECardsApi.kt:12,28-29 GeteCardsApiV2Impl.swift:68
GET eCard/GetAlleCardListPro ECardsApi.kt:13,31-32 GeteCardsApiV2Impl.swift:70,224
GET eCard/GetAllCardListPro ECardsApi.kt:34-35 GeteCardsApiV2Impl.swift:61
GET eCard/GeteCardTopConsumer/{n} (wallet) absent GeteCardsApiV2Impl.swift:64-66,98
GET c/cards-new/web/licenses (v2, flagged off in RELEASE) absent GeteCardsApiV2Impl.swift:19-32,56-57
POST Cloudinary padi/resources/search (artwork) CloudinaryApi.kt:10-18 ECardImageServiceImpl.swift:55-60

ECardInfo (domain/models/ECardInfo.kt:14-68) — ~55 fields including cardTitle, padiNumber, certDate, inWaterDate, instructorName/Number, store details, memberSince, lastRenewed, cardExpirationDate, qrCode, canPrint. AffiliateSummary is only four: affiliateID, fullName, birthdate, photo.

No certification-detail endpoint — detail renders from the list. No progression/path endpoint in the mobile apps (§10: {PRO_API}/journey/routing/othercourses is web-captured; §11 adds GET /c/credentials/{id}).


Learning / LMS (base learning-prod.padi.com/learning/):

Method Path Source
GET v2/entitlements/{affiliateID}{title, packageId, platform} only LearningV2Api.kt:11-12; GetEntitlementsResponse.kt
GET v2/courses/{courseID}/details LearningV2Api.kt:16-17
GET v2/courses/{courseID} (assets) LearningV2Api.kt:22-23
GET offline/learning (Basic auth, iOS only) OfflineLearningApiImpl.swift:21-27
POST offline/learning/{registrationId} (SCORM score, iOS only) SCORMApiImpl.swift:14,25
POST learning/lms/commit (SCORM runtime commit) PADIApi.kt:10-13

No per-course completion percentage exists — and unlike the others, this one survives §10 too. Progress lives inside the SCORM payload and no web front end reads it back.

Preferences and consent:

Method Path Source
GET p/personalization/api/Personalization/attributes/{key} PersonalizationApi.kt:11-12
POST p/personalization/api/Personalization/attributes PersonalizationApi.kt:16-17
GET p/personalization/api/Personalization/attributes (iOS only) PersonalizationServiceImpl.swift:46,58
GET api/Languages/GetLanguage/{platform}/{lang} LanguagesApi.kt:8-12

The Personalization service is a schemaless key/value store. Its only key in use is china_data_consent. Email/marketing preferences have no API — SSO webview to learning.padi.com/preferences/subscriber (BaseAppSettingsIpm.kt:252-255; Constant.swift:13).

Other:

Method Path Source
POST logbook (GraphQL, Hasura-shaped) LogbookApi.kt:12-18; LogBookService.swift:28
POST pros/graphql (ProChek) ProApi.kt:20-21
POST prochek/prochek/GetProChekByMemberNumber/ ProCheckApi.kt:9-12
GET/POST/PUT/DELETE api/CheckPoint/* (Discover Scuba Diving) CheckpointApi.kt:11-22; LearnAPIImpl.swift:21-32
GET c/commerce/api/Product/Search NewCourseApi.kt:11; CourseCatalogueServiceImpl.swift:186
POST v1/email/transact (account-deletion request) MessagingApi.kt:9-12; EmailTransactServiceImpl.swift:73
GET geolocation root (China PIPL gating) UtilsApi.kt:31-34; GeolocationServiceImpl.swift:26

No membership, Club or billing API is called by the mobile apps. (§10 and §11 close most of this.) (§10: GET /c/club/member/subscription is live-tested, and Piccolo serves entitlements.) In the current apps Club is a web link: Constant.swift:135,201, build.gradle.kts:100-101.


7. The other two apps, for contrast

Both are separate products on separate backends. Neither is a source of reusable contract for this card.

PADI AWARE (padi.aware.flutter) — conservation surveys. Clean Retrofit codegen at packages/data/lib/src/http/. Notable: GET /users/me and PUT /users/profile (services/user_service.dart:5-30) are the cleanest profile contract in the estate — but they serve the AWARE backend, not PADI's. Uses hosted PADI SSO (mobile_sso_auth.dart:8-16) plus an SLO session service at slo.global-prod.padi.com/graphqlthe one piece of cross-PADI identity plumbing that exists. Zero certification, learning, or membership surface.

PADI Adventures (travel-rn) — booking and commerce on travel.padi.com/api/mobile/v1 (Django). Own login (account/login), Authorization: JWT not Bearer, no refresh token, session in unencrypted AsyncStorage. Has the most granular contact-preference model anywhere (account/preferences/notification/ — per-channel flags across five categories) and a real DELETE account/. Certification data is self-declared free text, not from PADI's cert system.

Identity comparison — the substance of the MYP-08 gap:

PADI app (KMS) AWARE Adventures
IdP Cognito via PADI facade Hosted PADI SSO Own backend
Bearer idToken AWARE-issued JWT Authorization: JWT
Refresh yes yes none
Storage UserDefaults / SharedPreferences (plaintext) secure storage AsyncStorage (plaintext)
Server logout none DELETE /logout + SLO none

8. Gaps in the documentation itself

  1. No server-authored API contract exists anywhere. Every spec in the estate is client-derived.
  2. No Postman/Insomnia collection, no .http files, no HAR captures in any of the four repos.
  3. No GraphQL schema for any of the three GraphQL endpoints consumed (Logbook, Pros, SLO). Only Logbook has operation documents (kms-ios-padi/PADI/apollo.graphql).
  4. No ADRs in any repo.
  5. kms-ios-padi and kms-android-padi READMEs contain no backend integration content — Android's is ten lines of git commands.
  6. No mock server anywhere. The only response-shape fixtures are four plists under kms-ios-padi/PADITests/.

Items 1–3 are why this inventory was compiled from source rather than from documentation, and why every claim here carries a file:line.


9. Endpoint → item map (Logged In Experience)

Every endpoint the card's 15 items depend on, as called by the current mobile apps.

⚠️ in the Path column means no endpoint in the mobile apps — not "no endpoint at PADI". Check §10 before treating any of these as a gap. Only two survive as real gaps: per-course completion percentage, and the consent platform.

Identity & session → MYP-08

Method Path Source Status
POST login Swapi.kt:31-33 exists
POST token (refresh) Swapi.kt:86-88 exists
POST oauth/register Swapi.kt:69-71 exists
POST oauth/confirm Swapi.kt:75-77 exists
POST oauth/reset Swapi.kt:58-59 exists
POST password/reset/initiate Swapi.kt:53-54 exists
POST user/exist/legacy Swapi.kt:37-39 exists — returns the m2User/cognitoUser/travelUser triage
POST sso/exchange Swapi.kt:81-82 exists — the primitive unified login needs
server-side logout / revoke SessionRepositoryImpl.swift:152-168 absent — sign-out is local only
change password while authenticated absent — only the forgot-password flow

Base: api.global-prod.padi.com/auth/api/. Bearer is the idToken, not the access token.

Profile & preferences → MYP-02, PREF-01

Method Path Source Status
profile read IDToken.swift:11-50 absent in mobile — §10 closes it: GET /p/profile/web/me, live-tested
PUT user/update/{username} Swapi.kt:63-64 exists — accepts name/DOB, used only for guardian email
PUT user/update/language/{email} Swapi.kt:43-44 exists — backs PREF-01
POST profiles/{affiliateId}/MailingAddress LearningApi.kt:22-23 exists
GET globals/Countries LearningApi.kt:14-15 exists
GET globals/Countries/{countryId}/States LearningApi.kt:17-18 exists
GET globals/SupportedLanguages LearningApi.kt:28-29 exists — backs PREF-01
GET api/v1/analytics/{globalUUID} AnalyticsPadiApi.kt:8-11 exists — only source of home_country, diver_core_level
field-level permissions absent — no such system in any repo

Photos → MYP-06

Method Path Source Status
POST certifications/photos/upload ProApi.kt:14-16 exists — moderated cert photo, not an avatar
GET cp/certification-photo/.../GetCertificationPhoto CertificationPhotoApi.kt:7-8 exists
GET p/photos/web/certification GetCertificationPhotoApilmpl.swift:24,34 exists — iOS only; the only one exposing moderation state
delete photo absent

Certifications & eCards → MYP-03, MYP-10

Method Path Source Status
GET eCard/GetCardListConsumer ECardsApi.kt:10,21-22 exists — certifications
GET eCard/GeteCardListConsumer ECardsApi.kt:9,18-19 exists — purchased eCards
GET eCard/HaseCardConsumer ECardsApi.kt:11,24-25 exists
GET eCard/HaseCardPro ECardsApi.kt:12,28-29 exists
GET eCard/GetAlleCardListPro ECardsApi.kt:13,31-32 exists
GET eCard/GetAllCardListPro ECardsApi.kt:34-35 exists
GET eCard/GeteCardTopConsumer/{n} GeteCardsApiV2Impl.swift:64-66 exists — iOS wallet view
GET c/cards-new/web/licenses GeteCardsApiV2Impl.swift:19-32 exists — v2, feature-flagged off in RELEASE
POST padi/resources/search (Cloudinary) CloudinaryApi.kt:10-18 exists — card artwork
certification detail absent — renders from the list payload
certification path / progression FilterNextCourseType.kt absent in mobile — §10 closes it: {PRO_API}/journey/routing/othercourses

ECardInfo carries ~55 fields (ECardInfo.kt:14-68) — more than MYP-03 requires.

Learning → MYP-04, MYP-12

Method Path Source Status
GET v2/entitlements/{affiliateID} LearningV2Api.kt:11-12 exists — only {title, packageId, platform}
GET v2/courses/{courseID}/details LearningV2Api.kt:16-17 exists
GET v2/courses/{courseID} LearningV2Api.kt:22-23 exists — assets
POST learning/lms/commit PADIApi.kt:10-13 exists — SCORM commit; where progress actually lives
per-course completion % / last-accessed absent, and it survives §10 — the MYP-04 blocker. One unread response body: see §12
recommendation service absent in mobile — §10 closes it: GET /v2/courses?tag=recommended
Method Path Source Status
GET p/personalization/api/Personalization/attributes/{key} PersonalizationApi.kt:11-12 exists — only key in use is china_data_consent
POST p/personalization/api/Personalization/attributes PersonalizationApi.kt:16-17 exists — schemaless key/value store
email / marketing preferences (mobile) BaseAppSettingsIpm.kt:252-255 absent in mobile — §10 closes it; the web-side rows follow
GET/POST/PUT {PREFERENCES_URI}/v1/preference[/{id}] MyPADI §4.8 exists web-side — not yet consumed by mobile
GET/POST/PUT {PREFERENCES_URI}/v1/unsubscribe MyPADI §4.8 exists web-side
POST prefapi.scubadiving.com/recipient MyPADI §5 exists web-side — SFMC prefs
GET {PREFERENCES_URI}/v1/MarketingLanguage/{affiliateType} MyPADI §4.8 exists web-side — the PREF-01 open thread
consent-management platform absent, and it survives §10 — zero hits in all four repos, none in §11

Membership → MYP-05

Method Path Source Status
all membership / Club / billing Constant.swift:135,201 absent in mobile — §10 and §11 close status, renewal status and auto-renew; billing history alone is unresolved (§12)

Target side names the Pro API, the Piccolo entitlements engine and Stripe (MyPADI §5); mobile exposure unverified.

Dashboard & commerce → MYP-01

Method Path Source Status
aggregate dashboard endpoint absent, and it survives §10 and §11 — confirmed on every source
GET Product/Search NewCourseApi.kt:11 exists — but static X-API-KEY, not user-scoped
order / purchase history absent in mobile — §10 closes it: POST cart/getOrders, separate auth realm

Count — mobile surface only

Exist and are consumed by the mobile apps today 34
Not called by the mobile apps 18
Total surface this card touches 52

Do not read the 18 as gaps. Running them through step 2 (§10) closes most: profile read, membership status, preferences, recommendations and order history all have endpoints a web front end calls.

Four gaps survive both steps — and §12 re-tests all four against the MyPADI OpenAPI spec:

  1. Per-course completion percentage (MYP-04) — absent from the mobile apps, the web capture, the live-tested matrix and the spec. Holds, with one unread response body named in §12.
  2. Dashboard aggregation (MYP-01) — every section has an endpoint; nothing composes them. Holds.
  3. Consent and privacy systems (MYP-09, PREF-03) — the source says "Privacy systems not documented". Holds, searched directly.
  4. Membership billing history (MYP-05) — asked in June 2026, unanswered. Downgraded to unverified — the endpoint that would carry it is GraphQL and nobody has introspected the schema. Do not put this to PADI as a gap until they have.

And read all 34 with the transition caveat attached. Per Ralph Lai and Kanikar Phan (2026-03-31), several endpoints are being rebuilt for the legacy sunset and no list has been shared. Two questions to PADI, in this order:

  1. Which of these survive the sunset, and when do the replacements land? (PADI-1) — this can invalidate work already scoped as safe.
  2. Which of the three surviving gaps will be closed, and by when? (Not billing history — that is ours to verify first, per §12.)

The second is the question everyone is asking. The first is the one that can quietly turn a buildable verdict into rework, and it has not been asked.


10. Web front-end endpoints — step 2 of the test

The sections above inventory what the current mobile apps call. That is step 1. This section is step 2: endpoints that PADI web front ends call, which the unified Expo RN app can therefore also call.

Two sources, and the difference between them matters:

  • Live-tested — Confluence AWS Cognito Authentication Architecture §7 (2026-06-11). Called outside a browser with a real Cognito idToken, consumer and pro accounts, against non-prod. This is the only evidence class that proves a non-browser client can reach the endpoint.
  • Web-capturedMyPADI — Unified Diver Profile.docx. Observed in the browser Network tab. Proves the endpoint exists and serves the data; does not prove it answers a native client.

The access matrix — live-tested

Data Endpoint Auth
Reference data /r/references/Countries · /states · /cities · /Languages · /Currency none
Profile (self) /p/profile/web/me idToken
Profile by id /p/profile/affiliateid/{id} idToken
Club / membership /c/club/member/subscription idToken
Certifications /c/certification/affiliate/{id} idToken
Photo /p/photos/api/certification/{id} idToken
Courses /v2/entitlements/{id} · /v2/courses idToken
Logbook logbook-stage…/api/Logbook (Hasura GraphQL) idToken
Orders frontastic…/cart/getOrders commercetools session
Travel bookings travel.padi.com/api/v2/travel/account/bookings/ Travel JWT

Named failure: "Cert / Photo reject the SSO token (401) — need a different client/scope/key from the API owners."

Profile — web-captured

Base learning-prod.padi.com/profiles, Bearer + affiliate-id.

GET /{affiliateId} · GET /{affiliateId}?profile=true · POST /{affiliateId}/MailingAddress

Response fields: photoUrl, fullName, firstName, lastName, dob, memberNumber, memberSince, affiliateId, hasAutoRenewEnabled, languageCommunicationPreference, primaryEmailAddress, phoneNumber, countryCodeISO, teachingAddress{…}, mailingAddress{…}.

Club — web-captured

"The club experience largely calls the Pro API, the core preference API, the Piccolo entitlements engine, and Stripe-backed billing."

Endpoint Purpose
GET piccoloapi.scubadiving.com/customer/entitlements/active Active entitlements / benefits
GET {PRO_API}/journey/clubmember Club member status
GET {PRO_API}/pros/stripe/getcarddata/1/{id} Stripe card on file
GET {PRO_API}/journey/routing/othercourses Other-courses routing
GET {BASE_API}/m/membership/v1.2/store/provision Store provision

Entitlement matching is server-side in a c/club REST BFF — "the client just presence-checks each key" (ADR-0004, axelerant-padi/mobile-poc).

Preferences — web-captured

Method Endpoint
GET/POST/PUT {PREFERENCES_URI}/v1/preference[/{id}] · /v1/unsubscribe
GET {PREFERENCES_URI}/v1/MarketingLanguage/{affiliateType}
POST prefapi.scubadiving.com/recipient — push to SFMC

{PREFERENCES_URI} = https://api.core-prod.padi.com/preference. Categories: diveAbroad, localDiving, environmentConservation, events, diveCentricTrips, padiCommunications, thirdParty.

Defect: returns only numeric preferenceId + status; no endpoint returns the labels (PADI-8).

Learning — web-captured

GET /v2/entitlements/{affiliateId}?filter=idc · GET /v2/courses?tag=recommended · GET /v2/courses/{packageId}/details · GET /v2/courses/{id}/assessmentresults · POST /v2/courses/{packageId}/enroll

?tag=recommended is what makes MYP-12 buildable today — but see §14: the target-state reference places recommendations outside the API surface entirely. No percent-complete or last-accessed field exists on any of these (PADI-5).

Orders — the awkward one

Four steps, and a different credential:

  1. Cognito InitiateAuth with the store client 2i83ve0ida4mfe5meeem8i4qe9 → idToken
  2. POST account/getAccountForSso {idToken} → returns a frontastic-session response header (not a cookie — must be captured and re-sent)
  3. POST account/getAccount with that header
  4. POST cart/getOrders with that header → Order[]

Verified negative worth knowing: "the account Orders panel only fires cart/getCart, so it renders empty for every account (verified: a store account with 20 confirmed orders shows none). Verify orders via the API, not the store UI."

Auth model

"Authorization: Bearer <Cognito idToken> plus affiliate-id header. Default for profile, courses, club, pro, checkpoint, preferences, eCards, membership."

idToken expiry is 60 minutes, and aud must match the target client — "the exchange re-issues per target; you do not reuse one token everywhere."

What is not established

  • CORS / allowed-origin policy — no statement exists for any of these hosts. Confirmed absence.
  • §4.2, §4.5, §4.6 of the MyPADI doc — implied by numbering, content not recovered. The tables above are substantially but not provably complete.
  • mypadi-api-mapping and mypadi-api-explorer (Spin) — indexed as project records only. The highest-value next read.

11. The MyPADI OpenAPI spec — step 2, formalised

Source: SwaggerHub axeleranttechnologie-a45/mypadi/1.0.0 — OpenAPI 3.0.3, 16 operations, retrieved 2026-09-10 from https://api.swaggerhub.com/apis/axeleranttechnologie-a45/mypadi/1.0.0.

This is the most structured endpoint source in the corpus and it was not consulted when §10 was written. It corroborates the live-tested access matrix on every endpoint they share, and it adds eight operations that appear nowhere else in this study.

It does not overturn INT-4. The SwaggerHub org is axeleranttechnologie — Axelerant's, not PADI's. This is still Axelerant discovery, now written as a spec rather than prose. It is a better-quality artefact of the same evidence class, not a PADI contract. §8's claim that no PADI-authored contract exists stands; §8's phrasing "no server-authored API contract exists anywhere" should be read as "none authored by PADI", and "no Postman collection in any of the four repos" remains literally true — this lives outside the repos.

Where a spec operation and a live test agree, treat the endpoint as live-tested: the spec adds structure, the test adds proof.

Declared servers. api.global-prod.padi.com · api.padi.com · api.pro-prod.padi.com · logbook.global-prod.padi.com, plus non-prod api-stage.global-np.padi.com · api.global-np.padi.com · logbook-stage.global-np.padi.com. Security schemes: bearerAuth and basicAuth.

Coverage against this study

# Operation Summary In this study?
1 POST /auth/api/oauth/login Login Path variant — see below
2 GET /p/profile/web/me Profile by current user §10, live-tested
3 GET /p/profile/affiliate/{id} Profile by affiliate ID No — distinct from affiliateid
4 GET /preference/v1/preference/{id} Preference §10 (as {PREFERENCES_URI}/v1/preference)
5 GET /c/certification/affiliate/{id} Certifications §10, live-tested
6 GET /c/club/member/subscription Membership & Club benefits §10, live-tested
7 POST /pros/graphql Membership Data §6 (as ProChek) — new purpose
8 POST /api/Logbook Logbook §10
9 POST /o/oauth/api/token Token (client credentials, basicAuth) No
10 POST /p/profile/bulk Profile bulk lookup No
11 GET /c/codes/search/{email} Codes service No
12 GET /p/product/catalog/sku/{sku} Product catalog No
13 GET /c/credentials/{id}?allCredentials=true Credentials No
14 GET /c/certification/instructor/{id}/attribute-counts Certification attribute counts No
15 GET /p/profile/affiliateid/{id} Account Since Path in §10 — purpose was not known
16 GET /c/certification/affiliate/{id} (non-prod) Certifications duplicate of 5

Eight operations were missing from this study. None of them creates a new gap, but one removes one — the member query and /c/credentials/ are why MYP-05's billing gap is withdrawn in §12 — and three change how an item should be built.

What the new operations mean for the card

Operation Bearing
GET /c/credentials/{id} A credentials surface distinct from /c/certification/*. MYP-03 and MYP-10 both assumed certifications were the only list. allCredentials=true implies a filtered default — worth knowing before building the eCard wallet.
GET /p/product/catalog/sku/{sku} A user-token product lookup on the gateway. §9 flagged that the mobile catalogue call (Product/Search) uses a static X-API-KEY and is not user-scoped. This is the endpoint that fixes that, and it bears on MYP-12 and on Purchase History in MYP-01.
POST /pros/graphqlMembership Data The query in the spec returns memberSince, renewalStatus, autoRenewEnabled, memberNumber, memberStatus, primaryCredentialName, countryId, office{…}. §6 recorded this endpoint only as ProChek. renewalStatus and autoRenewEnabled are membership fields MYP-05 needs — this narrows the MYP-05 gap to billing history alone, which was already the finding, but now with an endpoint behind the status half.
GET /p/profile/affiliateid/{id}Account Since Named purpose for a path §10 listed without one.
POST /p/profile/bulk Batch profile by uuid[] with ssoLookup and exactMatch. Not a diver-facing need; relevant to account-merge and to MYP-08's identity reconciliation.
GET /c/codes/search/{email} Undocumented purpose. Email-keyed lookup — flag before assuming it is safe to call from a client.
GET /c/certification/instructor/{id}/attribute-counts Pro-side aggregate. Out of scope for this card; note for the Pro sections.
POST /o/oauth/api/token A second token endpoint using basicAuth + client_credentials — a service-to-service grant, distinct from the diver's Cognito flow. Belongs to the BFF, not the device.

Two discrepancies to resolve

  1. Two login paths. The spec declares POST /auth/api/oauth/login. The mobile apps call POST login on the same auth base — Swapi.kt:31 is literally @POST(value = "login"), resolving to auth/api/login. Both are documented as working. Nobody has said which the unified app should use, or whether one is being retired under PADI-1.
  2. Two profile-by-id paths. /p/profile/affiliate/{id} and /p/profile/affiliateid/{id} are both declared, with different summaries. §10 knew only the second.

⚠️ Credentials are exposed in this spec

The login operation carries two working-looking credential pairs in its request examples — usernames and plaintext passwords for a PADI test account and a @padi.com account, plus two Cognito clientId values.

The spec is publicly readable. It was retrieved for this study over plain HTTPS with no authentication and no API key. Anyone with the URL, or anyone who finds the SwaggerHub org, has those credentials.

This is not a scope finding — it is a live exposure, and it belongs with INT-3 rather than in a scope readout. Route it the same way: Tashrik first, then PADI in writing. The credentials are deliberately not reproduced here.


12. Gap re-test — what would overturn each gap

Prompted by a direct challenge: we cannot tell PADI something is a gap if it is already present. Every gap claim in this study was re-tested against the SwaggerHub spec, the live-tested matrix, the web capture and the four repos.

The distinction that matters. There are two kinds of "absent":

  • Proven absent — we have looked at the thing that would contain it, and it is not there.
  • Not yet seen — we have never inspected the response, so absence is an assumption.

The first is a gap. The second is an unverified claim wearing a gap's clothing, and it is the dangerous one: it puts PADI on the hook to build something they may already serve.

Three gaps in this study were resting partly on the second kind. They are marked below.

MYP-04 — course completion percentage · gap holds, one check outstanding

Proven absent from: the mobile apps (searched — the only percentage in either repo is DownloadService.kt:101, an asset-download progress bar), the SwaggerHub spec (which declares no learning endpoints at all), and the live-tested matrix.

Not yet seen: the response body of GET /v2/courses/{packageId}/details. The web capture summarises it as "course + status details" and nobody has recorded the fields. If a percentage exists anywhere, it is there.

Check before the readout: one authenticated GET /v2/courses/{packageId}/details and read the field list. If it carries a percentage or a last-accessed date, MYP-04 stops being a gap and becomes buildable.

MYP-05 — renewal date, tier, billing history · downgrade to unverified

This one was overstated. The study said these fields are "named on no endpoint". That was true of the sources consulted, but three relevant response shapes have never been inspected — and one of them cannot be reasoned about at all from a path:

Endpoint Why it could already carry this
POST /pros/graphql GraphQL. The spec's example query returns memberSince, renewalStatus, autoRenewEnabled, memberNumber, memberStatus, primaryCredentialName, countryId, office{…}. That is one query, not the schema. You cannot assert a field is absent from a GraphQL endpoint without introspecting it.
GET /c/club/member/subscription Live-tested as reachable; response fields never recorded.
GET /c/credentials/{id}?allCredentials=true Newly found in the spec, unexamined. primaryCredentialName on the member query suggests a credential/tier model exists behind it.

renewalStatus and autoRenewEnabled are already served — so the "status" half of MYP-05 was understated too, and it is stronger than the study currently says.

Check before the readout: a GraphQL introspection query against api.pro-prod.padi.com/pros/graphql with a diver idToken, then read /c/club/member/subscription and /c/credentials/{id}. This is the single highest-value verification left in the card — it can move MYP-05 from partial gap to buildable.

MYP-01 — dashboard aggregation · gap holds

Proven absent. The SwaggerHub spec declares 16 operations and none composes across domains; POST /p/profile/bulk is a batch lookup by uuid[], not a per-diver aggregate. Confirmed independently from the web side (2026-08-12 review) and the mobile side. Nothing found in any source composes profile + certifications + learning + membership in one call.

This gap is safe to state, and it is Axelerant's BFF to close rather than PADI's — which is the point worth making at the readout.

Proven absent, and searched directly. The only consent machinery anywhere in the estate is:

  • china_data_consent, a single key on the generic Personalization key/value store (Personalization.swift:11 ConsentKey)
  • TrackingConsent.GRANTED, hardcoded — that is the Datadog analytics SDK (PadiApplication.kt:318), not diver consent
  • eLearning parental-consent copy in strings.xml and the localisation files — text shown to a guardian, backed by no consent record

No consent platform, no data-export endpoint, no erasure endpoint, in any repo, capture, live test or the spec. The source's own words: "Privacy systems not documented."

Account deletion is confirmed manual. DeleteAccountViewModel.kt:47 composes an HTML email to privacy@padi.com asking an operator to "deactivate their SSO, remove them from all marketing communication lists, and send a confirmation email." One partial counter-example exists and is worth naming honestly: PADI Adventures has a real DELETE account/ — but it deletes a Travel account on a separate Django backend, not a PADI identity, so it does not close this.

Summary of the re-test

Gap Verdict after re-test
Dashboard aggregation (MYP-01) Holds — proven absent
Consent & privacy systems (MYP-09, PREF-03) Holds — proven absent, searched directly
Course completion % (MYP-04) Holds, with one unread response body outstanding
Membership billing history (MYP-05) Downgrade to unverified — a GraphQL schema nobody has introspected

Two checks stand between this study and a defensible gap list, and both are minutes of work with a valid idToken: introspect pros/graphql, and read /v2/courses/{packageId}/details. Until they are done, MYP-05's gap should be presented as unverified, not as a demand on PADI.


13. The web front ends themselves — step 2, from source

Step 2 of the test can rest on a browser capture written up in a .docx, or on the front-end source itself. The web applications are checked out under existing-web-apps/, so it rests on the source.

Eleven repositories, 2.5 GB: account.padi.com · club.padi.com · learning.padi.com · pro.padi.com · padi.com (Next.js) · languages.padi.com · customer-padi · diviac (Travel) · padi-blt, pro-cms, scuba-diving (Drupal).

252 unique endpoint constructions were extracted from production source across the five Vue/Next front ends, each cited to file:line. Extractor: scratchpad/extract_web_endpoints.py; output scratchpad/web_endpoints.json.

A new evidence class, and it outranks the others

Web-source — the endpoint appears in a checked-out web front end's own code, with a file:line. This is stronger than web-captured: a capture shows one session's traffic, the source shows the whole surface, including paths nobody happened to exercise while the Network tab was open.

It sits just below live-tested, which remains the only class proving a non-browser client is accepted.

What this changes

MYP-05 — billing. The gap was wrong, and so was the withdrawal.

GET {PADI_CLUB_URI}/member/subscription/billing exists and is called by two front ends — club.padi.com/src/api/club/index.js:29 and learning.padi.com/src/api/club/index.js:13.

But read what it returns. The function is getBillingLink, and the consumer is:

// club.padi.com/src/components/notification-bar/NotificationBar.vue:102
window.location.href = `${permissionsStore.billingLink}${isExpiredSoon.value ? '' : '/payment-methods'}`;

It returns a URL to a hosted billing portal, and the web app redirects to it. The web experience does not render billing history natively either. So structured billing history was never part of web parity, and this study was demanding from PADI something no PADI surface provides.

Corrected verdict: buildable at web parity today. Fetch the link with the idToken the app already holds, open it. Two residual points, neither an API gap:

  • Opening a payment portal from inside the app is an IAP question (PADI-7), not an endpoint question. That is the real constraint.
  • If the requirement genuinely means billing history rendered natively in the app, that exceeds web parity and should be recognised as new scope rather than parity.

Related, also newly found: GET {PRO_API}/pros/stripe/getstripesetupintentsecret/{id}/{id}, /setdefaultcard/{id}/{id}, /refreshstripememberdata/{id}/{id} (pro.padi.com/src/api/pros/index.js:512,522,542) — payment-method management alongside the getcarddata the study already had.

AUTH-11 — waivers. "No document-storage endpoint exists" is false.

Section 2 states: "No waiver, signature, or document-storage endpoint appears in any repo, capture or live test." There is one, and both learning.padi.com and pro.padi.com call it — TrueVault, a compliance-grade document vault:

Method Endpoint Source
GET {TRUEVAULT_URI}/Search/Diver learning.padi.com/src/store/modules/forms/actions.js:11
GET {TRUEVAULT_URI}/Search/Member …/forms/actions.js:10
GET {TRUEVAULT_URI}/Search/blob/{id} …/forms/actions.js:149
GET {TRUEVAULT_URI}/Search/document/{id} …/forms/actions.js:163
POST {TRUEVAULT_URI}/Truevault/Data (multipart) …/forms/actions.js:41

The response carries blobIds, formData, medicalRequired, createdDate, memberName — signed diver forms, listed per diver or per member, uploaded as multipart, retrievable as blob or document.

That is precisely the substrate AUTH-11 was said to lack. INT-6's challenge to the "Low effort" estimate should be revisited: a signing UI over an existing document service is exactly what exists. The Legal prerequisite and the missing family-linking model still gate the item — those are unchanged — but "there is no document service" is no longer a reason.

PADI-12 — answered, yes.

POST {COGNITO_URI}/password/change is live and called by both learning.padi.com/src/store/modules/auth/actions.js:137 and pro.padi.com/src/store/modules/auth/actions.js:165. AUTH-05 can keep all auth traffic on the facade; the direct-IdP route is a fallback, not a necessity. Close PADI-12.

AUTH-13 — B2B account creation. The gap is wrong.

Section 2 states: "No B2B registration endpoint. Nothing creates a dive centre, resort, or professional account." Both exist:

  • POST {PRO_API}/pros/account/create/memberpro.padi.com/src/api/account/index.js:6
  • POST {PRO_API}/pros/account/create/storepro.padi.com/src/api/account/index.js:17

The item stays post-Q1 by the artefact's own sequencing, and SCOPE-15 (does B2B belong in a consumer app) is still the right question. But it should be recorded as deferred with endpoints available, not as a gap.

MYP-04 — gap holds; the unread list grows to two

Both front ends call GET /v2/courses/{packageId}/report (learning.padi.com/src/store/modules/report/actions.js:7, pro.padi.com/…:9) alongside /details. Neither response shape is recorded anywhere. /report is now the likelier home for progress than /details.

Also newly surfaced: /v2/courses/{id}/instructor/assessmentresults, /v2/specialties, /v2/courses/enroll/{id}.

MYP-09, PREF-03 — gap holds, and TrueVault does not close it

TrueVault stores signed forms, not consent records. Nothing in any web front end serves marketing-consent history, policy-version acceptance, data export, or erasure. Searched directly across all five. The gap stands.

Two false leads, recorded so nobody re-raises them

  1. completionPercentage in club.padi.com/src/components/expired/type.ts:13. An optional prop on a UI interface, referenced nowhere else in the repo. Not an API field.
  2. progress: 75 and lastAccessed in learning.padi.com/tests/unit/store/modules/report/mutations.spec.js:59. Arbitrary fixture data in a test named "should handle complex report data", exercising a mutation that blindly assigns its argument. Not evidence of a response shape.

Both look like proof of a progress API. Neither is. They are worth naming because the next person to grep will find them too.

Endpoints this card touches that the study had not recorded

Endpoint Bearing
GET {PADI_CLUB_URI}/content/dashboard/general · /specific Club dashboard content — MYP-01. Not a cross-domain aggregate, so the aggregation gap holds, but the club section has more than subscription behind it.
POST {PADI_CLUB_URI}/member/print/opt-out Print preference — a preference surface outside the preference API.
GET {CLUB_API}/p/profile/web/me/addresses/mailing Address read on the profile route — MYP-02.
GET/PUT {PREFERENCES_URI}/v1/unsubscribe/{id} Per-id unsubscribe — MYP-07, PREF-02.
{PREF_API}/attribute Personalization attribute from the web side — MYP-09's key/value store.
{CONTACT_SYNC}/queue Contact-sync queue, called after profile writes — bears on PADI-3 (does a write propagate).
GET {PRO_API}/journey/routing/othercourses Confirmed in source (learning.padi.com/src/api/pros/index.js:65) — MYP-10 upgrades from web-captured to web-source.
GET {LEARNING_URI}/v2/courses?tag=recommended Confirmed in source (learning.padi.com/src/store/modules/courses/actions.js:33) — MYP-12 likewise.

What still has not been read

The five Vue/Next apps are covered. Not yet examined: padi.com (Next.js) beyond its URL constructions, customer-padi, diviac (Travel front end, 1.9 GB), and the three Drupal repos (padi-blt, pro-cms, scuba-diving). diviac is the one that matters for the Travel auth realm in MYP-08 and SCOPE-7.


14. The MyPADI Personalised Home reference — the successor to §11

Source: my-padi-api-reference.spin.axelerant.tech — OpenAPI 3.1.0, 20 operations, version 0.1.0-draft, read 2026-09-18. Produced in the 15 September design-to-API mapping session with the design team and PADI Engineering; organised by persona and home-screen section, with Figma-linked screenshots against each operation.

It supersedes §11's SwaggerHub spec without retiring it: same lineage, eight days later, four operations larger.

Carried forward 12 operations
Added 8 — learning/v2/entitlements, learning/v2/courses/{id}/details, learning/v2/courses/{id}/assessmentresults, p/photos/api/certification/{id}, affiliations/affiliated/{packageId}, forms/graphql, pros/renewals/info, c/certification/certificate/all
Dropped 3 — p/profile/web/me, p/profile/affiliate/{id}, preference/v1/preference/{id}

Three cautions, unchanged from §11 and one new. It is Axelerant-authored, not a PADI contract — its own introduction says "Response shapes are inferred from the fields the doc names. They need confirming against the real services." It is scoped to one screen, so absence from it carries no information; only presence does. And nobody at PADI has said it replaces anything — it is a design-to-API mapping, not a deprecation notice. INT-4 stands.

What it settles. §11's discrepancy 2 (two profile-by-id paths) resolves toward /p/profile/affiliateid/. §11's discrepancy 1 (two login paths) does not — two specs in a row now name auth/api/oauth/login and neither mentions the apps' auth/api/login. A third path pair joins it: certification photo on /p/photos/api/ here versus /p/photos/web/ in §9.

What it changes. The full item-by-item re-verdict is in Logged In Experience, which now carries a Target state block per item. The four that move:

  1. MYP-12 is downgraded — the reference lists "the recommended course/specialty modules" under Not API-backed, its own category for link-outs and Drupal content. This is the one absence that is not silence.
  2. MYP-03's payload collapses — the target Certification schema carries 4 fields against ECardInfo's ~55. A certifications list is not an eCard.
  3. MYP-08 becomes architecture — two credentials, and the one serving profile, certifications, club, codes and catalog is a client-credentials grant that cannot ship in a binary. The BFF becomes a prerequisite.
  4. MYP-04 gains a routeassessmentresults returns per-section booleans, making a percentage derivable. No client has ever called it.

One environment note for §2. In production the reference puts every /p/ and /c/ operation on api.padi.com except the certification catalog, still on api.global-prod.padi.com — which its author flags as a likely oversight rather than a real split.