Make API calls using OAuth
Learn how to set up OAuth for your Klaviyo integration.
Before you begin
Before you can initiate an OAuth connection, you need to create an app in Klaviyo.
Note that only owner, admin, and manager roles can create OAuth apps in Klaviyo. Learn more about user management and privileges.
You will learn
After following this guide, you will successfully make authorized API calls with OAuth. You will:
- Learn about the OAuth authorization code flow.
- Construct your app's installation URL.
- Request authorization.
- Retrieve access and refresh tokens.
- Test OAuth in Postman.
You can also learn about how to set up OAuth for your app with our official Build an OAuth app with Klaviyo course.
This guide includes code snippets from a Python example shared on our Github. You can also view our JavaScript example.
OAuth authorization code flow
Your app must use OAuth in order to be considered for Klaviyo’s Integrations Directory.
When a Klaviyo user installs your integration, they grant your app permission to use Klaviyo’s APIs on their behalf. You will be able to authenticate these APIs using access tokens. These access tokens come from integration with Klaviyo’s authorization code flow (shown below).
Construct your app's installation URL
The installation URL initiates the OAuth authorization flow for the account. This is where users will be directed to begin the installation process of your app after they click Install on your app’s listing page.
When a user installs your app, the list of permissions they’ll see will look like this:
Redirect the user to the authorization flow by constructing the following URL:
https://www.klaviyo.com/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope={scopes}&state={state}&code_challenge_method=S256&code_challenge={code_challenge}
The URL contains the following required parameters:
Query parameter | Description |
---|---|
response_type | Set to code . Represents the authorization code provided to retrieve the access token. |
client_id | The client ID of your app. |
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 the list of permissions that the user will be shown when they are authorizing the app. 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 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. |
Note: Make sure that you add your redirect URI and scopes to your app’s settings in Klaviyo.
You must use the least permissive scope set possible in compliance with our app listing requirements. If your app requests more scope permissions than necessary, it will be rejected.
Generate the code challenge and code verifier
Klaviyo requires PCKE for safely storing credentials for both public and confidential client types. PKCE consists of a code_verifier
and code_challenge
.
code_verifier | code_challenge |
---|---|
A high-entropy cryptographic random string with a length between 43 and 128 characters. Can contain letters, digits, underscores, periods, hyphens, or tildes. This should be unique for every authorization request. | The code verifier SHA-256 encoded |
Below are examples showing how to create the code_challenge
and code_verifier
:
def generate_code_challenge():
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')
return code_verifier, code_challenge
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), '+/', '-_'), '=');
You are required to generate a new
code_challenge
andcode_verifier
pair for your installation URL for every unique authorization request.
Store PKCE values across authorization requests
When the user authorizes the app, the user is redirected to your redirect_uri
with the returned authorization code (see Request authorization); you will exchange this code for an access token which will allow you to authenticate APIs on the user's behalf.
In your authorization 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 a data store (database or cache) along with a unique customer identifier. - Pass the unique customer identifier as
state
in your authorization request. After the user has authorized your app,state
is returned alongside the authorizationcode
. - When you are exchanging your
code
for an access token, pass in thecode_verifier
corresponding to thecode
. Then, use thestate
that was passed alongside thecode
to look up thecode_verifier
corresponding to the customer identifier in your data store.
The authorize()
method below generates the code_verifier
and uses state
to store the code_verifier
as a customer identifier (see our full Python example code):
pkce_data_store = {}
KLAVIYO_AUTHORIZATION_URL = "https://www.klaviyo.com/oauth/authorize"
@app.route('/oauth/klaviyo/authorize')
def authorize():
code_verifier, code_challenge = generate_code_challenge()
# Store code_verifier in datastore for later use
pkce_data_store.update({customer_id: code_verifier})
# Construct the authorization url
# Note how we pass in the customer_id as the state param so that we can look up the code_verifier in callback
auth_url = (f"{KLAVIYO_AUTHORIZATION_URL}?response_type=code&client_id={client_id}"
f"&redirect_uri={redirect_uri}&scope={scope}&code_challenge_method=S256"
f"&code_challenge={code_challenge}&state={customer_id}")
# Redirect them to Klaviyo so that they can authorize your app
return redirect(auth_url)
Example installation URL
Now that you are familiar with the required parameters, is an example of an installation URL, complete with PKCE values:
https://www.klaviyo.com/oauth/authorize?response_type=code&client_id=client_id&redirect_uri=https://your-website.com/oauth/klaviyo/callback&scope=lists:write campaigns:write metrics:read&code_challenge_method=S256&code_challenge=WZRHGrsBESr8wYFZ9sx0tPURuZgG2lmzyvWpwXPKz8U
Request authorization
When the user is directed to your app's installation URL, they will be prompted to provide permissions necessary for using the app.
Handle your app’s OAuth flow
Note that if the user is not logged into your app, they should be prompted to login. Once logged in, your app’s OAuth flow should redirect them to provide permissions needed for installation. As you develop your app, you should ensure that your app follows best practices, including handling OAuth flow properly for each possible user state.
Authorization is allowed
When the user selects allow, Klaviyo redirects to the redirect_uri
provided in your installation URL. Klaviyo will pass the following query parameters:
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.
The authorization code expires in 5 minutes. Once expired, your app should gracefully handle the installation error by providing an error message to the user indicating that the session is no longer valid.
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. |
Users should be notified with a message confirming that they have denied the permissions needed to use your app, and if they want to install the app, they’ll need to go back and select Allow to approve them.
Make sure your error messages for installation errors offer enough clarity to users and allows installation to be re-attempted after it is dismissed.
Retrieve access tokens
Once the user has authorized your integration and you have an authorization code, use a server-to-server request to exchange the code for an access token. The endpoint is https://a.klaviyo.com/oauth/token.
The client secret is private and should never be in the browser; this is a security risk that can expose all of your customer’s data. Be sure to use a server-to-server request. 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.
Structure your request with the request headers and body shown below:
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 .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. |
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 Authorization is allowed). |
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 to acquire this code. If there is a mismatch, this request will fail. |
The example callback()
method below fetches the authorization code from the returned query parameter to exchange it for an access token.
@app.route('/oauth/klaviyo/callback')
def callback():
# Get the auth code from the query param
authorization_code = request.args.get('code')
# Retrieve code_verifier from session or database
code_verifier = pkce_data_store.get(customer_id)
# Exchange code for token
headers = {
'Authorization': get_basic_authorization_header(),
'Content-Type': 'application/x-www-form-urlencoded'
}
data = {
'grant_type': 'authorization_code',
'code': authorization_code,
'code_verifier': code_verifier,
'redirect_uri': redirect_uri
}
response = requests.post(KLAVIYO_TOKEN_URL, headers=headers, data=data)
# Store the token info in your datastore
token_data_store.update({customer_id: response.json()})
make_api_call_with_refresh()
return '<pre>' + json.dumps(response.json(), indent=4) + '</pre>'
Access token considerations
- 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
.
Use the received access token
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.
Once you have successfully received an access token and refresh token, 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.
Request 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 authorization code (https://a.klaviyo.com/oauth/token), but the body is a refresh token instead.
This is 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/"
}
}
]
}
Use the following request headers and body to structure your request:
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. |
The example refresh_access_token()
method below handles requesting refresh tokens:
def refresh_access_token():
customer_id = '1234' # Retrieve this as needed
token_data = token_data_store.get(customer_id)
refresh_token = token_data.get('refresh_token')
headers = {
'Authorization': get_basic_authorization_header(),
'Content-Type': 'application/x-www-form-urlencoded'
}
data = {
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
response = requests.post(KLAVIYO_TOKEN_URL, headers=headers, data=data)
print('<pre>' + json.dumps(response.json(), indent=4) + '</pre>')
if response.status_code == 200:
# Update the stored token info
token_data_store.update({customer_id: response.json()})
if response.status_code == 400 and response.json().get('error') == 'invalid_grant':
# 1. Disconnect the integration on your end since the customer will need to re-authorize
# 2. (Optional) Trigger a win back email to the customer
pass
Refresh token considerations
- 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.
For a complete list of OAuth errors including refresh token errors and troubleshooting tips to resolve them, consult our OAuth troubleshooting guide.
Invalid grant error
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, including:
- The customer uninstalled your app in Klaviyo,
- The token expired after 90 days of no-use
- The token has been revoked by Klaviyo's internal systems for security reasons
- The token is incorrect.
You can treat all invalid_grant
responses, regardless of the error_description
, like the application has been uninstalled by the user. Learn about revoking tokens below.
Revoke access and refresh tokens
To prevent invalid grant errors and protect the security of your customers, make an API call to revoke any existing refresh and access tokens for the user (https://a.klaviyo.com/oauth/revoke).
Structure your request with the request headers and body shown below:
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 .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. |
Request body | Value |
---|---|
token_type_hint | (Optional) Must be access_token or refresh_token . |
token | The access or refresh token to be revoked. |
The example revoke_refresh_token()
method below calls the revoke endpoint to remove a refresh token:
def revoke_refresh_token():
url = "https://a.klaviyo.com/oauth/revoke"
headers = {
"Authorization": get_basic_authorization_header(),
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"token_type_hint": "refresh_token",
"token": refresh_token
}
response = requests.post(url, headers=headers, data=data)
```
After a token is revoked for an account, the app should be removed from the account’s integration page in Klaviyo, and the user can re-install it if they wish.
Learn about how to set up your app’s uninstallation flow and other flows applicable to your app in our [guide on handling your app’s OAuth flow](https://developers.klaviyo.com/en/docs/handle_your_apps_oauth_flow).
## Test OAuth in Postman
To test OAuth in Postman, download our latest [Postman collection](https://www.postman.com/klaviyo/klaviyo-developers/overview) and follow the steps below:
1. In Klaviyo, navigate to the _Manage apps_ page and click **Edit** on your listed OAuth app (three dots menu).
2. Make sure that your app has `events:read` and `events:write scopes`. Copy the client ID and client secret.
3. Add Postman's callback URL (<https://oauth.pstmn.io/v1/callback>) to your redirect URLs.
4. In Postman, import the Postman collection and navigate to the topmost tab of the collection. Under _Authentication_, add your OAuth client ID and client secret.
5. Enter the _Callback URL_ that was previously added to your redirect URLs (step 3) as a redirect URI to your [OAuth client](https://www.klaviyo.com/oauth/client).
6. Click **Get New Access Token**. Use the returned token to make an API call from the Events API.
## Next steps
For troubleshooting tips and a glossary of errors, consult our [OAuth troubleshooting tips guide](https://developers.klaviyo.com/en/docs/troubleshoot_oauth_errors). Once your app has successfully configured OAuth, follow our [Pass your app review](https://developers.klaviyo.com/en/docs/pass_your_app_review) and [Submit your app for review](https://developers.klaviyo.com/en/docs/submit_your_app_for_review) guides to get your app in Klaviyo's Integrations Directory.
## Additional resources
- [Pass your app review](https://developers.klaviyo.com/en/docs/pass_your_app_review)
- [Submit your app for review](https://developers.klaviyo.com/en/docs/submit_your_app_for_review)
- [Troubleshoot OAuth errors](https://developers.klaviyo.com/en/docs/troubleshoot_oauth_errors)
Updated 6 days ago