---
title: "Endpoint Reference"
slug: applicant-portal-endpoints
section: Portals
group: Applicant Portal
status: stable
description: "This reference covers **`Alliance-Strategies/PPPToo`** (`development` branch) — the live Participant Portal v2."
last-updated: 2026-10-07
---

# Endpoint Reference

This reference covers **`Alliance-Strategies/PPPToo`** (`development` branch) — the live Participant Portal v2. The predecessor repository, `ParticipantPortalPrototype`, is **archived and read-only**; its Express service, its `/api/v1` reverse proxy to a separate form-engine BFF, and its route registry are retired and are not documented here. [1] [2]

> **Endpoint-reference rule**
>
> The catalog below is enumerated from the NestJS controllers in `server/src`. It is accurate to the `development` branch at time of writing. Before building an integration, confirm the handler still exists and confirm the corresponding Hub authorization contract — the BFF is a proxy, so the Hub contract is the ultimate authority on what an applicant may read or write. [3]

## Service Boundary

| Boundary | Current implementation evidence | Integration guidance |
| --- | --- | --- |
| Browser client | React 18 + Vite SPA. Nine authenticated pages behind `RequireAuth`, three public entry routes, one public referee route, three legal pages. [4] | UI paths are not API endpoints; use them to identify product journeys, then trace the client request. |
| PPPToo BFF | **Stateless NestJS 11 service.** No database, no migrations, no seeding, no scheduled tasks. Every controller resolves against the Globus Hub through `@alliance-strategies/globus-sdk`. [5] [6] | There is no "local `/api/*` operations" tier and no `BFF_TARGET` proxy. The BFF *is* the backend. |
| Hub / Globus integration | `portalTokenContextMiddleware` forwards the applicant's Bearer to the Hub as `X-Portal-Access-Token`. [6] | Hub credentials and applicant context are a server-side concern, never browser secrets. |

## PPPToo BFF Catalog (/api/v1)

Every route below is prefixed `/api/v1` unless listed in the following subsection. [6]

