HomeGuidesAPI Reference
ChangelogHelp CenterCommunityContact Us
Guides

Migrate track, identify, and subscribe to our new APIs

Learn how to migrate your code from our v1/v2 APIs to our new APIs. We’ll explain what corresponding calls to use, and any major changes.

🚧

v1/v2 legacy APIs retired on June 30, 2024.

To learn more about Klaviyo’s API lifecycle, check out our updated API versioning and deprecation policy.

Migrate track calls

Old endpointNew endpoint
Track Profile Activity (v1/v2)
If you are using the endpoint Track Profile
Activity (Legacy)
, it works the same as the v1/v2
track endpoint, with two notable differences: the HTTP method is GET, and the data is base-64-encoded in a data query param.
Create Event (v2023-10-15)
This is a server-side endpoint, which uses a private API key. See the explanation of changes below for client-side information.
{
  "token": "PUBLIC_KEY",
  "event": "Added to Cart",
  "customer_properties": {
    "$email": "EMAIL_ADDRESS",
    "$phone_number": "PHONE_NUMBER",
    "$first_name": "FIRST_NAME",
    "$last_name": "LAST_NAME",
    "$city": "CITY",
    "$region": "STATE/REGION",
    "$country": "COUNTRY",
    "$zip": "ZIP_CODE",
    "$image": "IMAGE_URL",

    "CUSTOM_PROPERTY": "CUSTOM_VALUE"
  },
  "properties": {
    "$event_id": "UNIQUE_EVENT_ID",
    "$value": 9.99,
    "ProductName": "Winnie the Pooh",
    "ProductID": "1111",
    "Categories": [
      "Fiction",
      "Children"
    ],
    "ImageURL": "http://www.example.com/path/to/product/image.png",
    "URL": "http://www.example.com/path/to/product",
    "Brand": "Kids Books",
    "Price": 9.99,
    "CompareAtPrice": 14.99
  }
}
{
  "data": {
        "type": "event",
        "attributes": {
            "profile": {
                "data": {
                    "type": "profile",
                    "attributes": {
                        "email": "EMAIL_ADDRESS",
                        "phone_number": "PHONE_NUMBER",
                        "first_name": "FIRST_NAME",
                        "last_name": "LAST_NAME",
                        "location": {
                            "city": "CITY",
                            "region": "STATE/REGION",
                            "country": "COUNTRY",
                            "zip": "ZIP_CODE"
                        },
                        "image": "IMAGE_URL",
                        "properties": {
                            "CUSTOM_PROPERTY": "CUSTOM_VALUE",
                        }
                    }
                }
            },
            "metric": {
                "data": {
                    "type": "metric",
                    "attributes": {
                        "name": "Added to Cart"
                    }
                }
            },
            "properties": {
                "ProductName": "Winnie the Pooh",
                "ProductID": "1111",
                "Categories": [
                    "Fiction",
                    "Children"
                ],
                "ImageURL": "http://www.example.com/path/to/product/image.png",
                "URL": "http://www.example.com/path/to/product",
                "Brand": "Kids Books",
                "Price": 9.99,
                "CompareAtPrice": 14.99
            },
            "value": 99.99,
            "time": "2023-07-01T00:00:00",
            "unique_id": "UNIQUE_EVENT_ID"
        }
    }

Changes from v1/v2 to new APIs include:

  • There are now two endpoints, one for server-side contexts and one for client-side contexts.

  • The server-side endpoint leverages a private API key for auth, which must be passed in via the Authorization request header.

  • If you are using the Client endpoint, it requires the public API key to be passed via the company_id query parameter and is intended to be called from client-side contexts, e.g. browsers and mobile apps. While these client endpoints are available, we typically recommend using Klaviyo.js and our mobile SDKs for this purpose instead.

  • customer_properties are now passed under profile with new identifier mapping (all identifier fields for the new endpoint are nested under data.attributes): 

    Old structureNew structure
    customer_properties.$emailprofile.data.attributes.email
    customer_properties.$phone_numberprofile.data.attributes.phone_number
    customer_properties.$anonymousprofile.data.attributes.anonymous_id
    customer_properties.$idprofile.data.attributes.external_id
    customer_properties.$exchange_idprofile.data.attributes._kx
    customer_properties.$kidprofile.data.id
  • V1/v2 has the header content-type: application/x-www-form-urlencoded, whereas the new has content-type: application/json.

Digram showing relationship between Track endpoint and Create Event endpoint, mapping their properties

Migrate identify calls

Old endpointNew endpoint
Identify Profile (v1/v2)
If you are using Identify Profile (Legacy) endpoint,
it works the same as the v1/v2 identify endpoint, with two notable differences: the HTTP method is GET, and the data
is base-64-encoded in a data query param.
Server-side, with a private API key: Use the Create or Update Profile endpoint.

Client-side, with a public API key: We recommend using Klaviyo.js to accomplish this.
{
        "token": "PUBLIC_API_KEY",
        "properties": {
            "$email": "EMAIL_ADDRESS",
            "$phone_number": "PHONE_NUMBER",
            "$first_name": "FIRST_NAME",
            "$last_name": "LAST_NAME",
            "$image": "IMAGE_URL",
            "$city": "CITY",
            "$region": "STATE/REGION",
            "$country": "COUNTRY",
            "$zip": "ZIP_CODE",
            "CUSTOM_PROPERTY": "CUSTOM_VALUE"
        }
} 
{
  "data": {
    "type": "profile",
    "id": "ID",
    "attributes": {
      "email": "EMAIL",
      "phone_number": "PHONE_NUMBER",
      "external_id": "EXTERNAL_ID",
      "anonymous_id": "ANONYMOUS_ID",
      "_kx": "_KX",
      "first_name": "FIRST_NAME",
      "last_name": "LAST_NAME",
      "organization": "ORGANIZATION",
      "title": "TITLE",
      "image": "IMAGE_URL",
      "location": {
        "address1": "ADDRESS_1",
        "address2": "ADDRESS_2",
        "city": "CITY",
        "country": "COUNTRY",
        "region": "REGION",
        "zip": "ZIP",
        "timezone": "TIMEZONE",
        "ip": "IP"
      },
      "properties": {
        "newKey": "New Value"
      }
    },
    "meta": {
      "patch_properties": {
        "append": {
          "newKey": "New Value"
        },
        "unappend": {
          "newKey": "New Value"
        },
        "unset": "skus"
      }
    }
  }
}

Changes from v1/v2 to new APIs include:

  • Identifier mapping:

    Old structureNew structure
    properties.$emaildata.attributes.email
    properties.$phone_numberdata.attributes.phone_number
    properties.$anonymousdata.attributes.anonymous_id
    properties.$iddata.attributes.external_id
    properties.$exchange_iddata.attributes._kx
    properties.$kiddata.id
  • Certain location-related fields (e.g. $city, $region, $country, etc.) have been moved under data.attributes.location.

  • Any other custom profile properties should be passed under data.attributes.properties.

  • We’ve mostly moved away from $-prefixed properties. Any still remaining must be passed through via data.attributes.properties.

Migrate subscribe profile calls

POST /api/v2/list/{LIST_ID}/subscribe

{
    "profiles": [
        {
            "email": "EMAIL_ADDRESS"
        },
        {
            "phone_number": "PHONE_NUMBER",
            "sms_consent": true
        }
    ]
}
{
  "data": {
    "type": "profile-subscription-bulk-create-job",
    "attributes": {
      "profiles": {
        "data": [
          {
            "type": "profile",
            "attributes": {
              "email": "EMAIL_ADDRESS",
              "subscriptions": {
                "email": {
                  "marketing": {
                    "consent": "SUBSCRIBED"
                  }
                }

              }
            }
          },
          {
            "type": "profile",
            "attributes": {
              "phone_number": "PHONE_NUMBER",
              "subscriptions": {
                "sms": {
                  "marketing": {
                    "consent": "SUBSCRIBED"
                  }
                }
              }
            }
          }
        ]
      }
    },
    "relationships": {
      "list": {
        "data": {
          "type": "list",
          "id": "LIST_ID"
        }
      }
    }
   }
}

Changes from v1/v2 to new APIs include:

  • sms_consent moved to subscriptions.sms.marketing.consent.
  • You must specify email consent explicitly under subscriptions.email.marketing.consent.
  • May need to chunk v1/v2 requests in order to not exceed 100 profiles per request limit on new requests.
  • LIST_ID moved to the relationships section of the payload.

Note that, at this moment, you cannot add profile properties to your Subscribe Profiles API call. If you want to add profile properties to a profile while subscribing it to a list, you can:

Migrate unsubscribe profile calls

DELETE /api/v2/list/LIST_ID/unsubscribe

{
  "emails": [
    "EMAIL_ADDRESS_1",
    "EMAIL_ADDRESS_2"
  ], 
  "phone_numbers": [
    "PHONE_NUMBER"
  ]
}
{
  "data": {
    "type": "profile-subscription-bulk-delete-job",
    "attributes": {
      "profiles": {
        "data": [
          {
            "type": "profile",
            "attributes": {
              "email": "EMAIL_ADDRESS_1",
            }
         },
          {
            "type": "profile",
            "attributes": {
              "email": "EMAIL_ADDRESS_2",
            }
          },
          {
            "type": "profile",
            "attributes": {
              "phone_number": "PHONE_NUMBER",
            }
          },
        ]
      }
    },
    "relationships": {
      "list": {
        "data": {
          "type": "list",
          "id": "LIST_ID"
        }
      }
    }
  }
}

Changes from v1/v2 to new APIs include:

  • LIST_ID moved from a path parameter in the URL to the relationships section of the payload. It is also optional on the new endpoint.

Migrate profile exclusion calls

Exclude profile from all email marketing

Old endpointNew endpoint
Exclude Profile From All Email
Payload sent as x-www-form-urlencoded.
Suppress Profiles (v2023-10-15)
POST v1/people/exclusions

'email=email-address'

POST /api/profile-suppression-bulk-create-jobs/

{
  "data": {
    "type": "profile-suppression-bulk-create-job",
    "attributes": {
      "profiles": {
        "data": [
          {
            "type": "profile",
            "attributes": {
              "email": "EMAIL-ADDRESS"
            }
          }
        ]
      }
    }
  }
}

Changes from v1/v2 to new APIs include:

  • The new endpoint operates asynchronously, whereas the v1/v2 endpoint operates synchronously.
  • In the v1/v2 endpoint, you had to exclude profiles from email one at a time. In the new endpoint, you can manually suppress up to 100 profiles from receiving marketing emails in a single API call.
  • The request for the v1/v2 endpoint needed to be sent as a form encoded payload. The new endpoint accepts JSON only.

Get excluded and unsubscribed profiles

GET v1/people/exclusions
GET /api/profiles/profiles?filter=greater-or-equal(subscriptions.email.marketing.suppression.timestamp,YYYY-MM-DD)

Changes from v1/v2 to new APIs include:

  • Filter by list suppression and time of suppression in a single API call.
  • Fetch profiles by suppression data including suppression reason, timestamp, and list ID with the filters below:
Filter descriptionFilter structureAvailable filtersExample
Get excluded profiles by timestamp of suppressionsubscriptions.email.marketing.suppression.timestamp
  • greater-than
  • greater-or-equal
  • less-than
  • less-or-equal
?filter=greater-or-equal(subscriptions.email.marketing.suppression.timestamp,YYYY-MM-DD)
Get excluded profiles by suppression reasonsubscriptions.email.marketing.suppression.reasonequals?filter=equals(subscriptions.email.marketing.suppression.reason,"UNSUBSCRIBE")

Suppression reasons: UNSUBSCRIBE, SPAM_REPORT, BOUNCE, INVALID_EMAIL, USER_INITIATED.
Get excluded profiles by timestamp of list suppressionsubscriptions.email.marketing.list_suppressions.timestamp
  • greater-than
  • greater-or-equal
  • less-than
  • less-or-equal
?filter=less-or-equal(subscriptions.email.marketing.list_suppressions.timestamp,YYYY-MM-DD)
Get excluded profiles by list suppression reasonsubscriptions.email.marketing.list_suppressions.reason equals?filter=equals(subscriptions.email.marketing.list_suppressions.reason,"UNSUBSCRIBE")

Suppression reasons: UNSUBSCRIBE, SPAM_REPORT, BOUNCE, INVALID_EMAIL, USER_INITIATED.
Get excluded profiles by list IDsubscriptions.email.marketing.list_suppressions.list_idequals?filter=greater-or-equal(subscriptions.email.marketing.list_suppressions.list_id,"LIST_ID")

Additional resources