Set up OAuth
Learn how to set up OAuth for an integration you’ve built with Klaviyo.
In this article, you’ll learn how to create an OAuth integration in Klaviyo, and learn about the authorization code flow, troubleshooting steps, and error codes.
OAuth is our recommended authentication approach for partner-built integrations. It offers multiple benefits over a private key integration, including security, usability, and improved rate limits.
If you are a Klaviyo customer building a custom integration just for your company, you should use private API keys for your integration.
The last step in the OAuth setup process is submitting your app to get verified and published on Klaviyo’s Integration Directory. Please note that not all apps are eligible to be published on the directory. To be mindful of the data safety of our customers, we will include a warning message on unverified apps during installation.
Before you begin
Warning about OAuth metrics
When migrating from private key to OAuth, metric names should remain unchanged so that Klaviyo can brand these metrics with your icon. You should stop sending any data via private key once your users have installed your OAuth version. To learn more about branding your app’s metrics and what to avoid, read our branded app metrics guide.
In addition, please note that when you are making calls to our new event APIs, any values in the
service
field will be ignored.
Looking for OAuth example implementations? Head to Github:
- Javascript and Python examples
- Node SDK example. This example utilizes helpers from the beta branch of the klaviyo-api package to automatically keep access tokens up to date and simplify API calls to Klaviyo.
Create an OAuth integration in Klaviyo
- Navigate to the Manage apps page.
- Select Create app to create your new application.
- Name your application. Then, securely save your client secret and client ID. You won’t be able to view your client secret again, although you can generate a new one later. When you're done, click Create.
- After creating your app, you should get a confirmation page with help guides for submitting your app to be listed on Klaviyo's Integration Directory. Click Continue to continue setting up your OAuth app.
- Set your scopes using a space-separated list. You should include any and all scopes you will need for your OAuth authorization requests. You can consult the full list of available scopes, if needed.
- Interested in using our Webhooks API to subscribe to webhooks for your app? Make sure to include the
webhooks:read
andwebhooks:write
scopes. - We default to adding the
accounts:read
scope since it is helpful for most applications. This scope lets you call the Get Accounts API to request the account ID, name, and other info you may want to use in your application for a variety of purposes. - Example:
accounts:read events:read events:write profiles:read profiles:write
- Interested in using our Webhooks API to subscribe to webhooks for your app? Make sure to include the
- Edit your Redirect URLs (known as Redirect URIs in OAuth).
- These are the URLs that you have allowlisted Klaviyo to redirect users to after authorizing your app.
- If adding multiple redirect URLs, separate each by adding a space, then clicking the enter/return button on your keyboard.
OAuth authorization code flow
When a Klaviyo user installs your integration, it grants you permission to use Klaviyo’s API on the user’s behalf.
You will be able to authenticate these APIs on the user’s behalf using access tokens. These access tokens come from integration with Klaviyo’s authorization code flow.
Please keep the following in mind:
-
Authorization code grant flow:
-
Klaviyo accounts represent the company/organization granting you access. Each account can have many users. Each user can be part of many accounts, but when they install an app, they will be installing it for the specific account they are logged in as.
-
The code created after the user authorizes the integration expires after 5 minutes.
-
-
Access and refresh token details:
-
Access tokens and refresh tokens grant access to Klaviyo accounts, not users.
-
Access tokens may be up to 4,096 characters long and refresh tokens may be 512 characters long. Please plan accordingly when storing them.
-
Access tokens are valid for 1 hour (this is subject to change). Make sure to rely on the
expires_in
returned by/token
. -
A refresh token is issued when the app is authorized. This refresh token will remain valid until the app is uninstalled by the user.
- Caveat: If a refresh token is not used after 90 days, it is revoked. This is to clean up unused integrations.
-
Every time the refresh token is called, a new access token is issued, but the previous access token is not revoked. The access token remains valid until it expires, the user uninstalls the app, or it is revoked via
/oauth/revoke
. -
Refresh token can only be called 10 times per minute maximum. Existing access tokens will not be revoked, but no new access token will be provided until the time window passes.
-
1. User installs your integration
Since draft apps are not listed in Klaviyo's Integration Directory, you'll need to provide users with a direct install URL. This URL bypasses the Integration Directory and takes the user directly to your app's OAuth authorization endpoint. If you would like to submit your app to be included in the integration directory, read the Submit your application to the Integration Directory section below.
2. App URL redirects to the Klaviyo authorization page
You should require the user to be logged in prior to redirecting them to Klaviyo.
The app URL redirects users to the authorization page: https://www.klaviyo.com/oauth/authorize.
Pass the following query parameters in the URL (these fields are all required):
Query parameter | Description |
---|---|
client_id | The client ID of the integration. |
response_type | Set to code . |
redirect_uri | The user is redirected here after the authorization page. This must exactly match a URL that has been entered in the allowlist for your integration at https://www.klaviyo.com/manage-apps. |
scope | A space-separated list of scopes corresponding to permission to access certain API endpoints. It is also the list of permissions that the user will be shown when they are authorizing the app. Requested scopes that are not part of your OAuth integration will be ignored. See a list of the scope values and which endpoints they correspond to in the appendix below. Example: scope=lists:write campaigns:write metrics:read . |
state | Klaviyo returns whatever value you pass here as a query parameter in the redirect URI. Since PKCE protects against cross-site requests, you may use this field to store any value you want to track across the authorization request. We outline how this can be used for PKCE in the section below. |
code_challenge_method | Set to S256 . |
code_challenge | Cryptographically random string generated by the client. This allows Klaviyo to validate that the client exchanging the authorization code is the same client that requested it and that the authorization code has not been stolen and injected into a different session. More on this in the PKCE and code challenges section. |
Here is an example of the code, including all parameters:
https://www.klaviyo.com/oauth/authorize?response_type=code&client_id=client_id&redirect_uri=https://your-website.com/callback&scope=lists:write campaigns:write metrics:read&code_challenge_method=S256&code_challenge=WZRHGrsBESr8wYFZ9sx0tPURuZgG2lmzyvWpwXPKz8U
When a user installs your app, the list of permissions they’ll see will look like this:
PKCE and code challenges
Klaviyo requires PKCE. It is recommended for all OAuth client types public and confidential.
PKCE consists of a code_challenge
and code_verifier
. You will generate both of these prior to creating your authorization URL in step 2. According to the PKCE standard, a code_verifier
is a high-entropy cryptographic random string with a length between 43 and 128 characters (the longer, the better). It can contain letters, digits, underscores, periods, hyphens, or tildes. The code_challenge
is the code_verifier
SHA-256 encoded.
Below are examples showing how to create the code_challenge
and code_verifier
.
import os
import hashlib
import base64
verifier_bytes = os.urandom(32)
code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b'=').decode('utf-8')
challenge_bytes = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(challenge_bytes).rstrip(b'=').decode('utf-8')
let crypto;
// use node:crypto if your version of node supports it
try {
crypto = require('node:crypto');
} catch (error) {
crypto = require('crypto');
}
function generateCodes() {
const base64URLEncode = (str) => {
return str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
const verifier = base64URLEncode(crypto.randomBytes(32));
const sha256 = (buffer) => {
return crypto.createHash('sha256').update(buffer).digest();
}
const challenge = base64URLEncode(sha256(verifier));
return {
codeVerifier: verifier,
codeChallenge: challenge
}
}
$verifier_bytes = random_bytes(32);
$code_verifier = rtrim(strtr(base64_encode($verifier_bytes), '+/', '-_'), '=');
$challenge_bytes = hash('sha256', $code_verifier, true);
$code_challenge = rtrim(strtr(base64_encode($challenge_bytes), '+/', '-_'), '=');
If you get Invalid "code_verifier"
error, verify that your code matches this regex pattern which is the specifications of code verifiers:
r'^[a-zA-Z0-9\-._~]{43,128}$'
You are required to generate a new
code_verifier
andcode_challenge
pair every time you create an authorization URL for a user to install your integration.
Storing PKCE values across requests
When we redirect the user to your redirect_uri
with the code, you will exchange that code for an access token (more on this in step 3). In that request, you will also need to pass the code_verifier
so that we can check it against the code_challenge
in your authorization request.
To accomplish this, you will need some way to know which code_verifier
to send alongside the code you are exchanging. To accomplish this:
- Store your
code_verifier
in some data store (database or cache) along with a unique customer identifier. - Pass the unique customer identifier as state in your authorization request. We will return this state to you in step 3 after the user has authorized your app.
- When you are exchanging your code for an access token, pass in the
code_verifier
corresponding to this code. Then, take the customer identifier that was passed alongside the code and look up thecode_verifier
corresponding to this customer identifier in your data store.
3. Klaviyo redirects with an authorization code
Authorization is allowed
When the user authorizes the app, Klaviyo redirects to the redirect_uri
(known as the Redirect URL in Klaviyo) you provided in the previous step.
Klaviyo will pass the following query parameters upon success:
Query parameter | Description |
---|---|
code | The authorization code that you can exchange for an access token and refresh token in the next step. |
state | Klaviyo returns whatever state you pass us in your authorization request. |
If there is an error, Klaviyo follows the OAuth spec which defines the possible error responses.
Authorization is denied
When the user denies authorization to the app, Klaviyo will redirect to the same redirect_uri
noted above. Klaviyo will pass the following query parameters. Use error
and error_description
to redirect users to your site with your own messaging.
Query parameter | Description |
---|---|
error | Klaviyo will return an error of access_denied |
error_description | The message will state The+resource+owner+or+authorization+server+denied+the+request |
state | Klaviyo returns whatever state you pass us in your authorization request. |
4. Integration exchanges the authorization code for token
Now that the user has authorized your integration and you have an authorization code, exchange the code for an access token and a refresh token.
Note the following:
- Use a server-to-server request. The client secret is private and should never be in the browser because you would expose all of your customer’s data.
- If you didn’t save your client secret, you can generate a new one for the client via the UI and add it to your app environment settings now.
- The
Content-Type
must beapplication/x-www-form-urlencoded
.
Please make sure that your request body is in this format, and isn’t in some other format like JSON, which can result in difficult to debug errors.
The endpoint is: https://a.klaviyo.com/oauth/token
Request headers | Value |
---|---|
Authorization | (Required) Base 64 encoded string that contains the client ID and client secret key. The field must have the format: Authorization: Basic <base64 encoded client_id:client_secret> |
Content-Type | (Required) Set to application/x-www-form-urlencoded . |
Request body | Value |
---|---|
grant_type | Must be authorization_code . |
code | The authorization code that is the query parameter in the request to the redirect URL (see step 3 above). |
code_verifier | This is the plain-text random string you used to generate the code_challenge in your authorization request. Klaviyo hashes this value to check that it matches the code_challenge you sent in the authorization request that created this code. |
redirect_uri | This must be the same redirect_uri that you used in the authorization request in step 2 to acquire this code. If there is a mismatch, this request will fail. |
5. Integration receives access and refresh tokens
If your previous request is successful, you’ll receive the following response:
Key | Value type | Value description |
---|---|---|
access_token | string | An access token that can be used in the Authorization header for calling the API in place of an API key. |
token_type | string | How the access token may be used: always bearer . |
expires_in | int | The time period (in seconds) for which the access token is valid. |
refresh_token | string | This token can be used to request a new access token and refresh token. |
scope | string | The scopes that this access token has access to for accessing API resources. |
If unsuccessful, the possible error codes follow the OAuth spec.
6. Integration uses API: request
Now that you’ve saved the access token and refresh token from the previous step, you can use the access token to authenticate to our APIs. Here is a template curl request:
curl --request GET \
--url https://a.klaviyo.com/api/{endpoint}/ \
--header 'Authorization: Bearer {access_token}' \
--header 'accept: application/json' \
--header 'revision: {revision-header}'
Note that this request is the same as authenticating with a private key to Klaviyo’s APIs, but with a different value for the authorization header.
See information for private key authentication.
Refresh tokens
When your access token expires, you can get a new access token by making a POST request. It is the same as your request exchanging an auth code, but the body is a refresh token instead.
This the 401 error you will receive when the access token is expired:
{
"errors": [
{
"id": "653528d5-ad97-40ed-bf14-9494d4fcddc3",
"status": 401,
"code": "not_authenticated",
"title": "Authentication credentials were not provided.",
"detail": "Missing or invalid access token.",
"source": {
"pointer": "/data/"
}
}
]
}
The endpoint for the POST request is: https://a.klaviyo.com/oauth/token
Request headers | Value |
---|---|
Authorization | (Required) Base 64 encoded string that contains the client ID and client secret key. The field must have the format: Authorization: Basic <base64 encoded client_id:client_secret> |
Content-Type | (Required) Set to application/x-www-form-urlencoded . |
Request body | Value |
---|---|
grant_type | Set to refresh_token . |
refresh_token | A valid refresh token. |
Handling uninstalled integrations
If you make a refresh token request to /oauth/token
and receive a 400 response with {"error":"invalid_grant"}
in the response body, that means your refresh token is invalid. This could be due to several reasons. You can treat all invalid_grant
responses, regardless of the error_description
, like the application has been uninstalled by the user.
You can handle uninstallation by showing the app as uninstalled within your application and/or trigger a winback notification.
Then, the customer can re-install your integration, just like they installed it previously.
Here are the possible error_descriptions
you may get when a refresh token is invalid and the reasons for them:
Error description | Reason |
---|---|
"error_description": "Refresh token has been revoked" | This is likely the only reason you will be getting an “invalid_grant” in production. This will occur for one of two reasons:1. The customer uninstalled your app in Klaviyo. 2. Your app made a request to /oauth/revoke resulting in the app being revoked. This was likely because the customer uninstalled the app on your end. |
"error_description": "Refresh token does not exist" | This refresh token does not exist. If you are getting this while developing your app, check for typos when copying your refresh token. If you are getting this in production, check that you aren’t truncating your tokens and that you're using the exact same value that was sent to you in the API. |
"error_description": "Refresh token expired due to inactivity" | This is because the refresh token was not used for greater than 90 days. This means that the app was inactive for 90 days for this customer, since the refresh token is used frequently to refresh access tokens. |
7. Integration uses API: response
If successful, Klaviyo returns the following response:
Key | Value type | Value description |
---|---|---|
access_token | string | An access token that can be used in the Authorization header for calling the API in place of an API key. |
token_type | string | How the access token may be used: always bearer . |
expires_in | int | The time period (in seconds) for which the access token is valid. |
refresh_token | string | This token can be used to request a new access token. This will be the same refresh token as you used to make the refresh token request. |
scope | string | The scopes that this access token has access to for accessing API resources. |
Possible error codes will follow the OAuth spec.
Revoke an application
You may wish to revoke an application. One use case for this is allowing users to uninstall your application from your website. When a user uninstalls the integration with Klaviyo from your dashboard, you should revoke any refresh and/or access tokens you have for this user so that the integration doesn't continue to show as Installed within Klaviyo.
Revoking an application via this endpoint will uninstall the application for the account in Klaviyo, removing it from the account's integration page in Klaviyo.
You can use any valid access_token
or a refresh_token
in the request body; expired access tokens are still considered valid. After a successful revoke request, all remaining tokens for the company's installed application will be invalidated.
The endpoint is: https://a.klaviyo.com/oauth/revoke
Request headers | Value |
---|---|
Authorization | (Required) Base 64 encoded string that contains the client ID and client secret key. The field must have the format:Authorization: Basic <base64 encoded client_id:client_secret> . |
Content-Type | (Required) Set to application/x-www-form-urlencoded . |
Request body | Value |
---|---|
token_type_hint | Set to refresh_token OR access_token . |
token | The valid refresh_token OR access_token . |
If successful, Klaviyo will return a 200:
Key | Value type | Value description |
---|---|---|
success | string | True |
error | array | Shall be empty |
8. Register your integration for our Integration Directory (optional)
You've successfully created an OAuth integration with Klaviyo and implemented the authorization code flow.
In order for your app to be verified and published on Klaviyo’s Integration Directory, you’ll need to submit your app for review. Please note that not all apps are eligible to be published on the Directory. To be mindful of the data safety of our customers, we will include a warning message on unverified apps during installation. If your app qualifies for publication, we will be in touch with next steps.
Rate limits
Rate limiting for OAuth apps differs from our standard API rate limiting. OAuth apps receive their own rate limit quota per installed app instance (i.e., per account per app), while private key integrations share the same rate limit quota per account.
The rate limit response headers received by OAuth apps for API calls are specific to that app instance’s quota.
For instance, the Create Events endpoint has an XL (3500/m) rate limit. Say that a given Klaviyo account:
- Has a private key integration, PK Integration A. PK Integration A can make calls to Create Events with a steady rate limit of 3500/m.
- Installs OAuth App 1. OAuth App 1 will have access to a 3500/m Create Events rate limit, totally isolated from Integration A’s usage.
- Installs OAuth App 2. OAuth App 2 would receive a 3500/m rate limit, in isolation from the OAuth App 1 and the PK Integration A.
Troubleshooting tips
Invalid client
If you get an invalid_client
error:
- Check that your client id is correct.
- Check that
[ID]:[SECRET]
is base64 encoded. - Check that the prefix is
Basic
and is base64 encoded. - Check that you converted the base64 encoded credentials back to a string when concating with
Basic
. In some languages, the encoding creates bytes or some other format. Inspect the authorization header on your request to see if it looks right.- Example:
Authorization: Basic <base64 encoded client_id:client_secret>
.
- Example:
Invalid grant type
If you get an invalid_grant_type
error:
-
Check that you’re sending the correct grant type:
- Example:
authorization_code
,refresh_token
.
- Example:
-
Check that your request body is
application/x-www-form-urlencoded
.- Double check that you’re doing this correctly in your language of choice.
-
If you’re trying to use a refresh token and your request looks correct, this means that the refresh token is invalid because it is expired or revoked.
Invalid code verifier
If you get an Invalid "code_verifier"
error, verify that your code matches this regex pattern which is the specifications of code verifiers:
r'^[a-zA-Z0-9\-._~]{43,128}$'
Code challenge failed
If you get a Code challenge failed.
error for an/oauth/token
request, this means that when we S256 hash the code_verifier
it does not match the code_challenge
that your app initially provided during the authorization request. Please double-check that you're sending the correct code verifier and hashing your code_verifier
with S256.
403 HTML on /token or /revoke
If you get a 403 html error on /token
or /revoke
.
-
Verify that you’re making your request to the correct URL:
-
Requests to directly klaviyo.com will fail.
Example of error:
403 ERROR
The request could not be satisfied.
This distribution is not configured to allow the HTTP request method that was used for this request.
The distribution supports only cachable requests.
We can't connect to the server for this app or website at this time.
There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
Error glossary
Accessing OAuth client page
Type of error | Error message | Why and how to fix |
---|---|---|
404 | This www.klaviyo.com page can’t be found. | No webpage was found for the web address: https://www.klaviyo.com/oauth/client. Your current account doesn't have permission to view this page. Only the owner, admin, or manager of beta allowlisted accounts can view this page. Make sure you are in the correct account. |
User granting authorization
Type of error | Error message | Why and how to fix |
---|---|---|
503 | N/A | OAuth service is temporarily unavailable on Klaviyo. |
403 | Access to www.klaviyo.com was denied. You don't have authorization to view this page. | Make sure you are logging into an account as the owner, admin, or manager role. Only these roles can grant authorization to an app. |
Call to OAuth/token
Type of error | Error message | Why and how to fix |
---|---|---|
400 | "error":"invalid_grant" | If you are making a refresh request and you get this error, your refresh token is invalid. Your token could be invalid due to expiring after 90 days of no-use, it was revoked by someone in your account, or it was revoked by Klaviyo’s internal systems for security reasons, or it is a wrong or incorrect token. Your customer will need to reauthorize the integration. |
400 | "error":"invalid_request", "error_description":"Missing "refresh_token" in request." | Missing refresh_token in the request body. |
400 | "error":"unsupported_grant_type" | Missing or incorrect grant_type in the request body. For refresh flow, it should be refresh_token ; for authorize code flow, it should be authorization_code . Ensure the form is x-www-form-urlencoded . |
400 | "error":"invalid_grant", "error_description":"Code challenge failed." | When we S256 hash the code_verifier , it does not match the code_challenge that your app initially provided during the authorization request. Please double-check that you're sending the correct code verifier and hashing your code_verifier with S256. |
401 | "error":"invalid_client" | You must send client authentication as the basic auth header. Please check that your client ID and client secret are correct. Also, check that you converted the base64 encoded credentials back to a string when concating with Basic . In some languages, the encoding creates bytes or some other format. Inspect the authorization header on your request to see if it looks right.Example: Authorization: Basic <base64 encoded client_id:client_secret> . |
429 | “error”:”rate_limit_exceeded”, ”error_description”:”Rate limit exceeded for refresh token. Please try again after 1 minute.” | Rate limit exceeded for OAuth refresh token. A refresh token can be used 10 times per minute maximum. |
4xx | Invalid "code_verifier" | This is for the authorization code flow. The code verifier does not match with the hashed verifier code that was sent earlier. |
503 | N/A | OAuth service is temporarily unavailable on Klaviyo. |
Call to OAuth/revoke
The following are errors specific to the oAuth handshake.
Type of error | Error message | Why and how to fix |
---|---|---|
400 | "error": "invalid_client" | You must send client authentication as the basic auth header. Please see if your client ID and client secret are correct. Also, check that you converted the base64 encoded credentials back to a string when concating with Basic . In some languages, the encoding creates bytes or some other format. Inspect the authorization header on your request to see if it looks right.Example: Authorization: Basic <base64 encoded client_id:client_secret> . |
400 | "error":"invalid_request" | Make sure the request content-type is x-www-form-urlencoded .If it is, the token is missing in the request body. |
503 | N/A | OAuth service is temporarily unavailable on Klaviyo. |
API call
The following are general API errors that occur based on how the Authorization header is passed.
Type of error | Error message | Why and how to fix |
---|---|---|
401 | authentication_failed - Incorrect authentication credentials. | Access token is invalid. It either doesn't exist, is expired, or was revoked. Please obtain a new valid access token using the refresh token. |
401 | not_authenticated - Authentication credentials were not provided. | The authorization header for OAuth is formatted incorrectly. It is most likely due to missing or extra characters in the access token, missing Bearer prefix, and other format-related issues with the authorization header. |
503 | N/A | OAuth service is temporarily unavailable on Klaviyo. |
Additional resources
Updated 5 months ago