| Area | Method | Path |
| --- | --- | --- |
| Auth | POST | `/auth/login` |
| Auth | PUT | `/auth/password` |
| Auth | POST | `/auth/logout` |
| Auth | POST | `/auth/forgot-password` |
| Auth | PUT | `/internal/applicants/:portalUserId/email` |
| Auth | GET | `/internal/applicants/:portalUserId/email-operations/:operationId` |
| Onboarding | POST | `/onboarding/validate-token` |
| Onboarding | POST | `/onboarding/complete` |
| Context | GET | `/me/context` |
| Profile | PATCH | `/me/profile` *(editable fields: `phone`, `preferredName` — see note below)* |
| Profile photo | POST · GET | `/me/photo` |
| Programs | GET | `/programs` |
| Programs | GET | `/programs/:programId/config` |
| Applications | GET | `/applications` |
| Applications | POST | `/applications` |
| Applications | GET | `/applications/:id` |
| Applications | POST | `/applications/:id/passport-name-sync` *(proof-only body; fail-closed — see note below)* |
| Applications | GET | `/applications/:id/steps` |
| Applications | GET | `/applications/:id/journey` |
| Applications | GET | `/applications/:id/available-flows` |
| Applications | POST | `/applications/:id/flows/:applicationType` |
| Applications | GET | `/applications/:id/identity-annotations` |
| Applications | GET | `/applications/:id/step-access` |
| Applications | GET | `/applications/:id/steps/:stepKey` |
| Applications | GET | `/applications/:id/steps/:stepKey/review-feedback` |
| Applications | POST | `/applications/:id/steps/:stepKey/submit` |
| Applications | POST | `/applications/:id/steps/:stepKey/draft` |
| Criminal background | GET | `/applications/:id/criminal-background/status` |
| SWT status | GET | `/applications/:id/swt/enrollment-proof/status` |
| SWT status | GET | `/applications/:id/swt/academic-verification/status` |
| Documents | POST | `/documents` |
| Documents | GET | `/applications/:id/documents` |
| Documents | GET | `/applications/:id/documents/required` |
| Documents | POST | `/applications/:id/documents` |
| Documents | GET | `/applications/:id/documents/:docId/versions` |
| Documents | GET | `/applications/:id/documents/:docId/download` |
| Documents | DELETE | `/applications/:id/documents/:docId` |
| Document packages | GET | `/document-packages/artifacts/:artifactId/download` |
| Document packages | POST | `/document-packages/:requirementId/applicant-translation` |
| Documents | POST | `/applications/:id/skill-certifications` |
| Showcase media | POST | `/applications/:id/showcase-photos` |
| Showcase media | GET | `/step-media/:mediaId/file` |
| Showcase media | DELETE | `/step-media/:mediaId` |
| Terms | GET | `/applications/:id/terms/download` |
| Agreements | GET | `/agreements/required` |
| Agreements | POST | `/agreements/acceptances` |
| References | GET | `/applications/:id/references` |
| References | POST | `/applications/:id/references` |
| References | POST | `/applications/:applicationId/references/:id/replace` |
| References | POST | `/applications/:applicationId/references/:id/resend` |
| References | GET | `/references/:id/pdf` |
| References (public) | GET | `/references/lookup` |
| References (public) | POST | `/references/respond` |
| References (public) | POST | `/references/media` |
| References (public) | GET | `/references/media/:id/file` |
| Reference templates | GET | `/applications/:applicationId/reference-templates/:templateKey/pdf` |
| Arrival information | GET | `/arrival-information` |
| Arrival information | POST | `/arrival-information/confirm` *(specified only — no BFF route at development HEAD; see note below)* |
| Check-ins | GET | `/checkins/recurring/current-season` |
| Check-ins | GET | `/checkins/recurring/season/:seasonId` |
| Check-ins | GET | `/checkins/templates/:programId` |
| Notifications | GET | `/notifications` |
| Notifications | GET | `/notifications/count` |
| Notifications | PATCH | `/notifications/:id/read` |
| Notifications | POST | `/notifications/read-all` |
| Branding | GET | `/brands/:id/theme` |

> **Arrival information & confirmation**
>
> `GET /arrival-information` proxies the Hub's `GET /api/portal/arrival-information` (flat 200; `{ available: false, reason: "NO_CONFIRMED_PLACEMENT" }` pre-placement). Under the camp-wide arrivals decision the response carries the camp record verbatim plus `airport_transportation`, `arrival_revision`, and confirmation state, and the companion `POST /arrival-information/confirm` is specified to record the participant's explicit, revision-bound confirmation but is not implemented in the BFF at development HEAD (only `GET /arrival-information` exists) — see the [Host Arrivals contract](/docs/host-arrivals) for the frozen shape, idempotency, and staleness rules. Participants review and confirm only; there is no participant edit surface. The same `GET /arrival-information` also backs the `/travel` **Local Travel** page in the client.

> **Hub-side brand config**
>
> The Applicant Portal also fetches a brand's public configuration by ID from the Hub via `GET /api/portal/brands/:id` (for brand-specific theming/content without embedding brand data in the initial bundle) — see the [Globus Portal API Reference](/docs/globus-portal-api). Not a PPPToo BFF `/api/v1` route.
| Legal | GET | `/legal/:slug` |
| Analytics | POST | `/analytics/events` |
| AI text | POST | `/ai/text/review` |
| AI text | POST | `/ai/text/enhance-caption` |
| AI text | POST | `/ai/text/review-showcase` |
| AI text | POST | `/ai/text/suggestions` |
| AI documents | POST | `/ai/documents/extract-passport` |
| AI documents | POST | `/ai/documents/extract-certificate` |
| Health | GET | `/health` |
| Health | GET | `/version` *(running build — see note below)* |

### Step, Journey and Review Projections

The application read routes are projections over Hub data, built from the same ordered step list.

