Implementing the OAuth flow
Step-by-step guide to the Authorization Code Grant with OIDC
Prerequisites: Before implementing the OAuth flow, ensure you have your OAuth server configured, client credentials generated, JWKS endpoint accessible, and consistency key implementation chosen.
Flow at a glance
User -> Plaid -> Your /authorize -> User authenticates -> Your /token -> Plaid validates -> API access
(redirect) (login + 2FA) (code exchange) (JWKS check)The OAuth flow, step by step
The Authorization Code Grant follows a five-step process. This guide describes the OIDC flow. Plain OAuth 2.0 follows the same flow but without the ID token validation step.
Screen placement
Under regulatory requirements, the data recipient (or its platform) is responsible for obtaining user authorization. Plaid fulfills this obligation on behalf of apps via Plaid Link screens, divided as follows:
Data Provider hosts (your responsibility):
- Authentication screen - Users log in with their credentials on your domain.
- Account selection (optional) - If you are not using Plaid account selection, you may host account selection on your domain. This approach works but adds friction and limits the user experience for returning users, so Plaid does not recommend it for most integrations.
Plaid hosts (handled by Plaid Link, recommended):
- Data transparency messaging - Clear identification of which app is requesting access and what data will be shared.
- Account selection - User selects which accounts to share in Plaid Link after authenticating with you. Plaid recommends this model as it provides the best user experience for returning users.
Keep the flow streamlined. Beyond the authentication screen (and, if you choose DP-owned account selection, that screen), avoid inserting additional pages into the end-to-end linking flow. Every extra screen increases drop-off.
The five steps:
- Plaid redirects your user to authenticate
- User logs in and authorizes access
- You send Plaid an authorization code
- Plaid exchanges the code for tokens
- Plaid verifies the ID token and accesses your API
Step 1 - Plaid redirects the end user
When a user starts linking their account, Plaid redirects them to your authorization endpoint (as defined in your well-known config). The redirect includes these query parameters:
Query parameters
| Query parameter | Description | Value |
|---|---|---|
response_type | A string, indicating the type of response Plaid expects. | Set to: code |
redirect_uri | Where Plaid expects your organization to redirect back to once the user completes all authentication steps. | https://cdn.plaid.com/link/v2/stable/oauth.html |
scope | A set of strings indicating the set of scopes Plaid requests access to. | Will be set to openid and offline_access, at minimum |
client_id | The client ID you issued to Plaid. | A client ID |
state | Your organization will return the same string when redirecting to the redirect_uri. | An opaque string |
prompt | Specifies the type of authentication prompt the server will send to the end user. | Set to login |
code_challenge | Optional: This value is required when using PKCE. | The code_verifier, either as a Base64URL-encoded hash or in plain text |
code_challenge_method | Optional: This value is required when using PKCE. | Indicates the format of the code_challenge. May be either: S256 (hashed by SHA-256 and Base64URL-encoded) or plain (in plain text) |
https://auth.firstplatypus.com/oauth2/v1/authorize?response_type=code
&client_id=dc1fe34ae9e5e98147f2fd76060016a4
&redirect_uri=https%3A%2F%2Fcdn.plaid.com%2Flink%2Fv2%2Fstable%2Foauth.html
&state=v2.9f77edf0-a328-4501-9528-4a5f460cf770.0.0
&scope=openid%20offline_access%20accounts%20transactions
&prompt=login
&code_challenge=4dKRcTlKg7PbBxYokEH5gbfwfXcUdvDVYVdFZniVq4s
&code_challenge_method=S256This page will be requested directly by the user's device.
Step 2 - The user completes all authentication steps
At this step, the user authenticates with your system, typically username/password plus 2FA.
Two-factor authentication (2FA)
Plaid requires 2FA for Core Exchange connections. Users must complete a second authentication factor beyond username and password. 2FA aligns with industry security standards and reduces the risk of account takeover.
Use industry-standard 2FA methods such as one-time codes (SMS, email, authenticator apps), push notifications, biometric authentication, or hardware security keys. For mobile apps, biometric authentication provides the best user experience.
2FA communication requirements:
When sending 2FA prompts (SMS, email, push notification), the message must:
- Be clear about which app is requesting access - Include the app name from Plaid's authorization request
- Look legitimate - Use your standard templates and sender information to avoid appearing like phishing
- Be timely - Deliver within seconds of the authentication attempt
First Platypus Bank: Verify connection to Venmo.
Your code: 123456
If you didn't request this, contact us immediately.Subject: Verify connection to Copilot
Hi [Name],
Venmo is requesting access to your First Platypus Bank account.
Your verification code: 123456
This code expires in 10 minutes.
If you didn't request this, please contact us immediately.Mobile authentication
For mobile apps, enable App2App and biometric authentication. Your users will thank you.
User cancellation
If the user cancels, handle it as an error and send them back to Plaid with the right error code.
Step 3 - You create an authorization code and send it to Plaid
After the user successfully authenticates, generate a temporary authorization code and redirect the user's browser back to Plaid's redirect_uri. Include these query parameters:
| Query parameter | Description |
|---|---|
code | The temporary authorization code. Plaid will exchange this for an access token in the next step. |
state | The state parameter from step 1. Plaid verifies that the two values match. |
Example of your response
https://cdn.plaid.com/link/v2/stable/oauth.html?code=1284918391
&state=eyJvYXV0aF9zdGF0ZVStep 4 - Plaid requests an access token
Now, Plaid calls your token_endpoint to exchange the authorization code for tokens. This is a backend-to-backend call (no user involved), authenticated using the client ID and secret you issued.
The auth method comes from your well-known config. If you specified client_secret_basic, Plaid will send a basic auth header.
Request body format: application/x-www-form-urlencoded. Plaid sends these parameters:
Body parameters
| Body parameter | Description | Value |
|---|---|---|
prompt | Specifies the type of a prompt the server will send to the end user. | Will be set to: consent |
grant_type | The type of grant Plaid is exchanging for an access token. (In this case, an authorization code.) | Will be set to: authorization_code |
code | The temporary authorization code Plaid is exchanging for the access token. | The code you sent to Plaid in the previous step |
redirect_uri | Where Plaid expects your organization to redirect back to once the user completes all authentication steps. | https://cdn.plaid.com/link/v2/stable/oauth.html |
scope | The set of possible scopes. | An array of scopes, for example: [offline_access, openid, accounts, transactions, identity] |
audience | Optional: Defines who the token is for (the resource server that will consume the token). Plaid will send this value if this information is available. | A string, for example: https://api.firstplatypusbank.com |
code_verifier | Optional: This value is only required when using PKCE. | The code_verifier, created before starting the authentication flow |
curl --request POST 'https://auth.firstplatypusbank.com/oauth2/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header "Authorization: Basic YzVhNTI0NWIwNjJiZjg0MjBkMTFhYjQzNjFiMjhhMTU6clZYWU9vUVM0ckhVRzc5bl80OGFs" \
--data-raw '{
"prompt": "login",
"grant_type": "authorization_code",
"code": "1284918391",
"redirect_uri": "https://cdn.plaid.com/link/v2/stable/oauth.html",
"scope": ["offline_access", "accounts"]
}'Validate that the client_id, client_secret, code, and redirect_uri all match what you expect. If everything checks out, send Plaid the access_token, id_token, and refresh_token. All three are required to access your API.
Response parameters
| Property | Description |
|---|---|
access_token | An opaque string (likely a JWT structured according to the OAuth2 specification). Plaid will present this string as a bearer token to all requests made to your Core Exchange API. The access_token encodes the user's identity and the scope of access granted. |
expires_in | The lifetime of the access token, in seconds. Typically 15 minutes (900 seconds). Plaid checks for expiration before using an access token. If the access token has expired, Plaid will use the refresh token to request a new access token. If your organization expires the token before the stated expiration date, Plaid expects to receive a 401 response with an error code of "602 not authorized". |
id_token | An OIDC ID token. Plaid only reads the sub field from this token. In a deployment with multiple financial institutions, the sub field must be unique to each financial institution. (It doesn't need to be unique to the user across all financial institutions.) |
refresh_token | An opaque string (likely a JWT) that Plaid can use to request a new access token. Plaid will use this to fetch data periodically long after the original access token expires. See the refresh flow section for more information. Recommended expiration: 13+ months. Compliance requires reauthorization every 12 months, which Plaid handles automatically. Setting refresh token expiration to 13+ months ensures tokens remain valid through the reauthorization cycle. Don't use inactivity timeouts; some use cases involve infrequent API calls that shouldn't cause the connection to be invalidated. |
{
"access_token": "agstynmdygjdghabrgraeh...",
"expires_in": 900,
"id_token": "snsyjrhvjdtvyjvsgcegaethstj...",
"refresh_token": "dhcsrtjsrgayvkdisfdgntshstu..."
}Step 5 - Plaid verifies the ID token
This step only applies to OIDC implementations. Plain OAuth 2.0 implementations skip this step since there's no ID token to validate.
After receiving the id_token, Plaid verifies its authenticity by fetching your JWKS from the jwks_uri in your .well-known config.
How verification works:
- Fetch JWKS from your
jwks_uri(Plaid caches this for speed). - Use the
kidin the token header to find the right public key. - Verify the digital signature and claims (issuer, audience, expiration).
- Extract the
subfield (the user identifier).
User -> /authorize -> authenticates
-> /token -> returns tokens
-> Plaid fetches JWKS -> verifies ID token -> DonePlaid now has everything needed to access your Core Exchange API on behalf of the user.
Refresh flow
Access tokens expire. That's by design. When Plaid needs fresh data (e.g., balance checks before ACH transfers or transaction updates), it uses the refresh token to get a new access token. Same token_endpoint, different parameters.
Body parameters
| Body parameter | Description | Value |
|---|---|---|
grant_type | Specifies that Plaid is requesting a new access token to replace the expired access token. | Will be set to: refresh_token |
refresh_token | The refresh token you issued to Plaid. Recommended expiration: 13+ months. This expiration aligns with the reauthorization requirement (12 months) while providing a buffer for high-traffic periods, such as tax season, when users may not complete reauthorization immediately. | Example: dhcsrtjsrgayvkdisfdgntshstu... |
curl --request POST 'https://auth.firstplatypusbank.com/oauth2/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header "Authorization: Basic YzVhNTI0NWIwNjJiZjg0MjBkMTFhYjQzNjFiMjhhMTU6clZYWU9vUVM0ckhVRzc5bl80OGFs" \
--data-raw '{
"grant_type": "refresh_token",
"refresh_token": "dhcsrtjsrgayvkdisfdgntshstu..."
}'Validate the grant_type and refresh_token, then respond with new tokens: access_token, id_token, and refresh_token.
Pro tip: Rotate the refresh token each time for better security (new token, new expiration). But if you need to reuse the same refresh token across multiple requests, that works too.
Response parameters
| Property | Description |
|---|---|
access_token | An opaque string (likely a JWT structured according to the OAuth2 specification). Plaid will present this string as a bearer token to all requests made to your Core Exchange API. The access_token encodes the user's identity and the scope of access granted. |
expires_in | The lifetime of the access token, in seconds. Typically 15 minutes (900 seconds). Plaid checks for expiration before using an access token. If the access token has expired, Plaid will use the refresh token to request a new access token. If your organization expires the token before the stated expiration date, Plaid expects to receive a 401 response with an error code of "602 not authorized". |
id_token | An OIDC ID token. Plaid only reads the sub field from this token. In a deployment with multiple financial institutions, the sub field must be unique to each financial institution. (It doesn't need to be unique to the user across all financial institutions.) |
refresh_token | An opaque string (likely a JWT) that can be used to request a new access token. When using refresh token rotation, a new refresh token replaces the one sent in the request. If not using rotation, this may be the same refresh token. Recommended expiration: 13+ months. Set to at least 12 months to comply with reauthorization requirements. The 13+ month range provides buffer time for users to complete reauthorization during peak periods without connections failing. |
{
"access_token": "lngarogglkcangasgabba...",
"expires_in": 900,
"id_token": "snsyjrhvjdtvyjvsgcegaethstj...",
"refresh_token": "dhcsrtjsrgayvkdisfdgntshstu..."
}Quick troubleshooting
| Symptom | Common cause | Fix |
|---|---|---|
| User stuck on loading screen | Authorization endpoint timeout | Check endpoint latency < 3.5s |
| "Invalid code" errors | Code reuse or expiration | Expire codes after 10 min, single-use |
| Token validation fails | JWKS not accessible | Verify jwks_uri is public HTTPS |
| Refresh token rejected | Token expired | Set expiration to 13+ months |
invalid_grant errors | Code/redirect URI mismatch | Verify exact match on redirect_uri |
| CORS errors | Missing headers | Add Access-Control-Allow-Origin |
See the full error reference below, along with the Troubleshooting Guide for detailed debugging.
When things go wrong
OAuth errors fall into three categories, each handled differently. (Full details in RFC 6749 section 4.1.)
What if the redirect_uri doesn't match? If the redirect_uri in the authorization request doesn't match the registered value for that client_id, don't redirect. Show an error page instead. For Plaid, the redirect URI should always be https://cdn.plaid.com/link/v2/stable/oauth.html. Redirecting to an unregistered URI is a security risk that could expose authorization codes to attackers.
User cancellation
If the user cancels authentication or another error occurs (excluding redirect URI mismatches), redirect them back to Plaid with these parameters:
| Query parameter | Description |
|---|---|
error | The reason for the error. See the Authorization errors table below for a list of possible errors. |
state | The opaque string Plaid passed as the state parameter in the authorization_endpoint redirect step. |
https://cdn.plaid.com/link/v2/stable/oauth.html?error=access_denied
&state=eyJvYXV0aF9zdGF0ZVAuthorization error codes
| Error value | Description |
|---|---|
invalid_request | The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. |
unauthorized_client | The client is not authorized to request an authorization code using this method. |
access_denied | The resource owner or authorization server denied the request. |
unsupported_response_type | The authorization server does not support obtaining an authorization code using this method. |
invalid_scope | The requested scope is invalid, unknown, or malformed. |
server_error | The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.) |
temporarily_unavailable | The authorization server is currently unable to handle the request due to a temporary overload or maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP status code can't be returned to the client via an HTTP redirect.) |
Token exchange and refresh flow errors
Errors during token exchange or refresh? Return HTTP 400 with an error field from the table below. Add error_description and error_uri if you want to help with debugging. (Details in RFC 6749 section 5.2.)
{
"error": "invalid_grant",
"error_description": "Authorization grant does not match redirect URI",
"error_uri": "https://www.your-org.com/human-readable-error-info/"
}Token error codes
| Error code | Description |
|---|---|
invalid_request | The request is missing a required parameter. |
invalid_client | Client authentication failed. |
invalid_grant | The provided authorization grant or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client (rely on error description from FIs to further break this out). |
unauthorized_client | The authenticated client is not authorized to use this authorization grant type. |
unsupported_grant_type | The authorization grant type is not supported by the authorization server. |
invalid_scope | The requested scope is invalid, unknown, or malformed. |
Expired access token
When Plaid makes API requests with an access token, validate that it hasn't exceeded the lifetime you specified in expires_in during token issuance. Track when each token was issued and check that current_time - issue_time < expires_in.
If the access token has expired, send a 401 response with an error code of "602 not authorized". Plaid will automatically use the refresh token to request a new access token.