---
title: "Endpoint Reference"
slug: applicant-portal-endpoints
section: Portals
group: Applicant Portal
status: stable
description: "The verified API surface of both applicant portal backends, extracted from source on 2026-07-04 (both repos on the development branch)."
last-updated: 2026-07-04
---

# Endpoint Reference

# Applicant Portal — Endpoint Reference

The verified API surface of both applicant portal backends, extracted from source on 2026-07-04 (both repos on the development branch). PPPToo is the live rebuild; the v1 prototype remains deployed in dev but is reference-only.

See also: PPPToo Architecture and the Globus Portal API Reference .

## Two Backends, One Journey

PPPToo (Alliance-Strategies/PPPToo, services ppp2-participant-server/-client) is the stateless BFF: every applicant-scoped call is proxied to the Globus Hub with a static x-api-key plus the caller's Keycloak sub as X-Applicant-User-Id; Globus enforces row ownership. The Globus envelope is unwrapped centrally, and upstream 401s are rewritten to 403 so the SPA never self-logs-out on a Globus denial. v1 (ParticipantPortalPrototype, service applicant-participant-server) is the legacy stateful portal with its own Postgres, form-state and queues.

## PPPToo BFF Surface (/api/v1)

| Method | Path | Auth | Upstream | Purpose |
| --- | --- | --- | --- | --- |
| GET | /api/v1/health | Public | none (local checks) | 7-indicator health; keeps the v1 monitoring contract |
| POST | /api/v1/auth/login | Public | Keycloak token endpoint (direct grant) | Same-origin login; returns accessToken in body, no cookies |
| PUT | /api/v1/auth/password | Bearer JWT | Keycloak Admin API | Self-service password change on the caller's own account |
| POST | /api/v1/onboarding/validate-token | Public (magic-link token) | POST /api/portal/session + GET /api/applicants/:id | Non-burning identity preview for the set-password page |
| POST | /api/v1/onboarding/complete | Public (magic-link token) | POST /api/portal/session, then Keycloak upsert | Create-or-claim the Keycloak account bound to the Globus applicant |
| GET | /api/v1/applications | Bearer JWT | GET /api/portal/applications (+programs, applicant) | List caller's applications, enriched with program + identity |
| GET | /api/v1/applications/:id | Bearer JWT | GET /api/portal/applications/:id | Application detail |
| GET | /api/v1/applications/:id/steps | Bearer JWT | GET .../step-progress + GET /api/portal/step-library | Step progress joined with step library (render-ready steps) |
| GET | /api/v1/applications/:id/metadata | Bearer JWT | GET /api/portal/applications/:id/metadata | Application metadata |
| POST | /api/v1/applications | Bearer JWT | POST /api/portal/applications | Create application (AP-1: pinned workflow when available) |
| POST | /api/v1/applications/:id/steps/:stepKey/submit | Bearer JWT | POST .../steps/:stepKey/submit | Submit a workflow step's responses |
| GET | /api/v1/programs | Bearer JWT | GET /api/portal/programs | Program catalogue |
| GET | /api/v1/programs/:programId/config | Bearer JWT | GET /api/portal/programs/:id/config | Per-program portal config (step ordering, brandId) |
| GET | /api/v1/applications/:id/documents | Bearer JWT | Globus portal document routes (header-auth) | List uploaded documents (Globus-backed since 2026-07-04; +GET required for the catalogue) |
| POST | /api/v1/applications/:id/documents | Bearer JWT | Globus document upload (header-auth) | Upload document (max 15MB; PDF/JPEG/PNG/WebP) |
| GET | /api/v1/applications/:id/documents/:docId/download | Bearer JWT | Globus download; legacy ppp2/ ids fall back to GCS read-only | Download a document (+GET :docId/versions) |
| POST | /api/v1/applications/:id/skill-certifications | Bearer JWT | Globus document upload (documentType=skill-certification) | Skills and Hobbies certification upload (AP-5) |
| GET | /api/v1/checkins/recurring/current-season | Bearer JWT | GET /api/checkins/recurring/applicant/:id/current-season | Active operational cycle for the applicant's program |
| GET | /api/v1/checkins/recurring/season/:seasonId | Bearer JWT | GET /api/checkins/recurring/applicant/:id/season/:seasonId | Check-ins for an explicit season |
| GET | /api/v1/checkins/templates/:programId | Bearer JWT | GET /api/portal/check-in-templates/:programId | Per-program check-in template |
| GET | /api/v1/me/context | Bearer JWT | applications + programs + applicant profile (composed) | Bootstrap tuple: applicant, active application, program, brand, season |
| POST | /api/v1/ai/text/review | Bearer JWT | POST /api/openai/review-text | AI 'Review My Description' |
| POST | /api/v1/ai/text/enhance-caption | Bearer JWT | POST /api/openai/enhance-caption | AI caption enhancement |
| POST | /api/v1/ai/text/review-showcase | Bearer JWT | POST /api/openai/review-showcase | AI showcase review |
| POST | /api/v1/ai/text/suggestions | Bearer JWT | POST /api/openai/suggestions | AI writing suggestions |
| POST | /api/v1/analytics/events | Public (204) | none (structured logs) | Analytics event sink |
| GET | /api/v1/brands/:id/theme | Bearer JWT | GET /api/portal/brands + /api/portal/branding?slug= | Brand theming; host-relative logo paths made absolute |

### PPPToo Auth Model

Login is a Keycloak direct-grant on the public client front-react-applicant-portal-app; tokens carry aud: back-node-applicant-portal-app via the realm audience mapper. The access token is returned in the response body only — no cookies, because the SPA and API are cross-site and third-party cookies are blocked in Chrome. Protected routes verify the Bearer token (issuer + audience) with Passport JWT. Public routes: health, login, both onboarding routes (the magic-link token is validated by Globus), analytics.

## v1 Portal Surface (legacy)

> **Reference-only**
>
> The v1 portal (ParticipantPortalPrototype, pinned reference d5052d7c4) is being replaced by PPPToo with a hard cutover at parity. Do not build new integrations against this surface.

| Route group | Representative endpoints (under /api/v1) | Purpose |
| --- | --- | --- |
| auth | POST /auth/login, GET /auth/me, POST /auth/logout, GET /auth/flow, PUT /auth/password, password-reset family | Session login (cookie + token), flow-state routing, password management |
| onboarding / registration / otp | onboarding/*, onboarding/check-user, registration/*, otp/* | Magic-link onboarding, self-registration, one-time passcodes |
| applicants / my-application | applicants/*, my-application/*, applicant-profile/* | Profile reads/writes and the 'my application' aggregate |
| dashboard reads | applications, programs, progress, application-metadata | Portal dashboard reads |
| portal reads + documents | portal/*, documents list/upload/download, skill-certifications | Portal-scoped application, document and cert pipelines |
| branding / tenant | brands/*, tenant/branding/*, tenant/* | Brand theming and tenant metadata |
| ai / notifications / users | ai/text/*, notifications/*, users/* | AI text assists, notifications, user management |
| form-state (stateful) | form-state routes | Server-persisted drafts — the key stateful pattern PPPToo removed |
| webhooks / admin | webhooks/*, admin/queues(*) | Inbound webhooks and Bull-Board queue admin |

### v1 Auth Model

POST /auth/login sets an HttpOnly session_token cookie and returns the same Keycloak token as accessToken in the body. The guard extracts Bearer first, cookie second, with full JWKS RS256 verification (issuer + audience). Note the cross-domain cookie path is non-functional in Chrome — the deployed portal relies on the token path (kc_access_token in sessionStorage). A few routes (password-reset family) use service API-key auth instead.
