HomeGuidesAPI Reference
ChangelogHelp CenterCommunityContact Us
Guides

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).

OAuth authorization code flow diagram

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 parameterDescription
response_typeSet to code. Represents the authorization code provided to retrieve the access token.
client_idThe client ID of your app.
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/manage-apps.
scopeA 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.
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 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.

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_verifierand code_challenge .

code_verifiercode_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 and code_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:

  1. Store your code_verifier in a data store (database or cache) along with a unique customer identifier.
  2. Pass the unique customer identifier as state in your authorization request. After the user has authorized your app, state is returned alongside the authorization code.
  3. When you are exchanging your code for an access token, pass in the code_verifier corresponding to the code. Then, use the state that was passed alongside the code to look up the code_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 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.

🚧

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 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.

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 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.

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 bodyValue
grant_typeMust be authorization_code.
codeThe authorization code that is the query parameter in the request to the redirect URL (see Authorization is allowed).
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 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:

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.

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 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.

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 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.

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 bodyValue
token_type_hint(Optional) Must be access_token or refresh_token.
tokenThe 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)