Client & API
Build a two-part application: a client app that obtains user consent via OAuth2, and a resource API that validates access tokens and enforces scopes. This demonstrates the full three-party OAuth2 architecture.
Separating the client and API into two services is the standard OAuth2 architecture for third-party integrations. The client never sees the user’s Paylent password — it only receives an access token with specific scopes. The API doesn’t trust the client directly; it validates the JWT signature against Paylent’s public keys via JWKS. This means the API can verify tokens without making a network call to Paylent on every request.
Custom scopes like read:documents and write:documents give you fine-grained control over what each client can do. When the user logs in, they see exactly what permissions the app is requesting and can make an informed decision. This is the same consent model used by GitHub, Google, and other OAuth providers — and it’s the right pattern whenever a third-party app needs to act on behalf of a user.
What you’ll build
Section titled “What you’ll build”- A client (port 3000) that authenticates users via Authorization Code + PKCE and calls the API with access tokens
- A resource API (port 4100) that validates JWT tokens against Paylent’s JWKS endpoint and checks scopes
Prerequisites
Section titled “Prerequisites”- Paylent running locally (
mix phx.server) - Example data loaded
- Node.js 18+ or Bun
Configure in the dashboard
Section titled “Configure in the dashboard”This example needs a confidential OAuth client and two custom scopes. Here’s how to set them up:
-
Register an OAuth client
Open the dashboard at
http://dashboard.localhost:4000, select the examples-dev environment, and navigate to Clients. Click Register Application and fill in:- Name: My Documents App
- Client Type: Confidential
- Grant Types: Authorization Code, Refresh Token
- Redirect URIs:
http://localhost:3000/callback

Copy the client secret and add it to the example’s
.envfile. -
Create custom scopes
Navigate to Scopes in the sidebar. You’ll see the standard OIDC scopes (
openid,profile,email). Click Create Scope to add two custom scopes:read:documents— Read documentswrite:documents— Write documents

These scopes are what the client requests during authorization, and what the API checks when handling requests.

Run the example
Section titled “Run the example”-
Install dependencies
Terminal window cd examples/client-and-apinpm install -
Start both servers
Terminal window npm run devThis starts:
- Client at
http://localhost:3000 - API at
http://localhost:4100
- Client at
-
Open the client and log in
Go to
http://localhost:3000. You’ll see “Third-Party Documents App” with a login button.
Click Log in with Paylent. You’re redirected to Paylent’s login page. Enter:
- Email:
[email protected] - Password:
password

- Email:
-
Browse the app
After login, you’ll see a menu with links:
- View your profile — Calls
GET /api/profileon the resource API, which requires theprofilescope - View your documents — Calls
GET /api/documents, which requires theread:documentsscope

- View your profile — Calls
How it works
Section titled “How it works”Client (Authorization Code + PKCE)
Section titled “Client (Authorization Code + PKCE)”The client implements the OAuth2 Authorization Code flow with PKCE manually:
// Generate PKCE challengeconst codeVerifier = generateCodeVerifier();const codeChallenge = await generateCodeChallenge(codeVerifier);
// Redirect to Paylent's authorize endpointconst authUrl = new URL(`${ISSUER}/oauth/authorize`);authUrl.searchParams.set("response_type", "code");authUrl.searchParams.set("client_id", CLIENT_ID);authUrl.searchParams.set("redirect_uri", REDIRECT_URI);authUrl.searchParams.set("scope", "openid profile read:documents");authUrl.searchParams.set("code_challenge", codeChallenge);authUrl.searchParams.set("code_challenge_method", "S256");On the callback, the client exchanges the authorization code for tokens:
const tokenResponse = await fetch(`${ISSUER}/oauth/token`, { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code, code_verifier: codeVerifier, redirect_uri: REDIRECT_URI, }),});Resource API (JWKS validation)
Section titled “Resource API (JWKS validation)”The API validates access tokens by fetching the public keys from Paylent’s JWKS endpoint:
// Fetch JWKS from Paylent (cached for 5 minutes)const jwksResponse = await fetch(`${ISSUER}/.well-known/jwks.json`);const jwks = await jwksResponse.json();
// Verify the JWT signature and claimsconst publicKey = await importJWK(matchingKey, "RS256");const { payload } = await jwtVerify(token, publicKey);Each endpoint checks that the token includes the required scope:
app.get("/api/documents", requireScope("read:documents"), (c) => { // Only accessible with a token that has "read:documents" scope});Next steps
Section titled “Next steps”- CLI Auth — Authenticate from a CLI tool using the same PKCE flow
- OAuth Endpoints — Full reference for the token, authorize, and introspect endpoints