- **Phase grouping.** `GET /applications/:id/steps`, `/journey` and `/steps/:stepKey` carry `phaseKey`, `phaseName` and `phaseOrder` on every step, joined from the application's published program configuration on the Hub. `phaseKey` comes from the authored config, then from the progress row's `phase_key`, then `general`; `phaseName` and `phaseOrder` are `null` when the config has no matching phase or cannot be read. Steps are ordered by `phaseOrder` then `stepOrder` only when every step has a numeric `phaseOrder`; otherwise plain `stepOrder` is used.
- **Journey position.** Each projected step also carries `journeyPositionKey`: the progress row's `journey_position_key` (or `journeyPositionKey`) value first, then the authored program config value, otherwise `null`.
- **Review state on steps.** Each step can carry `needsAction`, `rejectionMessage`, `rejectionCode` and `documentReviewStatus`. `GET /applications/:id/journey` (the lean projection, without `stepData` or `stepConfig`) joins these server-side from the Hub step-access snapshot, which is authoritative: a rejected step stays flagged in the journey, and if the step-access read fails the request fails rather than silently dropping the rejection state. Ownership of the application is checked before that read. The full `/steps` projection takes the review fields from the progress row.
- **`GET /applications/:id/step-access`** returns one entry per step: `stepKey`, `accessLevel`, `reason`, `bannerMessage`, `unlock_at`, `needsAction`, `rejectionMessage`, `rejectionCode`, `documentReviewStatus`, `canApplicantEdit`, `canApplicantSubmit`, `unlockConditions`, plus `reviewItems` and `reviewFeedback` when the Hub supplies them. Only applicant-visible review items cross the BFF: items whose status is `changes_requested` or `applicant_resubmitted`, excluding internal, private and draft-session items. Each review item carries `id`, `targetType`, `targetItemId`, `targetLabel`, `fieldPath`, `status`, `comment` and `requestedAt`.
- **`GET /applications/:id/steps/:stepKey/review-feedback`** returns `{ stepKey, overallComment, items }` for one step under the same visibility filter; a published `reviewFeedback` block (even with no items) is authoritative, and the legacy review items are used only when no block exists. An unknown step returns 404.

### Caching

These applicant-facing reads emit `Cache-Control: no-store`: `GET /applications/:id`, `/steps`, `/journey`, `/step-access`, `/steps/:stepKey`, `/steps/:stepKey/review-feedback`, `/applications/:id/identity-annotations`, and `GET /applications/:id/criminal-background/status` (a polling target, so every poll must reach the Hub). The `GET /applications` list does not set the header.

### Route Notes

