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.
Note that this feature is currently in limited availability. Please fill out this OAuth interest form if you want to be invited. We'll reach out to you if you've been accepted to the limited release.
Before you begin
Partner apps should only use OAuth access tokens to make API calls
If a customer has adopted your OAuth app, you should only be making API calls for them using the OAuth access token. Making API calls to any v1/v2 or date-versioned endpoints with either private or public API keys will result in duplicate metrics and potentially other issues for the customer account.
In addition, please note that when you are making calls to our new APIs, any values in the
service_key
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 this page: https://www.klaviyo.com/oauth/client. If you get a 404, your company has not been allowlisted for the feature.
- Please confirm that you are using the right Klaviyo account.
- If you’re in the right account, please reach out to your contact at Klaviyo and we will add your account to the allowlist.
- 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.
- 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.
- 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:red profiles:write
- 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 10 minutes (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
.
-
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/oauth/client. |
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. 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
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
}
}
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:
- 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
Partner apps should only use OAuth access tokens to make API calls
If a customer has adopted your OAuth app, you should only be making API calls for them using the OAuth access token. Making API calls to any v1/v2 or date-versioned endpoints with either private or public API keys will result in duplicate metrics and potentially other issues for the customer account.
In addition, please note that when you are making calls to our new APIs, any values in the
service_key
field will be ignored.
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
Currently, we provide a generic description in response to invalid refresh tokens. Please note that starting on December 18, 2023, we will provide more specific error descriptions in the response under
invalid_grant
. These upcoming descriptions are detailed below.
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 tokens
When you want to revoke a token, you can do so using the dedicated endpoint. This will remove the integration from Klaviyo.
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.
Now, if you wish to make your integration accessible to a wider audience and have it featured in the Klaviyo Integration Directory, you need to be a Klaviyo partner. You can apply to become a partner by filling out our tech partner application form, but note that not everyone is eligible.
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 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
.
-
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.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 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. |
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> . |
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 . |
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 about 1 year ago