# Build on Portfolio Dividend Tracker > Register an OAuth app for Portfolio Dividend Tracker (PDT) and let investors connect their PDT account to it. Apps are public OAuth 2.0 clients that use the authorization code flow with PKCE (S256), then call the PDT REST API with the investor's access token. ## Overview | | | |---|---| | Registration endpoint | `POST https://mcp.portfoliodividendtracker.com/oauth/register` (no authentication) | | Authorization endpoint | `https://auth.portfoliodividendtracker.com/oauth/authorize` | | Token endpoint | `https://auth.portfoliodividendtracker.com/oauth/token` | | Server metadata | `https://mcp.portfoliodividendtracker.com/.well-known/oauth-authorization-server` | | API base URL | `https://api.portfoliodividendtracker.com/v1` | | Client type | Public: no client secret, `token_endpoint_auth_method` is `none` | | PKCE | Required, `code_challenge_method=S256` only | | Scopes | `read` (read portfolio data), `write` (create, update and delete it) | | Token lifetimes | Access token 1 hour, refresh token 30 days | | Rate limits | API: 1000 requests per minute per access token. Other endpoints answer `429` with `Retry-After` when called too often | Who does what: - Registering an app needs no account and no human: an agent can do it. - Authorizing needs the investor: they sign in to PDT in a browser and approve the app. Never ask an investor for their PDT password. - API access comes with a paid PDT subscription. Without one, the API answers `403 API_ACCESS_PAUSED`. For local development, an investor can create a personal access token in PDT under Integrations › API (https://app.portfoliodividendtracker.com/account/integrations/api) and send it as `Authorization: Bearer {token}`. These tokens start with `pdt_`. Use them for testing only; apps that other investors use need the OAuth flow below. ## 1. Register the app Send the app's name and redirect URIs as JSON. Register once per app, then store the `client_id` in the app's configuration. ```sh curl -X POST https://mcp.portfoliodividendtracker.com/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "My dividend dashboard", "redirect_uris": ["https://your-app.com/callback"] }' ``` Request body: - `client_name` (string, 1 to 255 characters, optional): Shown to investors when they approve access. Defaults to the host of the first redirect URI. - `redirect_uris` (array of strings, at least 1, required): Where PDT sends the investor back. Any `http` or `https` URL, including `http://localhost:{port}/...` for development, or a URI with a host on one of the custom schemes `claude`, `cursor`, `vscode` and `vscode-insiders`. The `redirect_uri` of later requests must match one of these exactly. Response `201 Created`: ```json { "client_id": "9f3c2a71-5b8e-4d1a-a6c4-2e7f0b9d1c38", "grant_types": [ "authorization_code", "refresh_token" ], "response_types": [ "code" ], "redirect_uris": [ "https://your-app.com/callback" ], "scope": "mcp:use", "token_endpoint_auth_method": "none" } ``` The `scope` in this response is the scope for MCP clients. REST API apps request `read` and/or `write` when authorizing (step 2). Errors (`400`), as in RFC 7591: - `{"error": "invalid_redirect_uri", "error_description": "..."}`: A redirect URI is not a valid URL or uses a scheme that isn't allowed. - `{"error": "invalid_client_metadata", "error_description": "..."}`: Another field is invalid, e.g. `client_name` is longer than 255 characters. `429` means too many registrations; wait for the number of seconds in `Retry-After`. Registration is anonymous: the client is not linked to a PDT account. To change redirect URIs, register a new client and use its `client_id`. ## 2. Send the investor to PDT Create a PKCE code verifier (43 to 128 URL-safe characters) and its S256 challenge (the base64url-encoded SHA-256 hash of the verifier, without padding), plus a random `state`. Keep the verifier and state for the callback, for example in the session. Then redirect the investor to the authorization endpoint. ```js const verifier = base64url(crypto.getRandomValues(new Uint8Array(32))); const challenge = base64url(new Uint8Array( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)), )); const state = crypto.randomUUID(); // Keep verifier and state (e.g. in the session) for the callback. const url = new URL('https://auth.portfoliodividendtracker.com/oauth/authorize'); url.search = new URLSearchParams({ client_id: 'YOUR_CLIENT_ID', redirect_uri: 'https://your-app.com/callback', response_type: 'code', scope: 'read write', state, code_challenge: challenge, code_challenge_method: 'S256', }); location.assign(url); function base64url(bytes) { return btoa(String.fromCharCode(...bytes)) .replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); } ``` Query parameters: `client_id`, `redirect_uri` (one of the registered URIs), `response_type=code`, `scope` (space-separated: `read write`, or `read` for apps that only read; without `scope` only `read` is granted), `state`, `code_challenge`, `code_challenge_method=S256`. Do not request `mcp:use`; it is only for MCP connections. To start the flow, show the investor a "Sign in with PDT" button that links to the route of your app that does the above. Use this markup as is (plain HTML with inline styles; in JSX, convert the `style` attribute to an object): ```html Sign in with ``` The investor signs in and approves the app. PDT then redirects to `redirect_uri` with `code` and `state`, or with an `error` parameter (RFC 6749, e.g. `access_denied`) when the investor declines. Reject the callback when `state` doesn't match. ## 3. Exchange the code for tokens Post the code and the verifier to the token endpoint, form-encoded. There is no client secret. ```sh curl https://auth.portfoliodividendtracker.com/oauth/token \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d redirect_uri=https://your-app.com/callback \ -d code=$CODE \ -d code_verifier=$VERIFIER ``` Response: ```json { "token_type": "Bearer", "expires_in": 3600, "access_token": "...", "refresh_token": "..." } ``` Store both tokens securely, on the server side where possible. ## 4. Call the API Send the access token as a bearer token. Every endpoint is under `https://api.portfoliodividendtracker.com/v1`. ```sh curl https://api.portfoliodividendtracker.com/v1/portfolio \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` - Responses wrap their content in `{"data": ..., "meta": ...}`. Lists are paginated with `page` and `perPage` (default 20, maximum 100); `meta` has `currentPage`, `perPage`, `total` and `lastPage`. - Money is `{"value": "123.45", "currency": "EUR"}`, with the value as a decimal string. Portfolio values are in the investor's portfolio currency (`portfolioCurrency` on `GET /v1/me`). - Every endpoint, parameter and field: https://api.portfoliodividendtracker.com/llms-full.txt Errors have this shape. Match on `error.code`, never on `message`, and include `traceId` when contacting support. ```json { "error": { "code": "TOKEN_INVALID", "message": "This token has been revoked.", "details": { "reason": "revoked" }, "traceId": "..." } } ``` | Status and code | What to do | |---|---| | `401 UNAUTHENTICATED` | Send an access token. | | `401 TOKEN_INVALID` | `details.reason` is `invalid`, `expired` or `revoked`. Refresh the token (step 5); when that fails, send the investor through step 2 again. | | `403 INSUFFICIENT_SCOPE` | The token lacks `write`. Authorize again with `scope=read write`. | | `403 API_ACCESS_PAUSED` | The investor's PDT subscription has ended. Tell them; the connection works again once they renew, without authorizing again. | | `422 VALIDATION_FAILED`, `422 BUSINESS_RULE_VIOLATION` | Fix the request; `message` and `details` say what is wrong. | | `429 RATE_LIMITED` | Wait for the number of seconds in the message, then retry. | ## 5. Refresh the access token Access tokens expire after an hour. Exchange the refresh token for a new pair; no investor interaction is needed. Store the new refresh token from every response and use it next time. ```sh curl https://auth.portfoliodividendtracker.com/oauth/token \ -d grant_type=refresh_token \ -d client_id=YOUR_CLIENT_ID \ -d refresh_token=$REFRESH_TOKEN ``` When the refresh token has expired (after 30 days) or was revoked, send the investor through step 2 again. Investors can disconnect apps in PDT at any time, which revokes their tokens.