HomeGuidesAPI Reference
ChangelogHelp CenterCommunityContact Us

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.

Creating an OAuth integration has multiple benefits over a private key integration, including security, usability, and improved rate limits.

📘

The last step in the OAuth setup process is submitting a form (optional) 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:

Create an OAuth integration in Klaviyo

  1. Navigate to this page: https://www.klaviyo.com/oauth/client.
  2. Select Create app to create your new application.
    OAuth landing page in Klaviyo with Create app with black background in upper right
  3. 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. 
    Set up OAuth page in Klaviyo with title, client ID and client secret
  4. 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 new Webhook APIs to subscribe to webhooks for your app? Make sure to include the webhooks:read and webhooks:write scopes. Then, join our Webhooks waitlist.
    • 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
      Demo app draft page in Klaviyo showing OAuth settings including scopes
  5. 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.
      Demo app page showing OAuth settings including Redirect URLs

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.

OAuth authorization code flow diagram

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 parameterDescription
client_idThe client ID of the integration.
response_typeSet to code.
redirect_uriThe 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/oauth/client.
scopeA 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.
stateKlaviyo 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_methodSet to S256.
code_challengeCryptographically 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 permission 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 hashlib
import base64

verifier_bytes = os.urandom(32)
code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b"=")
challenge_bytes = hashlib.sha256(code_verifier).digest()
code_challenge = base64.urlsafe_b64encode(challenge_bytes).rstrip(b"=")
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 should generate a new code_verifier and code_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:

  1. Store your code_verifier in some data store (database or cache) along with a unique customer identifier.
  2. 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.
  3. 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 the code_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 parameterDescription
codeThe authorization code that you can exchange for an access token and refresh token in the next step.
stateKlaviyo 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 parameterDescription
errorKlaviyo will return an error of access_denied
error_descriptionThe message will state The+resource+owner+or+authorization+server+denied+the+request
stateKlaviyo 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 be application/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 headersValue
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 bodyValue
grant_typeMust be authorization_code.
codeThe authorization code that is the query parameter in the request to the redirect URL (see step 3 above).
code_verifierThis 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_uriThis 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: 

KeyValue typeValue description
access_tokenstringAn access token that can be used in the Authorization header for calling the API in place of an API key.
token_typestringHow the access token may be used: always bearer.
expires_inintThe time period (in seconds) for which the access token is valid.
refresh_tokenstringThis token can be used to request a new access token and refresh token.
scopestringThe 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 headersValue
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 bodyValue
grant_typeSet to refresh_token.
refresh_tokenA 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 descriptionReason
"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:

KeyValue typeValue description
access_tokenstringAn access token that can be used in the Authorization header for calling the API in place of an API key.
token_typestringHow the access token may be used: always bearer.
expires_inintThe time period (in seconds) for which the access token is valid.
refresh_tokenstringThis 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.
scopestringThe 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 headersValue
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 bodyValue
token_type_hintSet to refresh_token OR access_token.
tokenThe valid refresh_token OR access_token.

If successful, Klaviyo will return a 200:

KeyValue typeValue description
successstringTrue
errorarrayShall 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 register your integration through our submission form. 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>.

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.
  • 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 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}$'

403 HTML on /token or /revoke

If you get a 403 html error on /token or /revoke.

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 errorError messageWhy and how to fix
404This 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 errorError messageWhy and how to fix
503N/AOAuth service is temporarily unavailable on Klaviyo.
403Access to www.local-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 errorError messageWhy 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.
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.
4xxInvalid "code_verifier"This is for the authorization code flow.
The code verifier does not match with the hashed verifier code that was sent earlier.
503N/AOAuth service is temporarily unavailable on Klaviyo.

Call to OAuth/revoke

The following are errors specific to the oAuth handshake.

Type of errorError messageWhy 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.
503N/AOAuth 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 errorError messageWhy and how to fix
401authentication_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.
401not_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.
503N/AOAuth service is temporarily unavailable on Klaviyo.

Additional resources