Section 1 — Logged In Experience
Card: Logged in Experience · 15 items · Source: PADI Mobile Scope Alignment v-ceb35f4d
Consumer: the unified PADI app — Expo React Native, shipping as an update to the existing PADI main app.
This document answers one question per item, and since 18 September 2026 it answers it twice:
- Current state — can the unified app build this with an endpoint that exists today?
- Target state — does the MyPADI Personalised Home API Reference provide for it?
It is not an audit of kms-ios-padi or kms-android-padi. Those apps are being replaced. They appear here only as evidence — the strongest kind available, because an endpoint the current app calls is proven to work from a mobile client with a diver's token.
Why this card was re-verdicted against a second source
This card is the study's analysis of the MyPADI dashboard, and MyPADI now has a declared target-state API surface: the PADI Personalised Home — API Reference (OpenAPI 3.1.0, 20 operations,
v0.1.0-draft), produced in the 15 September design-to-API mapping session with the design team and PADI Engineering, and published atmy-padi-api-reference.spin.axelerant.tech.Each item below therefore carries a
Target stateblock naming what that reference provides — or does not. Where the two states disagree, the disagreement is the finding.Three cautions, and they govern everything below.
It is 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 before this reference is shared outside the team." It is a better artefact of the same evidence class as the SwaggerHub spec in the API inventory §11 — Axelerant discovery, now persona-organised. Finding 5 below still stands.
Absence from it is not deprecation. The reference is scoped to one screen. It names the sections it deliberately excludes because they resolve to a link, Drupal content or the Dive Shop Locator. So only presence is evidence here; an endpoint missing from it may simply not appear on that screen.
Presence in it is not a commitment either. Nobody at PADI has said which of today's endpoints are being retired in favour of these. That is still PADI-1, and this reference is the closest thing to a partial answer the study has.
The test
- Does the existing mobile app call an endpoint for this? → usable, lowest risk.
- If not — does a PADI web front end call one? → usable. RN speaks the same HTTP as Vue.
- Only if neither → a genuine gap PADI must close.
Evidence classes, strongest first
| Class | What it means |
|---|---|
| Live-tested | Called outside a browser with a real Cognito idToken and confirmed working. Source: Confluence AWS Cognito Authentication Architecture §7 (2026-06-11) — "Tested live against non-prod with a real idToken (id & access token, consumer & pro accounts)." This is the only class that proves non-browser callability. |
| Mobile-proven | The current native apps call it. Cited to file:line. |
| Web-captured | A web front end calls it, observed in the browser Network tab. Source: MyPADI — Unified Diver Profile.docx. Proves the endpoint exists and serves the data; does not prove it answers a non-browser client. |
| Requirement only | Named in an epic or ticket, with no endpoint anywhere. This is a gap. |
Overlapping items — the card has 15 entries, not 15 capabilities
Two pairs in the artefact describe the same capability under two IDs, and a third pair reads like a duplicate but is not. That is a property of the source list, not of this study, but it is worth naming up front so the analysis is not read twice — and so the two real duplicates are not estimated twice.
| Pair | Relationship | How this document handles it |
|---|---|---|
| MYP-07 Email communication preferences ≡ PREF-02 Manage contact preferences | Same endpoints, same auth, same defect. No stated difference. | Analysed once under MYP-07. PREF-02 carries only what is genuinely its own — the granularity question. |
| MYP-09 Consent / preference governance ≡ PREF-03 Manage privacy and consent options | Same missing systems. MYP-09 is the orchestration framing, PREF-03 the diver-facing framing. | Analysed once under MYP-09. PREF-03 carries only the deletion-path exposure. |
| MYP-10 Certification path tracker ≈ MYP-12 Next-course recommendations | Adjacent, but not the same — they resolve to different endpoints (journey/routing/othercourses vs /v2/courses?tag=recommended). |
Analysed separately. The overlap is in the requirement wording, not the API. |
Effective distinct capabilities: 13 — 15 IDs less the two real duplicates. All 15 IDs are retained below because they are the artefact's own identifiers and estimation will be tracked against them; the duplicates carry a pointer instead of a second analysis. SCOPE-6 and SCOPE-9 confirm the pairings; only SCOPE-6 can change the count.
Two further titles overlap in wording without overlapping in work: MYP-02 says "preferences" in its title but its scope text is personal information only, and PREF-01 (language) is a preference written through a different endpoint than the rest.
Read this first — six findings that govern every item
1. The auth model is settled, and it is one header
"Authorization: Bearer <Cognito idToken> plus affiliate-id header. Default for profile, courses, club, pro, checkpoint, preferences, eCards, membership." — MyPADI doc §2
The unified app already holds this token. Most of the card is reachable with a header pair it will have on day one.
2. One token does not open everything — plan for token exchange
"The minted token's aud matches the target client — each property validates against its own client id… The exchange re-issues per target; you do not reuse one token everywhere." — Cognito Architecture
Three separate realms, and this is architecture, not a detail:
| Realm | Credential |
|---|---|
| PADI gateway — profile, club, courses, certs, logbook | Cognito idToken (60-min expiry) |
| Store — orders | commercetools session, bridged from a store-client idToken |
| Travel — bookings | Travel JWT |
Known failure: "Cert / Photo reject the SSO token (401) — need a different client/scope/key from the API owners."
3. Language is written in three places and does not propagate
"Language is stored in 3 places — Cognito custom:language, preference communicationLanguage, and profile languageCommunicationPreference — written by different endpoints, so an edit on one platform doesn't propagate." — MyPADI doc §11.1
This directly complicates PREF-01: the endpoints are fine, the data model is not.
4. There is still no aggregate endpoint — and the target state makes it worse
Confirmed from both sides. Every dashboard section is its own call. Axelerant owns a BFF/middleware layer per the 2026-08-12 review, so composing is in scope — but a BFF can only compose what exists.
The target reference confirms this and quantifies it. It documents 20 operations across six hosts for one screen, with no composition layer of any kind. And the persona model makes it worse than a fan-out: which dashboard renders is itself derived from API responses — GET /c/codes/search/{email} and the certifications list decide whether a diver sees First Visit, DSD Completed, eLearning In Progress or Teach Diving. So the shell cannot render until identification resolves, which serialises part of a launch sequence that was already wide.
This is not a theoretical worry: the 15 September session recorded an action to "simulate all API requests to measure call count and latency." Someone on that call had already seen it.
5. Nothing here is a PADI-published contract
Every endpoint below is either observed in production traffic or live-tested by Axelerant. PADI has published no API documentation; it was requested 2026-04-24 and again 2026-07-13. And per PADI-1, an unknown subset is being rebuilt for the legacy sunset.
The target reference does not change this. It is Axelerant-authored, self-declared as inferred, and version 0.1.0-draft.
6. The target state needs two tokens at once, and routes per endpoint
This is the single most consequential thing the reference adds, and it upgrades Finding 2 from a warning to a specification. The reference marks on each operation which credential it takes:
| Credential | Minted by | Serves |
|---|---|---|
bearerAuth — the idToken |
POST /auth/api/oauth/login |
learning, pro, logbook |
oauthToken — an access_token |
POST /o/oauth/api/token (client credentials) |
profile, certifications, codes, catalog, club |
Read that split carefully: certifications and profile move off the diver's idToken and onto a client-credentials token. A client-credentials grant is a service credential — it is minted from a Client ID and Secret Key, which cannot ship in a mobile binary.
So in the target state the unified app cannot call profile, certifications, club, codes or catalog directly. Something server-side has to hold the secret and mint that token. That makes the Axelerant BFF a hard requirement rather than a performance optimisation, and it is the strongest architectural statement in this card.
It also quietly explains Finding 2's known 401: "Cert / Photo reject the SSO token". They reject it because in the target model they do not take it.
MYP-01 — Authenticated Dashboard Shell & Navigation
Area 11 Platform Capabilities · Jira E9 API status: Gap · Feasibility: buildable section by section; no aggregate
What it needs. A logged-in landing surface with five sections — Profile, Certifications, eLearning Progress, Membership, Purchase History — each loading independently.
Endpoints available. Four of the five sections now have a path:
| Section | Endpoint | Class |
|---|---|---|
| Profile | GET /p/profile/web/me |
Live-tested |
| Certifications | GET /c/certification/affiliate/{id} · eCard/* |
Live-tested + mobile-proven |
| eLearning | GET /v2/entitlements/{affiliateId} |
Live-tested |
| Membership | GET /c/club/member/subscription |
Live-tested |
| Purchase History | POST cart/getOrders |
Live-tested, different auth realm |
Gap. The aggregation layer, not the sections. Five calls across three auth realms on every cold launch — and the store call needs a session bridge before it can fire at all.
Notes for the RN client. This is the strongest case in the card for the Axelerant-owned BFF: one call from the device, fan-out server-side, one place to hold the store-session bridge and the token exchange. Without it the launch sequence is five round trips and three credentials. PLT-01 (offline foundation, committed) also means each section needs a cache and staleness policy the web requirement never had to specify.
Target state — MyPADI Personalised Home reference. The gap is confirmed and enlarged.
The reference documents 20 operations across six hosts for this one screen and provides no aggregation of any kind. Worse, the persona model adds a dependency the current requirement never had: which dashboard renders is derived from API responses — GET /c/codes/search/{email} (matching sku against OPEN_WATER_SKUS / DSD_SKUS, with processedDate distinguishing completed from merely assigned) plus the certifications list decide between First Visit, DSD Completed, eLearning Purchased, In Progress, Completed, Keep Diving and Teach Diving.
So identification must resolve before the shell can choose what to render. That serialises the front of a launch sequence that was already a wide fan-out — and it is why the 15 September session logged an action to "simulate all API requests to measure call count and latency."
Purchase History, meanwhile, has no entry in the reference at all, and Offers is listed under Not API-backed.
Verdict. The dashboard is buildable. The performance and auth shape is the open question, and it is Axelerant's to design rather than PADI's to provide.
Questions: PADI-2 (aggregate endpoint) · SCOPE-1 (Purchase History in Q1 scope)
MYP-02 — Profile — personal information, preferences, view & edit (field-level permissions)
Area 01 Identity, Profile & Account · Jira E10 API status: Existing — web · Feasibility: all five fields available
What it needs. Display "full name, profile photo, preferred language, home country, and communication preferences"; allow edits; GDPR controls; degrade gracefully.
Endpoints available.
| Method | Endpoint | Auth | Class |
|---|---|---|---|
| GET | /p/profile/web/me |
idToken | Live-tested |
| GET | learning-prod.padi.com/profiles/{affiliateId} |
Bearer + affiliate-id | Web-captured |
| POST | /profiles/{affiliateId}/MailingAddress |
Bearer + affiliate-id | Web-captured + mobile-proven |
| PUT | /user/update/{email} — name, DOB, gender |
Bearer | Web-captured + mobile-proven |
| PUT | /user/update/language/{email} |
Bearer | Web-captured + mobile-proven |
| GET | /r/references/Countries[/States/cities] |
none | Web-captured |
The profile response carries every field scope line 1 asks for:
photoUrl · fullName · firstName · lastName · dob · memberNumber · memberSince · affiliateId · hasAutoRenewEnabled · languageCommunicationPreference · primaryEmailAddress · phoneNumber · countryCodeISO · teachingAddress{…} · mailingAddress{…}
Home country is countryCodeISO on the profile, and profile photo is photoUrl — both on the same response, so neither needs the analytics feed as a fallback.
Gap. None for read or write. What remains is a correctness problem, not an availability one: no web front end writes to Salesforce. Every documented write goes to Cognito or the learning profiles API. Salesforce write-back exists only as unbuilt scope (PADI-68, endpoint "TBC"). If Salesforce is the system of record, the propagation path is undefined for web and mobile alike.
Notes for the RN client. "Real time" write-back conflicts with PLT-01: an edit made at a dive site queues and syncs later, so two devices need a conflict rule nobody has specified. And /p/profile/affiliateid/{id} exists alongside the self route — the doc's own §11.1 asks "what authorizes by-affiliateId access?", which matters if the app ever renders another diver's profile.
Target state — MyPADI Personalised Home reference. Covered, on a different path from the one this card records.
| Need | Target endpoint | Credential |
|---|---|---|
| Full profile record | GET /p/profile/affiliateid/{affiliateId} |
oauthToken |
| Lookup by global UUID | POST /p/profile/bulk |
oauthToken |
Two things follow. First, GET /p/profile/web/me — the endpoint this card currently leans on, and the one that made MYP-02 Existing — web — does not appear in the reference. Per the absence caution above that is not a retirement notice, but it is the second spec in a row to prefer the affiliateid path, and the API inventory §11's discrepancy 2 (two profile-by-id paths) is now resolved in that direction.
Second, and larger: profile is an oauthToken operation in the target model, not an idToken one. See Finding 6 — the app cannot mint that credential itself.
Field-level permissions remain absent. Nothing in the reference addresses them.
Verdict. Buildable now. The risk is data integrity across systems, not missing APIs.
Questions: PADI-3 (Salesforce propagation) · SCOPE-2 (field-level permissions) · SCOPE-3 (offline conflict rule)
MYP-03 — Certifications & Digital eCard Display
Area 02 Certifications & eCard · Jira E12 API status: Existing — mobile · Feasibility: buildable, one auth caveat
Endpoints available. The best-covered item in the card. Mobile-proven: the whole eCard/* family (ECardsApi.kt:9-35) with a ~55-field ECardInfo, plus Cloudinary artwork. Live-tested alternative: GET /c/certification/affiliate/{id} on the gateway.
Caveat that matters. "Cert / Photo reject the SSO token (401) — need a different client/scope/key from the API owners." The current app reaches these with its own client; a new app minting tokens through the unified SSO flow may not. Confirm the client/scope before treating this as free.
Gap. None for display. The 24-hour LMS auto-surfacing webhook remains unverified and is a freshness question, not a capability one.
Notes for the RN client. List-shaped and cacheable — suits CERT-01 (offline certification history, committed).
POC coverage. poc/padi-cert-offline-poc builds exactly that on Expo + Realm: certifications as a "durable local cache that reconciles (pull-only) from the server", rendering instantly online or offline. Certifications are read-only by design — they originate in the LMS, M2/Macola and Salesforce and the app never edits them — so the offline model is settled.
Target state — MyPADI Personalised Home reference. Covered, and the payload is dramatically thinner than the eCard needs.
| Need | Target endpoint |
|---|---|
| Certifications | GET /c/certification/affiliate/{affiliateId} |
| Professional credentials | GET /c/credentials/{affiliateId} |
| Certification photo state | GET /p/photos/api/certification/{affiliateId} |
The reference's Certification schema carries four fields — certificationName, certificationNumber, certificationDate, diverNumber. The current mobile ECardInfo carries ~55 (ECardInfo.kt:14-68), including every field the rendered card needs: stock ID, card title, diver lines, instructor lines, store block, logos, footer lines, canPrint, qrCode.
So the target reference describes a certifications list, not an eCard. If it is read as the replacement for eCard/GetCardListConsumer, the card-rendering payload disappears. That is the most likely place for this transition to break something that works today, and it should be asked directly rather than assumed to be an omission of convenience.
Two further constraints came out of the 15 September session and are not in the spec text: the certifications API returns rows in arbitrary order and the query parameters for sorting by completion date do not work; and pinning has no API at all — it must be stored server-side to stay in sync between web and mobile, and only one certification can be pinned.
View all certifications and Order a card are both listed under Not API-backed — link-outs to learning.padi.com and store.padi.com.
Verdict. The strongest item in the card. Recommended for Q1 subject to PADI-4 — confirm the client and scope before treating it as free.
Questions: PADI-1 (sunset) · PADI-4 (cert token scope)
MYP-04 — eLearning Progress Section
Area 05 Learning & Course Progress · Jira E13 API status: Gap · Feasibility: not buildable as specified
What it needs. In-progress courses with completion percentage and last-accessed date.
Endpoints available. Courses yes, progress no:
| Endpoint | Returns |
|---|---|
GET /v2/entitlements/{affiliateId}?filter=idc |
Items[]{packageId, platform, title} |
GET /v2/courses/{packageId}/details |
course + status details |
GET /v2/courses/{id}/assessmentresults · /report |
results |
GET /v2/courses?tag=recommended |
recommended courses |
Gap — the clearest in the card, and it survives step 2. No endpoint or field named for a completion percentage or last-accessed date exists anywhere: not in the mobile apps, not in the web capture, not in the live-tested matrix. It appears only as a requirement, in PADI-70, whose own acceptance criterion is still unmet and which depends on PADI-67 ("API credentials and documentation for LMS" — status Pending).
The one unread response body has now been read, and it confirms the gap. The gap was already proven absent from the mobile apps (the only percentage in either repo is DownloadService.kt:101, a download progress bar), from the MyPADI OpenAPI spec (which declares no learning endpoints at all), and from the live-tested matrix — but GET /v2/courses/{packageId}/details was summarised only as "course + status details" and its fields had never been recorded.
It has been read, but it answers a narrower question than first reported. The endpoint is the LMS course-detail call — GET https://learning-prod.padi.com/learning/v2/courses/{courseId}/details (padiww/ios-padi-app PADI/data/api/impl/LanguageCourses/LanguageCoursesApiImpl.swift:26) — and the iOS model that decodes it (PADI/model/view/OfflineLearning/LanguageCourse.swift:10-29) declares packageId, hasAnalytics, heroUrl, title, isSampleCourse, dateStarted, status, template, hasLegacy, availableLanguages[]. No percentage, and no last-accessed date — dateStarted is a start, not a last access.
Correction. An earlier revision of this card read that as proving the response carries no percentage. It does not. A Swift
Decodabledeclares only the fields the client uses, so this establishes that the iOS app does not consume a percentage from this endpoint — not that the server omits one. The target-state block below sets out what the three surviving sources actually support.
One narrower question replaces it. A different model — PADI/model/view/OfflineLearning/UserCourse.swift:25, the offline-learning course payload — declares percentRemaining: String? alongside complete: Int?. Neither is read anywhere in the app, and Android declares no equivalent. Whether the server populates it is one live call, tracked as PADI-31. Full analysis in 18-learning-course-progress.md.
POC coverage — this settles both the gap and the fallback. poc/mobile-poc's eLearning spike ([PAM-17]) reports MY COURSES: WORKING · COURSE DETAIL: WORKING · OFFLINE PLAYBACK: WORKING · PROGRESS/QUIZ COMMIT: WORKING.
But read what it actually built for progress:
Completion badge (
LessonStatusBadge) — 3-state (not-started/in-progress/complete), not a progress bar. PLP uses the server's per-lessonasset.status; KD7 uses local CMI (cmi.core.lesson_status).
A team that had every incentive to render a percentage built a three-state badge instead — because the server serves a status, not a percentage. That is the MYP-04 gap, confirmed by construction rather than by absence of evidence.
It also means the reduced form in SCOPE-4 is not a proposal — it is already working, with per-lesson status, offline playback and progress commit back to PADI's real hosts. Two honest limits the POC records: EVICTION POLICY: NOT IMPLEMENTED and TWO-DEVICE MERGE: NOT PROVEN.
Notes for the RN client. SCORM commit had to be re-architected for RN — the native apps intercept CMI commits with an override.js shim and an Android scheme-handler, and react-native-webview has neither, so the commit path runs as injected JS. That work is done and proven; it is not a fresh risk.
Target state — MyPADI Personalised Home reference. This is where the target state moves the item most, and it replaces the check this card previously reported as closed.
Three endpoints form a chain:
| Step | Endpoint | Returns |
|---|---|---|
| 1 | GET /learning/v2/entitlements/{affiliateId} |
Entitlement{packageId, sku, assignedDate} |
| 2 | GET /learning/v2/courses/{packageId}/details |
CourseDetails{packageId, customCourseId, title, status, progressPercent} |
| 3 | GET /learning/v2/courses/{customCourseId}/assessmentresults |
AssessmentResult{sectionId, title, isCompleted} — per-section completion |
Step 3 is the finding. Per-section booleans make a completion percentage derivable — count completed sections over total. That is precisely the reduced form this study proposed as SCOPE-4, and it now has a named endpoint behind it rather than being a suggestion.
It also matches what PADI said. From the 15 September session: "PADI Engineering said section progress not determinable; Vatsal found APIs that do it, needs confirmation." Both halves are true — PADI does not serve progress, and an endpoint exists from which it can be computed.
And it corrects an overstatement in this card. An earlier revision recorded the /details check as closed, on the grounds that the iOS decoder for that endpoint declares no percentage field. That inference was too strong: a Swift Decodable declares only the fields the client uses, so it proves non-consumption, not absence. Three sources now expect progress data to be reachable and none has been observed:
CourseDetails.progressPercent— asserted by the reference, which flags its own response shapes as inferredUserCourse.percentRemaining(padiww/ios-padi-appPADI/model/view/OfflineLearning/UserCourse.swift:25) — declared, never read, absent from AndroidAssessmentResult.isCompleted— the derivable route
No mobile client calls /assessmentresults — zero references across both native apps, axelerant-padi/mobile-poc and PadiWW/padi.mobile.app. But a web client does, in production: PadiWW/pro.padi.com src/store/modules/course/actions.js:48-70 calls it to render the eRecord, branching on affiliateId between the learner's own record and the instructor's view of a student. (An earlier draft of this card said no client had ever called it; that was wrong.) So the endpoint is live and reachable — what is unverified is its response body, which the reference itself declares inferred.
Learning Dashboard, Continue Learning and Course archive are all listed under Not API-backed.
Verdict. Not buildable as written. Offer the reduced form and let PADI decide.
Questions: PADI-5 (does the LMS expose progress at all) · SCOPE-4 (is the reduced form acceptable)
MYP-05 — Membership & Billing Section (Club API)
Area 08 Club & Membership · Jira E14 · Flags IAP, parking API status: Existing — web (partial) · Feasibility: status yes, billing no, purchase blocked
Endpoints available.
| Endpoint | Auth | Class |
|---|---|---|
GET /c/club/member/subscription |
idToken | Live-tested |
GET piccoloapi.scubadiving.com/customer/entitlements/active |
Bearer + affiliate-id | Web-captured |
GET {PRO_API}/journey/clubmember |
Bearer + affiliate-id | Web-captured |
GET {PRO_API}/pros/stripe/getcarddata/1/{id} |
Bearer + affiliate-id | Web-captured |
POST /pros/graphql — member { … } |
idToken + affiliate-id | Spec-declared |
GET /c/credentials/{id}?allCredentials=true |
idToken | Spec-declared |
An architecture note worth carrying: club.padi.com does entitlement matching server-side in a c/club REST BFF reachable with the idToken the app already holds — "the client just presence-checks each key" (ADR-0004, axelerant-padi/mobile-poc, 2026-07-24). The app does not need entitlement logic.
More is served than the endpoint paths suggest. The MyPADI OpenAPI spec's member query on POST /pros/graphql returns memberSince, renewalStatus, autoRenewEnabled, memberNumber, memberStatus, primaryCredentialName, restrictedCountry, countryId and office{…}. Renewal status and auto-renew are already available, not just membership status.
Gap — but state it as unverified, not as a demand on PADI. Renewal date, tier and billing history are not named on anything we have inspected. That is not the same as their being absent:
POST /pros/graphqlis GraphQL. The above is one example query, not the schema. Field absence cannot be asserted from a GraphQL endpoint without introspecting it, and nobody has.GET /c/club/member/subscriptionis live-tested as reachable; its response fields were never recorded.GET /c/credentials/{id}is newly found and unexamined — andprimaryCredentialNameimplies a credential/tier model sits behind it.
The question was asked directly in #eng-padi-web on 2026-06-23 — "how much data can be exposed through this api like status, tier, renewal date, billing summary?" — and is unanswered. Introspect the schema before putting this to PADI as a gap (see the API inventory §12).
POC coverage — and it answers part of the outstanding check. poc/mobile-poc's Pro & Club spike reports MEMBER DASHBOARD: WORKING · RENEWAL: WORKING (WebView checkout) · NATIVE CLUB (c/club BFF): WORKING · MEMBER-TYPE GATING: VERIFIED (2/3/9 + lapsed/restricted).
It ran the member query against pros/graphql and recorded the response: memberSince, renewalStatus (e.g. "timetorenew"), autoRenewEnabled, primaryCredentialName, memberNumber, memberStatus, restrictedCountry, countryId, office{…} — plus per-credential {credentialName, expirationDate, autoRenewEnabled}.
So renewal status, auto-renew and a credential expiry date are proven served, and the membership dashboard is proven buildable. What is still unseen is a billing history and a membership-level renewal date distinct from credential expiry. The §12 introspection is still worth doing, but it is now a narrower question.
Blocked, separately. IAP gates anything that initiates a paid renewal. Owner PADI business/legal, point person Kevin Braun, open since April with no date.
The billing endpoint exists, and it is a link. GET {PADI_CLUB_URI}/member/subscription/billing is called by two web front ends (club.padi.com/src/api/club/index.js:29, learning.padi.com/src/api/club/index.js:13). It returns a hosted billing-portal URL, and the web app redirects to it — window.location.href = billingLink + '/payment-methods' (NotificationBar.vue:102).
So web does not render billing history natively either. Structured billing history was never part of web parity, and this study was asking PADI for something no PADI surface provides. See the API inventory §13.
Target state — MyPADI Personalised Home reference. Covered for status, and it adds a candidate endpoint for the unresolved billing half.
| Need | Target endpoint | Returns |
|---|---|---|
| Club membership | GET /c/club/member/subscription |
ClubSubscription{status, renewalDate, autoRenew} |
| Pro membership record | POST /pros/graphql |
memberNumber, autoRenewEnabled, memberSince, renewalStatus, memberStatus, home office |
| Renewal information | GET /pros/renewals/info |
new — not in any earlier source |
GET /pros/renewals/info is the first endpoint named anywhere in this study that plausibly carries renewal history. It does not close the billing-history question, but it makes it answerable: read it, rather than ask PADI whether such a thing exists.
One caution on autoRenew. The reference presents it as a clean boolean. The 15 September session records that it is inferred from the absence of term.cancelAt, and that the NEVER case is an open issue. A schema field that is actually a derived inference is exactly the kind of thing that survives into an estimate as "one field, already served".
A lighter-weight membership check also surfaced in that session and is not in the reference: GET api.pro-prod.padi.com/journey/clubmember, returning isMember and firstTimeUser.
The pros/graphql introspection remains outstanding. The reference lists fields, but it infers them; field absence still cannot be asserted against a GraphQL endpoint nobody has introspected.
Verdict. Status, renewal status, auto-renew and benefits are buildable today. Billing is buildable at web parity today — fetch the link, open it. Purchase is blocked by IAP, and opening a payment portal in-app is itself an IAP question (PADI-7), not an endpoint one. If the requirement means billing history rendered natively, that exceeds web parity and is new scope, not a gap.
Questions: PADI-6 (billing history endpoint) · PADI-7 (IAP decision date)
MYP-06 — Profile image upload
Area 01 Identity, Profile & Account API status: Existing — web · Feasibility: buildable; requirement still ambiguous
Endpoints available. photoUrl on the profile response for display; GET /p/photos/api/certification/{id} (live-tested) and {PRO_API}/certifications/photos/getphoto for the certification photo; POST certifications/photos/upload (mobile-proven) for upload.
The ambiguity is unchanged and unresolved by any endpoint. The only photo pipeline is the moderated certification photo — reviewed, rejectable, printed on the eCard. If the item means a casual avatar, that pipeline is wrong regardless of which endpoint serves it.
Caveat. Photo shares the cert 401 problem — the SSO token is rejected.
Target state — MyPADI Personalised Home reference. Read only, on a path that differs from the one this card records.
GET /p/photos/api/certification/{affiliateId} returns a PhotoStatus. Note the path segment: the reference uses /api/ where this card records p/photos/web/certification (GetCertificationPhotoApilmpl.swift:24,34). Two paths, one purpose, and nobody has said whether the web variant survives — the same shape of question as §11's two login paths.
No upload operation appears in the reference. The target state as documented describes reading photo state, not writing a photo. Under the absence caution that is not proof the upload is going away — but this card's upload evidence (certifications/photos/upload, ProApi.kt:14-16) remains the only source for the write half, and it is a moderated certification photo rather than an avatar.
Like profile and certifications, this is an oauthToken operation — see Finding 6.
Verdict. Buildable. Which artefact it means is a product decision, and it should be answered once for MYP-02 and MYP-06 together.
Questions: SCOPE-5 (avatar or cert photo) · PADI-4 (photo token scope)
MYP-07 — Email communication preferences
Area 10 Engagement API status: Existing — web · Feasibility: buildable
This entry carries the analysis for the MYP-07 / PREF-02 pair. PREF-02 is not re-analysed; it records only the per-channel granularity question.
Endpoints available.
| Method | Endpoint | Auth |
|---|---|---|
| GET/POST/PUT | {PREFERENCES_URI}/v1/preference[/{id}] · /v1/unsubscribe |
Bearer + affiliate-id |
| GET | {PREFERENCES_URI}/v1/MarketingLanguage/{affiliateType} |
Bearer + affiliate-id |
| POST | prefapi.scubadiving.com/recipient — push to SFMC |
Bearer + affiliate-id |
{PREFERENCES_URI} = https://api.core-prod.padi.com/preference. Named categories: diveAbroad, localDiving, environmentConservation, events, diveCentricTrips, padiCommunications, thirdParty.
Known defect, from the source itself. "The recipient/preference API returns only numeric preferenceId + status; no endpoint returns the email-category labels or descriptions." The app would have to hardcode the labels — brittle, and wrong the moment PADI adds a category.
Security note. A separate unauthenticated path on the same host was verified in #eng-padi-web (2026-08-17): "Calls to the API currently succeed without credentials and without IP restrictions." Not a blocker for this item; worth raising on its own.
Target state — MyPADI Personalised Home reference. Not covered.
The preferences endpoint that appears in the API inventory §11's SwaggerHub spec — GET /preference/v1/preference/{id} — is absent from this reference, and no communication-preference operation replaces it.
Apply the absence caution strictly here: email preferences are not a section of the personalised home, so their absence is expected and says nothing about whether the endpoint survives. This item's verdict is unchanged, and it still rests on the web-captured evidence below.
What it does mean is that the target-state source offers no corroboration for MYP-07 — so of all the items on this card, this is the one whose evidence base has not improved.
Verdict. Buildable now. Ask for a label endpoint before building a hardcoded list. Estimate this once across MYP-07 and PREF-02 unless SCOPE-6 comes back saying PREF-02 means per-channel.
Questions: PADI-8 (category label endpoint) · SCOPE-6 (is this the same item as PREF-02)
MYP-08 — Unified login
Area 01 Identity, Profile & Account API status: Existing — mobile · Feasibility: buildable; multi-realm is the real work
Endpoints available. The full Cognito facade is mobile-proven (Swapi.kt:31-88) — login, refresh, register, confirm, reset, user/exist/legacy, and sso/exchange.
The finding that reshapes this item. Unified login is not one session. It is a token-exchange architecture across three realms — PADI gateway (idToken), store (commercetools session via store-client idToken), Travel (Travel JWT). The idToken expires in 60 minutes and its aud must match the target client.
That is a genuinely new requirement for the unified app. The current PADI app never needed it because it never reached the store or Travel. Adventures had its own login entirely.
Gap. None in endpoints. The work is client architecture: a token broker holding three credential types, refreshing on a 60-minute cycle, bridging the store session.
Target state — MyPADI Personalised Home reference. Covered — and it turns this item from a login question into an architecture one.
| Operation | Yields | Serves |
|---|---|---|
POST /auth/api/oauth/login |
idToken (bearerAuth) |
learning, pro, logbook |
POST /o/oauth/api/token |
access_token (oauthToken), via client credentials |
profile, certifications, codes, catalog, club |
See Finding 6. The second grant needs a Client ID and Secret Key, which cannot ship in a mobile binary — so in the target model the app cannot reach profile, certifications, club, codes or catalog on its own credentials at all. A server-side component must mint and hold that token.
This is the strongest architectural claim in the card, and it arrives from the target state rather than from the current one: the BFF stops being a performance optimisation and becomes a functional prerequisite.
The two login paths discrepancy is unresolved. The reference declares POST /auth/api/oauth/login; the shipped apps call auth/api/login (Swapi.kt:31). Two specs in a row now name the former and neither mentions the latter.
Verdict. Buildable, and larger than "unified login" sounds. Scope it as session architecture, not a sign-in screen.
Questions: SCOPE-7 (does "unified" include Travel and store realms in Q1)
MYP-09 — Consent / Preference governance and orchestration
Area 01 Identity, Profile & Account · Jira E101 API status: Gap · Feasibility: not buildable as specified
This entry carries the analysis for the MYP-09 / PREF-03 pair. PREF-03 is not re-analysed; it records only the data-export and deletion increment.
What it needs. "Single source of truth opt-in / opt-out synchronization across systems · transactional vs marketing classification · blacklist / unsubscribe governance."
Endpoints available. The preference CRUD from MYP-07, and /v1/unsubscribe. That is the mechanism, not the governance.
Gap, and the source names it. MyPADI doc §8.2 lists "Privacy (data export/delete, consent) → Privacy systems not documented." No consent platform appears in any repo or capture. And the language triple-write problem is this item in miniature: three stores, three endpoints, no propagation.
Target state — MyPADI Personalised Home reference. Nothing. No consent, preference-governance, export or erasure operation appears anywhere in the reference.
That is consistent with every other source, and consent is not a dashboard section, so the absence caution applies. But the gap has now survived a fourth independent check: the four repos, the live-tested matrix, the SwaggerHub spec, and this reference.
Verdict. Not buildable as specified. The Low impact / Low effort estimate should be challenged — it describes cross-system orchestration over systems that are undocumented.
Questions: PADI-9 (system of record for preferences and consent) · INT-1 (challenge the estimate)
MYP-10 — Certification path tracker
Area 02 Certifications & eCard API status: Existing — web (unverified fit) · Feasibility: requirement missing
Endpoints available. GET {PRO_API}/journey/routing/othercourses — "Other-courses routing" — is real and web-captured, on the Pro API with Bearer + affiliate-id.
Gap. Not the endpoint. Nobody has said what a "certification path" is: an ordered prerequisite graph, or a flat next-course list. Those are different builds, and journey/routing/othercourses may serve one and not the other. Its name suggests routing, not progression.
Not the same as MYP-12. They read as a pair but resolve to different endpoints: this item is progression ({PRO_API}/journey/routing/othercourses, Pro API), MYP-12 is recommendation (/v2/courses?tag=recommended, learning API). Both are analysed in full because both are separate builds.
Target state — MyPADI Personalised Home reference. Covered — by a different route from the one this card records, and the two imply different builds.
GET /c/certification/certificate/all returns CertificateDefinition for every certification PADI defines. Combined with the diver's own GET /c/certification/affiliate/{affiliateId}, a path tracker becomes a client-side computation: the full syllabus, minus what the diver holds.
This card's current route is server-side — {PRO_API}/journey/routing/othercourses returns what comes next, already reasoned. So there are now two candidate designs:
| Route | Where the progression logic lives | Cost |
|---|---|---|
journey/routing/othercourses |
PADI's server | Cheap client, opaque rules |
certificate/all + the diver's certs |
The app (or the BFF) | Full control, and Axelerant owns the syllabus rules |
Neither is wrong; they are different products. SCOPE-8 (what a certification path is) now has a concrete second option to choose between, which makes it answerable.
One environment wrinkle: this is the single operation the reference still pins to api.global-prod.padi.com while its nine /c/ siblings moved to api.padi.com — the reference's own author calls that a likely oversight.
Verdict. Probably buildable. Unverified until someone states the requirement.
Questions: SCOPE-8 (what is a certification path) · SCOPE-9 (is this distinct from MYP-12)
MYP-11 — Contact QR code (share diver contact)
Area 01 Identity, Profile & Account · Flags post-Q1 API status: Deferred · Feasibility: out of Q1 scope
Sequenced after Q1 2027 by the artefact; its detail entry is an empty stub. eCard records already carry a qrCode field, and CERT-06 (diver-initiated QR) is also deferred — scope them together when they return.
Target state — MyPADI Personalised Home reference. Not covered, as expected — the item is Post Q1 2027 and a contact QR code is not a personalised-home section. No change.
MYP-12 — Next-course recommendations (personalized)
Area 05 Learning & Course Progress API status: Existing — web · Feasibility: buildable; "personalized" undefined
Endpoints available. GET /v2/courses?tag=recommended — returns courseTitle, customCourseId, image.url, Bearer + affiliate-id. A recommendation endpoint exists and is already called by a web front end.
Not the same as MYP-10. Different endpoint, different API, different question: MYP-10 asks what comes next in the syllabus, this asks what should we show this diver. Merging them would be a scope decision, not a technical one.
Gap. None, unless "personalized" means CDP-driven next-best-action, which the Digital Overhaul Proposal names and no endpoint serves. If ?tag=recommended is sufficient, this is buildable today.
Target state — MyPADI Personalised Home reference. This item is downgraded.
The reference lists "the recommended course/specialty modules" explicitly under Not API-backed — its category for sections that "resolve to a link, Drupal content, or the Dive Shop Locator rather than a call."
So the target state does not serve recommendations at all. GET /v2/courses?tag=recommended — the endpoint this card's verdict rests on — does not appear in the reference, and unlike the other absences this one is not silence: the reference affirmatively places the capability outside the API surface.
The 15 September session is consistent: an open action to "clarify specialty definition and course ranking with Ayush (design team)" — a design question, because there are no rules to read off an API.
The nearest thing the reference does provide is GET /p/product/catalog/sku/{sku}, a user-token catalog lookup. That resolves a known SKU; it does not choose one.
So MYP-12 splits by which state you build for. Against today's endpoints it is buildable at the rules-based reading. Against the declared target state it is a gap with a design question in front of it — and PAM-36, its delivery story, sits at Later - Release 2027, which is consistent with the target state not serving it.
Verdict. Buildable at the rules-based reading. Confirm the intent before estimating.
Questions: SCOPE-9 (distinct from MYP-10) · SCOPE-10 (CDP or rules-based)
PREF-01 — Manage preferred language
Area 01 Identity, Profile & Account API status: Existing — mobile · Feasibility: buildable; propagation is broken
Endpoints available. PUT /user/update/language/{email} (mobile-proven), GET globals/SupportedLanguages, GET {PREFERENCES_URI}/v1/MarketingLanguage/{affiliateType}.
The problem, quoted from the source. "Language is stored in 3 places — Cognito custom:language, preference communicationLanguage, and profile languageCommunicationPreference — written by different endpoints, so an edit on one platform doesn't propagate."
So the app can set a language. It cannot set the language. A diver changing it in the app may still receive email in the old one — which is precisely the failure the MyPADI vision promises to fix, quoted in MYP-02's own context note.
The doc raises it as an open question: "What is the system of record for language, and can a single write propagate to Cognito + Salesforce + SFMC + LMS?"
Target state — MyPADI Personalised Home reference. Not covered. No language or preference operation appears. Language is not a personalised-home section, so the absence caution applies and the verdict is unchanged.
The three-store propagation defect in Finding 3 is untouched by the target state — and since the reference's course-detail operation notes that culture "needs to follow the app's selected language rather than being pinned to en-US", the target state inherits the same problem on a new surface.
Verdict. The screen is buildable in a day. Whether it works depends on an unanswered data-ownership question. Do not present this as commit-ready without stating that.
Questions: PADI-10 (system of record for language)
PREF-02 — Manage contact preferences
Area 10 Engagement API status: Existing — web · Feasibility: buildable
Duplicate of MYP-07. Same endpoints, same auth, same missing-labels defect — see MYP-07 for the analysis; it is not repeated here. This entry records only what is specific to PREF-02.
What is specific to it: granularity. "Contact preferences" implies channels; MYP-07's wording implies email only. The preference API serves seven flat categories with no per-channel dimension. PADI Adventures, by contrast, already models per-channel flags (email / SMS / push) across five categories — accountNotifications, reminders, divingOpportunities, adventuresPromotionsUpdates, travelInspirationalNews.
So if PREF-02 means channels, the existing endpoint is the wrong shape and this stops being a duplicate. If it means the same seven email categories, it is MYP-07 under a second name.
Target state — MyPADI Personalised Home reference. Not covered — same position as MYP-07, of which this is the second identifier. No corroboration either way.
Verdict. Buildable on MYP-07's endpoints at the email reading. Unresolved at the multi-channel reading.
Questions: SCOPE-6 (same item as MYP-07, or is per-channel intended) · PADI-8 (category labels)
PREF-03 — Manage privacy and consent options
Area 01 Identity, Profile & Account API status: Gap · Feasibility: not buildable as specified
Same gap as MYP-09, from the diver's side rather than the platform's — see MYP-09 for the analysis; it is not repeated here. This entry records only what is specific to PREF-03.
What is specific to it: deletion and export, not consent. MYP-09 is about keeping opt-ins synchronised. PREF-03 additionally implies data export and account deletion — GDPR subject rights, which no endpoint in any source serves at all.
The current app's deletion path is an email to privacy@padi.com asking an operator to deactivate the account by hand (v1/email/transact). A manual step in a GDPR deletion path is an exposure whichever way this item is scoped, and it does not go away by building a new app.
Target state — MyPADI Personalised Home reference. Nothing — see MYP-09. Export and erasure appear in no source, now including this one.
Verdict. Not buildable. Scope alongside MYP-09, but note that closing MYP-09 does not close this — export and deletion are additional.
Questions: PADI-9 (consent system of record) · INT-2 (manual deletion is a compliance exposure)
Summary
Current state — what exists today
| Status | Items | Count |
|---|---|---|
| Existing — mobile | MYP-03, MYP-08, PREF-01 | 3 |
| Existing — web | MYP-02, MYP-05*, MYP-06, MYP-07, MYP-10, MYP-12, PREF-02 | 7 |
| Gap | MYP-01*, MYP-04, MYP-09, PREF-03 | 4 |
| Deferred | MYP-11 | 1 |
* partial — MYP-05 has status but not billing; MYP-01 has sections but not aggregation.
Ten of fifteen items have an endpoint today. That holds only because step 2 was run — counting what the current mobile apps call would have put eight items in the gap column.
Target state — what the MyPADI Personalised Home reference provides
| Position | Items | Count |
|---|---|---|
| Covered — endpoint named | MYP-02, MYP-03, MYP-04, MYP-05, MYP-06, MYP-08, MYP-10 | 7 |
| Confirmed gap | MYP-01, MYP-09 | 2 |
| Affirmatively not API-backed | MYP-12 | 1 |
| Out of the reference's scope — verdict unchanged | MYP-07, MYP-11, PREF-01, PREF-02, PREF-03 | 5 |
* covered with a material caveat — see each item.
Read the fourth row carefully. Those five are not target-state gaps. The reference covers one screen and names the sections it excludes; their absence carries no information. Only the first three rows are evidence.
Where the two states disagree — the four that matter
- MYP-12 is downgraded. The reference places "the recommended course/specialty modules" under Not API-backed. This is the one absence that is not silence — the capability is affirmatively outside the API surface, and
PAM-36sitting at Later - Release 2027 is consistent with that. - MYP-03's payload collapses. The target
Certificationschema carries 4 fields; the shippedECardInfocarries ~55. A certifications list is not an eCard. If the transition is read as a swap, card rendering loses its data — the likeliest place for this to break something that works. - MYP-08 becomes an architecture item. The target model needs two credentials at once, and the one serving profile, certifications, club, codes and catalog is a client-credentials grant that cannot live in a mobile binary. The BFF stops being an optimisation and becomes a prerequisite — see Finding 6.
- MYP-04 gains a route and loses a proof. Per-section booleans from
/assessmentresultsmake a percentage derivable, which is SCOPE-4's reduced form with an endpoint behind it. No mobile client calls it, butPadiWW/pro.padi.comdoes in production (src/store/modules/course/actions.js:48-70), so the route is live rather than theoretical. The claim that the gap was "proven by field list" is withdrawn — see below.
Gap re-test — three hold, one withdrawn, one correction
Every gap claim was re-tested against the specs, the live-tested matrix, the web capture and the four repos — because we cannot tell PADI something is a gap if they already serve it. The re-test is the API inventory §12.
- Dashboard aggregation (MYP-01) — holds, and the target state enlarges it: 20 operations, six hosts, no composition, plus a persona-identification step that must resolve before the shell can render. Axelerant's BFF to close, not PADI's.
- Consent and privacy systems (MYP-09, PREF-03) — holds, now against a fourth independent source.
- Course completion percentage (MYP-04) — holds as specified, but the "proven by field list" claim is withdrawn. That inference read the iOS decoder for
/v2/courses/{packageId}/detailsas the response shape. ADecodabledeclares only the fields a client uses, so it proves non-consumption, not absence. Three sources now expect progress data to be reachable and none has been observed: the reference'sprogressPercent(self-declared as inferred),UserCourse.percentRemaining(declared, never read, iOS-only), andAssessmentResult.isCompleted(the derivable route — called in production byPadiWW/pro.padi.com, never by a mobile client, and never with its body captured by this study). PADI-31 is the one live call that settles it, and it is now a confirmation rather than a discovery. - ~~Membership billing history (MYP-05)~~ — downgraded to unverified.
POST /pros/graphqlis GraphQL and field absence cannot be asserted without introspecting it.renewalStatusandautoRenewEnabledare served, and the target state addsGET /pros/renewals/info— the first candidate endpoint for the history half.
Two checks now stand between this and a defensible gap list, both minutes of work with a valid credential:
- Introspect
pros/graphql— unchanged, and the target state does not retire it. - Call
/learning/v2/courses/{customCourseId}/assessmentresults— replaces the/detailscheck, which was answered incompletely.
And four risks that are not about missing endpoints
- Language does not propagate across its three stores (PREF-01), and the target state inherits it — the reference's own note that
culturemust follow the app's language rather than being pinned toen-USis the same defect on a new surface. - Cert and Photo reject the SSO token — 401 (MYP-03, MYP-06). Finding 6 explains why: in the target model they do not take that token. This stops being a bug report and becomes a design constraint.
- Two unresolved path pairs — login (
auth/api/oauth/loginvsauth/api/login) and certification photo (/p/photos/api/vs/p/photos/web/). Two specs in a row have named one of each and ignored the other. - An unknown subset of these endpoints is being rebuilt for the legacy sunset (PADI-1).
PADI-1 is still the one to raise first, and this card is now the best argument for it: there are two documented API surfaces for the same dashboard, produced eight days apart by the same team, and they disagree in four places. Neither is a PADI contract. Every Existing verdict above — in both states — is only as durable as PADI's answer.