- **`POST /documents`** (flat, application-agnostic upload used by the form engine) attaches the file to the application named in the `X-Application-Id` request header (must be a UUID; otherwise 400). Globus enforces ownership on the upload. Without the header the BFF resolves the applicant's active application, returning 400 if none exists and 409 if the only match is terminal (`stageIsTerminal` is true, or the status is `withdrawn`, `rejected`, `cancelled`, `lapsed` or `completed`), rather than filing to the first application on record.
- **Step media uploads (Hub).** `POST /api/upload-photo` and `POST /api/upload-criminal-background` take a multipart `file` plus `applicationId` (and optional `stepKey` and `fieldName`) from a session holding `applications.manage`, and answer with the flat body `{ url, originalName }`, where `url` is a Globus proxy path. Photos accept JPEG or PNG up to 5 MB (`415 UNSUPPORTED_PHOTO_FORMAT`, `413 PHOTO_TOO_LARGE`, `502 PHOTO_STORAGE_FAILED`). Criminal-background documents accept PDF, JPEG, PNG or Word files up to 20 MB (`415 UNSUPPORTED_DOCUMENT_FORMAT`, `413 DOCUMENT_TOO_LARGE`, `502 DOCUMENT_STORAGE_FAILED`). Both return `400 NO_FILE` when no file is sent and `404` for an unknown application, and a multer size abort is mapped to the same `413` code. Apart from the `404`, which uses the standard error envelope, rejections are flat `{ error, code }`. Storage is GCS only with no local fallback, so a failed write is reported rather than saved elsewhere.
- **Criminal-background translations.** `GET /applications/:id/criminal-background/status` returns the Hub's document-package list for the application verbatim (processing status, original and translation artifact references). `GET /document-packages/artifacts/:artifactId/download` streams an original or AI-translated artifact; the Hub checks ownership and answers a non-disclosing 404 for unknown or foreign artifacts. When a record is held as `awaiting_applicant_translation` (unsupported language), `POST /document-packages/:requirementId/applicant-translation` uploads the applicant's own English version (multipart `file`, standard upload policy); the Hub owns ownership and awaiting-state checks (404, or 409 `TRANSLATION_FALLBACK_NOT_AVAILABLE`).
- **Document withdrawal (Hub).** `DELETE /api/portal/applications/:appId/documents/:docId` (verified portal token, application ownership checked) withdraws a document that has not passed review. It returns `409` with the body `{ error, code }` and `code` `DOCUMENT_NOT_DELETABLE` when the document is approved (state `approved`, or the legacy `verified`), and `SIGNED_AGREEMENT_NOT_DELETABLE` for a signed-agreement artifact whatever its state; a document from another application returns `404`. A police-clearance (criminal background) document retires its active package record but keeps its artifacts, other documents also lose their GCS object on a best-effort basis, and success returns `{ success: true, id }` in the standard envelope.
- **SWT status.** `GET /applications/:id/swt/enrollment-proof/status` returns `{ applicationId, assessment }` (assessment `null` until the Hub has a record) and `GET /applications/:id/swt/academic-verification/status` returns `{ applicationId, vacationVerification, academicVerificationRequests }`. Both are forwarded verbatim and read-only: no eligibility is computed in the BFF, and verification requests are created by staff.
- **Reference templates.** The blank-template PDF route is application-scoped (`/applications/:applicationId/reference-templates/:templateKey/pdf`): the Hub verifies the portal token owns the application and resolves the program brand, so callers cannot choose a brand. The controller also passes `applicantSub(req)` to the Globus service call that fetches the PDF. The earlier generic `/reference-templates/:idOrKey/pdf` route no longer exists; the client rewrites template download links with the active application id.
- **Step submit and draft `effectiveContractDigest`.** The request bodies of `POST /applications/:id/steps/:stepKey/submit` and `POST /applications/:id/steps/:stepKey/draft` can each carry an optional `effectiveContractDigest`. The controller reads it with `parseContractDigest`, and the parsed digest is passed to `globus.submitStep` on submit and to `writes.saveDraft` on draft.
- **Showcase photos.** `DELETE /step-media/:mediaId` removes an applicant's showcase photo (204); storage and the applicant-scoped delete are owned by Globus.
- **Legal pages.** `GET /legal/:slug` is unauthenticated on the BFF (the sign-in screen links to it) and proxies the Hub route `GET /api/portal/brands/:slug/legal`, which is keyed by brand slug and authenticated with the `integrations.portal` API key. It returns `{ slug, name, policies, contact }`: `policies` holds only the currently published disclaimer and privacy policy, and `contact` holds `website`, `contactEmail` and `contactNumber`. Unauthored policies and unset contact fields are `null` with a `200`; an unknown slug returns `404`, which the BFF passes through.
- **Structured errors.** The BFF reports machine-readable rejections as `{ error: { code, message, ...extra } }`, built by the `fail(status, code, message, extra)` helper in `server/src/common/http-errors.ts`. A global `AllExceptionsFilter` forwards every `HttpException` with its status and body intact, backfilling a message only if one is missing (a bare-string body is wrapped as `{ message }`), and turns any other unexpected error into a generic applicant-safe 500 (the full error is logged server-side only). On the client, `ApiError.code` is mapped to applicant-facing copy by `client/src/lib/error-codes.ts` (for example `STEP_LOCKED`, `AGREEMENT_VERSION_STALE`, `DUPLICATE_REFEREE`, `MAX_REFERENCES`, `COOLDOWN`, `LIMIT`). A bare SCREAMING_SNAKE_CASE string in `body.error` (the Hub reference-management convention, for example `{ error: 'DUPLICATE_REFEREE', submissionId }`) is recognised as the code.
- **`POST /auth/login` login evidence.** After a successful password grant, the BFF calls `globus.recordSuccessfulLogin` with the access token, in a loop of two attempts. Only this successful password-grant path emits login evidence. If delivery does not succeed, the BFF logs `portal_login_evidence_delivery_failed` with `coverage: 'partial'`.
- **`POST /auth/forgot-password` no-account path.** The `email` body field is trimmed and lowercased before use. If the outcome of the Keycloak reset-email attempt is `no_account`, the BFF calls `requestInvite` with that email, which calls `globus.requestPasswordResetInvite`. A failure of that call is logged as a warning (`[auth] forgot-password invite request failed`).
- **`PATCH /me/profile`** accepts only `phone` (E.164, stripped of formatting) and `preferredName` (non-empty, at most 100 characters; `null` clears it). Other keys are dropped, and a body with nothing editable returns 400. Legal first and last name are not editable here. The Hub route behind this endpoint is wider: `GET /api/integrations/applicant-portal/profile/by-id/:id` (like the by-email and by-portal-user reads, which use the same profile builder, while `GET /api/portal/applicant/profile` returns it at the top level of its response) returns `preferredName` inside its `profile` object, and `PATCH` on the same path accepts it (a non-empty string or `null`, alongside `firstName`, `lastName`, `phone` and `profileImageUrl`; any other key is a 400). It is stored on the applicant record as `preferredName` and is also synced onto the applicant from the `preferredName` field of the Personal Information steps. The passport authoritative-identity step never writes it: that step sets only the legal first name, last name and date of birth (plus its identity-source bookkeeping).
- **`POST /ai/documents/extract-passport` size gate.** After downloading the document, the controller compares the raw file size (`arrayBuffer.byteLength`) with `MAX_PASSPORT_FILE_SIZE_BYTES` before the base64 encode. A file over the limit is not extracted: the controller throws a `BadRequestException` with the body `{ success: false, data: null, error: 'OCR_PAYLOAD_TOO_LARGE', message }`. The upload itself is unaffected, and the message asks the applicant to enter the passport details manually.
- **`POST /applications/:id/passport-name-sync`** takes only a `proof` body (raw name fields are rejected with 400). After ownership and proof checks it always answers 409 `PASSPORT_FINAL_SAVE_REQUIRED` and writes nothing: passport details are applied to the profile only by the Hub when the passport step is saved and submitted, never from extraction alone.
- **Applicant photo error codes (Hub).** The staff photo read `GET /api/applicants/:id/photo` (API key scope `applicants.read`; any of `applicant.view`, `applicant.profile.view` or `applicants.list.view`) streams the resized photo (`?s=` sets the size, `?s=full` returns the original) and reports failures as `{ error, code, applicantId }`: `404 PHOTO_LOCKED_EMPTY` when `?applicationId=` names an application whose identity photo is locked with no photo, `404 PHOTO_PLACEHOLDER` when the stored image is under 500 bytes, and `502 GCS_FETCH_FAILED` when the object cannot be fetched from storage. With no stored photo it returns `404 PHOTO_NOT_SYNCED` (applicant linked to a portal user) or `404 PORTAL_USER_NOT_LINKED`. The portal-session twin `GET /api/portal/applicant-photos/:id` uses the envelope `{ error: { code, message, details, traceId } }` with `PHOTO_PLACEHOLDER`, `GCS_FETCH_FAILED` and `PHOTO_NOT_SYNCED`, and answers `404 APPLICANT_SCOPE_MISMATCH` when `:id` is not the session applicant.
- **Identity email correction (private, Globus → Portal).** `PUT /api/v1/internal/applicants/:portalUserId/email` (body `operationId`, `expectedEmail`, `newEmail`) changes an applicant's sign-in email via an idempotent compare-and-set. It returns `409` if `expectedEmail` does not match, `newEmail` is taken, or the `operationId` is reused with different inputs. `GET /api/v1/internal/applicants/:portalUserId/email-operations/:operationId` resolves the outcome of an ambiguous attempt, returning a status of `applied`, `superseded`, or `not_found` without exposing the email. Both routes use the shared machine credential in the `x-api-key` header, not an applicant Bearer.
- **Email correction error codes.** On the Portal, the 409 body carries `code`: `OPERATION_ID_REUSED` (the `operationId` was already applied with a different `expectedEmail` or `newEmail`), `EMAIL_CHANGED_AFTER_OPERATION` (the `operationId` was applied but the login email has since changed away from `newEmail`), `EXPECTED_EMAIL_MISMATCH` (the current login email is not `expectedEmail`) and `DUPLICATE_EMAIL` (another portal account already uses `newEmail`). Replaying the same `operationId` with the same inputs returns `applied` again. A successful `PUT` answers `{ operationId, status: 'applied', replayed }`, where `replayed` is true for such a replay; an unknown `portalUserId` is `404`. The Hub caller is `POST /api/admin/applicants/:applicantId/change-email` (`applicant.manage`; session, or API key with `integrations.portal` and `admin.write`). For an activated applicant it updates the linked portal login first, and saves nothing in Globus if that fails. Its errors: 409 `DUPLICATE_EMAIL` when another applicant owns the email; 409 `CONFLICT` with `details.code` `PORTAL_IDENTITY_LINK_REQUIRED` (activated applicant with no linked portal identity), `PORTAL_IDENTITY_CONFLICT` (the portal answered 409) or `APPLICANT_EMAIL_CHANGED_CONCURRENTLY` (the applicant's email changed while the correction was being saved); 502 `INTERNAL_ERROR` with `details.code` `PORTAL_IDENTITY_UPDATE_FAILED` (the portal update failed; the identity client's own code, for example `PORTAL_IDENTITY_OUTCOME_AMBIGUOUS`, is passed through when it raised one) or `PORTAL_IDENTITY_COMPENSATION_FAILED` (the correction failed after the portal login was addressed and the compensating revert on the portal also failed, in which case the identity client's own code is passed through if it raised one; the message says the correction needs reconciliation and `details.operationId` names the operation).
- **Step save retry (client).** When a step submit fails with no HTTP response at all (the client error classifier's `network` code), the portal waits 1.5 seconds (`NETWORK_RETRY_DELAY_MS`) and resubmits exactly once. Structured rejections (`STEP_LOCKED`, 4xx, 5xx) are never retried. Failed saves emit a `step_error` analytics event carrying the classifier code, on both the form-engine and the Showcase manual-save paths. Reads time out after 20 seconds, while writes have no client-side timeout by design, so a slow document submit is not aborted mid-flight.
- **`GET /version`** reports the running server commit as `{ sha, environment, service }`. `sha` is read from `GIT_SHA`. `environment` is the deployment environment tag (development, QA or prod) read from `PPP2_ENVIRONMENT`. Both fall back to `unknown` when unset. `service` is the fixed human-readable name `ppp2-participant-server`.

### Unprefixed Compatibility Routes

Three routes are **excluded from the `api/v1` global prefix** and served at their bare paths:

- `POST /api/national-window/get`
- `POST /api/national-window/list`
- `POST /api/eligibility/calculate`

The compiled `useEligibilityValidation` hook inside `form-engine-portable` calls these paths directly. The bundle is shared with BackOfficePrototype, which has no global prefix at all, so the paths cannot be changed portal-side. `EligibilityController` registers them unprefixed to match. Treat this as a fixed contract with the form engine. [6]

## Authentication and Request Context

PPPToo uses **Bearer tokens held in `sessionStorage` — no cookies**. `client/src/lib/api.ts` is the sole API client and token store. A `401` on any path other than `/auth/login` is treated as an expired session and clears the token. When the client reads the token payload locally, it translates the base64url characters `-` and `_` back to `+` and `/`, pads the segment, and decodes the bytes as UTF-8 before parsing the JSON. Route protection is a `RequireAuth` component in `client/src/App.tsx` that redirects to `/sign-in` when no token is present. [4] [7]

There is no `SimpleAuthProvider`, no `/auth/start` or `/auth/callback` route, no return-path sanitisation, and no query-string token handoff — those belonged to the archived predecessor.

| Context | What the `development` branch verifies | What must be traced separately |
| --- | --- | --- |
| Browser session | `sessionStorage` Bearer, `RequireAuth` redirect, CORS with `credentials: false`. [4] [6] [7] | Deployment-specific `CORS_ORIGINS` and Keycloak client configuration. |
| BFF request path | Global prefix `api/v1`; `20mb` JSON limit on `/api/v1/applications`, `100kb` everywhere else. [6] | Per-controller validation and error mapping. |
| Hub call | Applicant Bearer forwarded as `X-Portal-Access-Token` via `globus-sdk`. [6] | The exact Hub credential/context form applicable to one consumer route. |
| Realm integrity | `assertRealmInvariants()` fails the boot if the live Keycloak realm has drifted from the Terraform baseline. [6] | Terraform realm definition in `terraform/keycloak`. |

## Client Journeys

| Journey | Client routes | Backing BFF area |
| --- | --- | --- |
| Sign-in and recovery | `/sign-in`, `/forgot-password` | `/auth/*` |
| Magic-link onboarding | `/onboarding` | `/onboarding/*` |
| Dashboard | `/dashboard` | `/me/context`, `/applications` |
| Application work | `/application` | `/applications/:id/steps*` |
| Documents | `/documents` | `/applications/:id/documents*`, `/documents` |
| Check-ins | `/check-ins` | `/checkins/*` |
| Notifications | `/notifications` | `/notifications/*` |
| Profile and settings | `/profile`, `/settings` | `/me/photo`, `/me/profile`, `/auth/password` |
| Local Travel | `/travel` | `/arrival-information` |
| Arrival information | `/arrival-information` | `/arrival-information` |
| Legal | `/privacy-policy`, `/disclaimer`, `/contact` | `/legal/:slug` |
| Referee (public) | `/reference/respond` | `/references/lookup`, `/references/respond`, `/references/media` |

## Retired Reference Material

Earlier revisions of this page described an Express application service with local migrations, program seeding, scheduled tasks, port 5000, and a same-origin `/api/v1` reverse proxy to `BFF_TARGET`. Those facts belonged to `ParticipantPortalPrototype` and are retired — that repository is archived. Client routes such as `/my-application`, `/my-documents`, `/my-payments`, `/monthly-checkins`, `/sevis-checkin`, `/my-evaluations`, `/travel-requests/:applicationId`, `/j2-*`, `/programs/*`, `/apply/:programId`, and `/register/:programId` do not exist in PPPToo. [1] [2]

## Related Nexus References

| Page | Use it for |
| --- | --- |
| [PPPToo — Participant Portal v2](/docs/pppto-architecture) | Current system architecture, service boundary, and bootstrap detail. |
| [Globus Portal API Reference](/docs/globus-portal-api) | Hub-side portal/session/integration/partner route and credential contracts. |
| [References Flow](/docs/references-flow) | Reference-specific submission behavior. |
| [Portal Overview](/docs/portal-boundaries) | Cross-portal responsibility map. |

## References

[1]: https://github.com/Alliance-Strategies/PPPToo "Alliance-Strategies/PPPToo — Participant Portal v2 (replaces the archived ParticipantPortalPrototype)"
[2]: https://github.com/Alliance-Strategies/PPPToo/blob/development/package.json "PPPToo root workspace manifest"
[3]: /docs/globus-portal-api "Globus Portal API Reference"
[4]: https://github.com/Alliance-Strategies/PPPToo/blob/development/client/src/App.tsx "PPPToo client route registry and RequireAuth guard"
[5]: https://github.com/Alliance-Strategies/PPPToo/blob/development/server/package.json "PPPToo BFF manifest"
[6]: https://github.com/Alliance-Strategies/PPPToo/blob/development/server/src/main.ts "PPPToo BFF bootstrap"
[7]: https://github.com/Alliance-Strategies/PPPToo/blob/development/client/src/lib/api.ts "PPPToo API client and token store"
