The DSS Public API uses OAuth 2.0 Authorization Code with PKCE. Every partner uses the same flow. We do not issue API keys and we do not offer client credentials.

Why this flow

OAuth Authorization Code + PKCE is the right shape for “user X grants third-party app B access to their data on service A.” It’s what Google, Stripe, Plaid, Slack, and every dashboard-style consumer API uses. PKCE protects the authorization code if it’s ever intercepted in transit. We use opaque tokens stored server-side, not JWTs. Revocation is instant — when a user disconnects, every token we’ve issued to your client for that user stops working on the next request. There’s no token expiry window where revoked-but-not-expired tokens still work.

Endpoints

The flow at a glance

Scopes

Scopes are additive — request only what your UI actually renders. Granular scope requests get higher consent rates and a tighter blast radius if anything ever leaks. The set of scopes you may request is configured per partner at onboarding. Asking for a scope you weren’t granted at onboarding returns invalid_scope on the authorize call.

PKCE

PKCE (Proof Key for Code Exchange) closes the loophole where an intercepted authorization code could be redeemed by an attacker. Every request needs a fresh, single-use pair:
  • code_verifier — a high-entropy random string, 43-128 chars, kept on your server.
  • code_challengebase64url(sha256(code_verifier)), sent on the authorize call.
We require code_challenge_method=S256. plain is rejected.

Generating a PKCE pair

The reference vectors come from RFC 7636 §4.4. Our verification is tested against them — if your implementation matches the spec, it’ll match ours.

Step 1 — Send the user to /oauth/authorize

This is a browser redirect, not a server-to-server call. The user has to sign in to DSS and approve consent in their own browser session.

Parameters

What the user sees

A consent screen listing your partner name and the requested scopes in plain English. On approval the browser is redirected back to your redirect_uri:
On denial, the same redirect with ?error=access_denied&state=.... Render a useful message and offer to retry.
Validate state first thing on the callback. If it doesn’t match the value you stashed against the user’s session, drop the request — treat it as a CSRF attempt, not a flow you can recover.

Step 2 — Exchange the code for tokens

Authorization codes are single-use and expire after 60 seconds. Exchange immediately on receipt.

Response

Both tokens are opaque. Don’t try to parse them — they have no meaning client-side.

Step 3 — Call the API

Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. See Rate limits.

Refreshing tokens

Access tokens live for 1 hour. Refresh them with grant_type=refresh_token to get a new pair.

Rules

  • Refresh tokens rotate. Every successful refresh revokes both the old access token and the old refresh token, and issues a new pair. Always swap your stored value for the new one immediately.
  • Refresh cannot widen scope. You can request a narrower scope parameter to issue a more limited token, but not a broader one.
  • Refresh tokens are 90-day TTL. If a user hasn’t been seen for 90 days the refresh token expires and they need to reconsent.
  • One concurrent refresh per token. If you race two refreshes with the same refresh_token, only one wins and the other returns invalid_grant. Treat that as “use the value the winning call returned” — don’t reconsent.

Revoking a token

You can revoke either the access token or the refresh token via RFC 7009. Revoking one revokes the paired other.
Per RFC 7009 the response is always 200 OK with no body — revoking an unknown token is a no-op, not an error.

What happens when the user disconnects

A user can disconnect your integration from their DSS dashboard at any time. When they do:
  1. Every access and refresh token issued to your client for that user is revoked immediately. Subsequent calls return invalid_token401.
  2. A signed user.consent.revoked webhook fires to your registered webhook URL — see Webhooks.
  3. Under our Data Processing Agreement you have 30 days to delete any cached DSS data for that user.
To trigger the same flow from your side (e.g. the user disconnects from your settings UI), call POST /oauth/revoke with the user’s token. Because revoking either token in a pair revokes both, a single call tears down that user’s connection on our side — no separate cleanup call is needed.

Token storage on your side

  • Store both tokens encrypted at rest, keyed by your user record.
  • Never log them. The audit log on our side has everything we need; logging on your side just creates a leak risk.
  • Treat the access token as bearer credentials. Anyone with it can read whatever the granted scopes allow until it expires or is revoked.

Failure modes

OAuth errors follow RFC 6749 §5.2 — shape is { "error": "...", "error_description": "..." }, not RFC 7807 Problem Details. Resource endpoint errors do use Problem Details — see Errors.