What are custom objects?
Custom objects let you sync structured data into Klaviyo and link it to customer profiles — things like pet records, product subscriptions, reservations, or wishlist items. Once your data is in Klaviyo, you can use it to personalize emails, build segments, and trigger flows.
Custom objects is a paid feature, only available to customers with a paid email plan.
This API requires the following scopes:
custom-objects:readcustom-objects:write
How it works
Key concepts
- Data source: Represents a collection of data from an external system where your data source records live (e.g., a table from your internal database or Shopify Subscriptions). You send JSON records as data source records to a data source in Klaviyo. A single data source can be used by multiple object types. We recommend using different data sources for different payloads, but schema is not required or enforced for any data source records.
- Object type: The kind of custom object you're creating (e.g., "Pet", "Reservation"). Creating one automatically generates a draft schema and an empty source mapping.
- Schema: Defines the structure of your object — its properties and relationships. Starts in
DRAFTstatus. Must be set toACTIVEbefore data is processed into object records. You can only modify a schema while it's inDRAFT. - Source mapping: The bridge between your data source records and your schema. Uses JSONPath expressions to extract fields from your data and map them to schema properties. Each property mapping references a data source by its
source_id, which is how the data source connects to the object type. - Profile relationship: Links each object record to a Klaviyo profile via an identifier like email, phone number, external ID, Klaviyo ID, or anonymous ID.
Data flow
Data Source Records → Data Source → Source Mapping → Schema → Custom Object Records
↓ ↓
(extracts fields (linked to profiles
via JSONPath) via relationships)
Data source records are stored when you ingest them. When you activate a schema, Klaviyo processes all existing data source records into custom object records and links them to profiles. After activation, new records are processed immediately.
Object properties
Every object can have up to 15 properties (or 30 depending on your billing plan). Each property must not exceed 8 KB in size.
Each property must be one of the following types:
STRING(calledTextin the UI): Max 2 KB. Source data must NOT be a boolean or array type.INT(calledNumber (Integer)in the UI): Supports up to 2 KB large integers. May be coerced from string type. Source data must NOT have a non-zero fractional component.FLOAT(calledNumber (Decimal)in the UI): Supports up to 2 KB large floating point numbers. May be coerced from a string type.BOOLEAN: Represents true or false. Acceptable values arefalse,'f','n','no','off','false',true,'t','y','on','yes','true', case insensitive.TIMESTAMP: Must follow Klaviyo's acceptable timestamp formats. Converted to UTC if a timezone offset is provided. Assumed UTC if no timezone offset is provided. Defaults to midnight if no time is provided.
Property names must be valid identifiers: letters, numbers, and underscores only. They cannot start with an underscore, cannot be Python reserved keywords (e.g., class, return), and cannot start with the prefix kl_ (reserved for internal use). The name id is reserved for the built-in identifier at property ID 0. Note that property names are independent from field names in your data source records — the source mapping connects them via JSONPath, so your data can have keys with spaces or special characters (e.g., "Variant Name", "$value") even though property names cannot.
The built-in id property is created automatically and does not count against your total property limit. The built-in id is populated by the id_path field in your source mapping — it stores whatever value that JSONPath extracts from your data source records (e.g., a product ID, coupon code, or subscription ID). You do not need to define a separate property for your unique identifier; the built-in id handles it.
The value extracted by id_path must be globally unique within the object type. If two records produce the same id value, the second record will overwrite the first. For example, if you have line items from different subscriptions that both use id: "00001", they will collide. In this case, use a more specific identifier (e.g., subscription_line_id) or a composite value that ensures uniqueness across all records of that object type.
If the extracted value does not match the declared property type, Klaviyo will attempt to coerce it. For example, Klaviyo will attempt to convert the string
"123"to the integer123. If coercion fails, the entire object record will be marked invalid and skipped.Array values are stored as their Python string representation. For example,
["Skis", "black"]would be stored as"['Skis', 'black']"(with square brackets and single-quoted strings). If you need a specific string format (e.g., JSON or comma-separated), convert the array to a string before ingestion. Support for native array properties is coming soon.
Profile identifiers
Custom object records can be linked to a profile via a profile relationship. When configuring the relationship mapping, you specify which field in your data identifies the profile and what type of identifier it is:
email— email addressphone— phone number: must be in ISO E.164 formatexternal-id— an ID from your external system (must already exist on the profile in Klaviyo)klaviyo-id— Klaviyo's internal profile ID
If you configure multiple profile identifiers (e.g., both email and phone) but only have one available, include the identifier you have and set the other to null. Setting a profile identifier to null in the payload will not overwrite or remove that identifier from the profile.
Updating object records
If you ingest a record with the same unique identifier as an existing record, the existing record is updated with the new values. You must provide the entire object record when updating — if you exclude any properties, those properties are set to null. If your source mapping extracts profile identifiers from the same record as object properties, you must include the profile identifiers as well. To update object properties and profile links independently, consider separating them in your record structure.
When multiple object types share a data source, each source mapping extracts independently. The "entire object record" requirement applies per object type, not per data source record. For example, if you re-send a subscription payload without the line_items array, the Subscription object updates normally, but the Line Item source mapping finds nothing to match and existing Line Item records are left untouched — missing array data does not null out or delete child records.
Relationships between object types
Currently, the Custom Objects API supports linking object records to profiles (via profile relationships). Support for linking custom objects to other custom objects (object-to-object relationships) is planned but not yet available for data ingestion. If your data model requires relationships between different object types (e.g., linking Subscription Line Items back to a Subscription), you can store a reference ID as a string property for now.
JSONPath reference
Source mappings use JSONPath expressions to extract data from your data source records. There is no limit on nesting depth — you can chain as many levels as your data requires. The bracket notation ($['field']) handles keys with spaces, special characters, and $ prefixes:
| Pattern | Description | Example |
|---|---|---|
$['field'] | Top-level field | $['reservation_id'] |
$['parent']['child'] | Nested field | $['owner']['email'] |
$['a']['b']['c'] | Deeply nested field | $['properties']['event_properties']['ProductID'] |
$['array'][*]['field'] | Field from each item in an array | $['pets'][*]['name'] |
$['special key'] | Key with spaces or special characters | $['Variant Option: Size'], $['$value'] |
You don't need to flatten or preprocess your source data. The source mapping handles extraction from nested structures and arrays. Fields in your data source records that are not referenced by any source mapping are still persisted in the data source record, but will not appear on the resulting object record. If you later add a mapping for those fields, the data will be available on the object record.
id_pathin source mappingsThe
id_pathfield tells Klaviyo which field uniquely identifies each record. It can reference any field in your data — it doesn't have to be namedid, and it can be a string, number, or any scalar type. For example,$['CouponIdentifier'],$['ProductID'], or$['subscription_id']are all valid. The value extracted byid_pathis converted to a string and stored in the built-inidproperty (property ID 0) on the resulting object record. Theid_pathis repeated in every property mapping because each mapping must identify which record it belongs to.
Array wildcards
Array wildcards (e.g.,
$['items'][*]['price']) can be used to expand a list of records from a single data source record. When expanding custom object records from JSON arrays, be sure to include wildcards in both theid_pathand thedata_pathJSONPath values.
Mixing top-level and array paths
When using array wildcards, you can also reference top-level fields (e.g.,
$['email']) in thedata_pathof both property mappings and relationship mappings. Top-level values are applied to every record extracted from the array. This is useful when a shared field like an email address or a parent ID sits outside the array in your data. See the parent-child example for a worked example.
Quick start
This walkthrough creates a "Pet" custom object from scratch using the programmatic API. By the end, you'll have pet records linked to customer profiles in Klaviyo.
The Custom Objects Definition APIs are currently in Beta (released 2026-01-15).
Beta endpoints require the revision header
revision: 2026-01-15.pre. The data ingestion endpoints (Create Data Source, Bulk Create Data Source Records) use the stable revisionrevision: 2025-07-15.
Here's the data we'll be working with — three pet records, each with an email to link to a customer profile:
[
{"id": 1, "name": "Buddy", "type": "dog", "breed": "Golden Retriever", "age": 3, "birth_datetime": "2023-03-15T00:00:00Z", "email": "[email protected]"},
{"id": 2, "name": "Whiskers", "type": "cat", "breed": "Siamese", "age": 2, "birth_datetime": "2024-06-01T00:00:00Z", "email": "[email protected]"},
{"id": 3, "name": "Nibbles", "type": "rabbit", "breed": "Holland Lop", "age": 1, "birth_datetime": "2025-01-10T00:00:00Z", "email": "[email protected]"}
]
ID format (ULID)
Identifiers in the Custom Objects API are formatted as ULIDs — 26-character alphanumeric strings, e.g.,
01JKV3YV229CP2NVMXHM5SD9N5. Save the IDs returned from each response, as you'll need them in later steps.
Step 1: Create a data source
A data source represents where your data comes from. Create one using the Create Data Source endpoint.
Set
visibilityto"private". Other visibility values are not currently available.
curl --request POST \
--url https://a.klaviyo.com/api/data-sources \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2025-07-15' \
--data '
{
"data": {
"type": "data-source",
"attributes": {
"visibility": "private",
"title": "Pet Registry",
"description": "Source of truth for customer pets"
}
}
}
'
Save the returned id — you'll need it in later steps.
Step 2: Create an object type and define its schema
Create an object type with its schema properties in a single call using the Create Object Type endpoint. This also automatically creates a source mapping.
Each property needs a unique numeric id (starting from 1) and a data type.
curl --request POST \
--url https://a.klaviyo.com/api/object-types \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-01-15.pre' \
--data '
{
"data": {
"type": "object-type",
"attributes": {
"title": "Pet",
"description": "Customer pets",
"object-schema": {
"data": {
"type": "object-schema",
"attributes": {
"properties": [
{"id": "1", "name": "name", "type": "STRING"},
{"id": "2", "name": "type", "type": "STRING"},
{"id": "3", "name": "breed", "type": "STRING"},
{"id": "4", "name": "age", "type": "INT"},
{"id": "5", "name": "birth_datetime", "type": "TIMESTAMP"}
]
}
}
}
}
}
}
'
The response includes the IDs you'll need for the remaining steps. Save the draft-schema ID from relationships:
{
"data": {
"type": "object-type",
"id": "01KJ5HFANFPC2W4XG9D4GP24J1",
"relationships": {
"draft-schema": {
"data": {
"type": "object-schema",
"id": "01KJ5HFAKYX5N1W2A9TB1RDQ86"
}
}
}
}
}
One ID for schema, source mapping, and activation
The
draft-schemaID (01KJ5HFAKYX5N1W2A9TB1RDQ86in this example) is reused as the source mapping ID. You'll use this same ID in Step 3 (profile relationship URL), Step 4 (source mapping URL and body), and Step 6 (activation URL and body).
Step 3: Create a profile relationship
Every custom object must be linked to Klaviyo profiles. Create this relationship on your draft schema.
The meta object is required and must include a name for the relationship (same naming rules as properties: letters, numbers, and underscores only). The type must be "profile-object-schema". The id field must follow the format "profile-<name>", where <name> matches the name in your meta object (e.g., "profile-pet_owner" for a relationship named pet_owner). This id is distinct from the server-generated relationship_id returned in the response — the relationship_id (a ULID) is what you'll use in the source mapping in Step 4.
curl --request POST \
--url https://a.klaviyo.com/api/object-schemas/<DRAFT_SCHEMA_ID>/relationships/profile-object-schemas \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-01-15.pre' \
--data '
{
"data": [
{
"type": "profile-object-schema",
"id": "profile-pet_owner",
"meta": {
"name": "pet_owner",
"description": "The profile who owns this pet"
}
}
]
}
'
The response includes the server-generated relationship_id in meta:
{
"data": [
{
"type": "profile-object-schema",
"id": "profile-pet_owner",
"meta": {
"relationship_id": "01K5YKKQKM819VVGCPZZKGE6D4",
"name": "pet_owner",
"description": "The profile who owns this pet"
}
}
]
}
Save the relationship_id from the response — you'll need it in the next step.
Step 4: Configure the source mapping
The source mapping tells Klaviyo how to read your data source records. It connects each schema property to a field in your data using JSONPath expressions, and links records to profiles.
Every schema property must have exactly one corresponding property mapping with the same
id, and every relationship must have a corresponding relationship mapping. For example, a schema property with"id": "3"must have a property mapping with"id": "3". Mismatches will cause activation to fail.
curl --request PATCH \
--url https://a.klaviyo.com/api/source-mappings/<DRAFT_SCHEMA_ID> \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-01-15.pre' \
--data @- << 'EOF'
{
"data": {
"type": "source-mapping",
"id": "<DRAFT_SCHEMA_ID>",
"attributes": {
"property_mappings": [
{"id": "1", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['id']", "data_path": "$['name']"}},
{"id": "2", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['id']", "data_path": "$['type']"}},
{"id": "3", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['id']", "data_path": "$['breed']"}},
{"id": "4", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['id']", "data_path": "$['age']"}},
{"id": "5", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['id']", "data_path": "$['birth_datetime']"}}
],
"relationship_mappings": [
{
"relationship_id": "<RELATIONSHIP_ID_FROM_STEP_3>",
"type": "simple",
"source": {
"source_id": "<DATA_SOURCE_ID>",
"id_path": "$['id']",
"type": "profile",
"related_id_paths": [
{
"identifier_type": "email",
"path": "$['email']"
}
]
}
}
]
}
}
}
EOF
Understanding source mapping fields:
source_id: The data source ID from Step 1. This is how the source mapping connects to your data source.id_path: JSONPath to the unique identifier field in your record (e.g.,$['id']). This tells Klaviyo which field uniquely identifies each record, so it can group properties from the same record together. It's the same across all mappings for a given data source.data_path: JSONPath to the specific field value for this property (e.g.,$['name']).type: The mapping type. Use"simple"for direct field-to-property mapping.relationship_id: The ULID from the Step 3 response. This links the mapping to the profile relationship on the schema.identifier_type: How to match to a Klaviyo profile. Accepted values:phone,external-id,klaviyo-id.
Step 5: Ingest your data
Send your records using the Bulk Create Data Source Records endpoint. Each pet is a separate record:
curl --request POST \
--url https://a.klaviyo.com/api/data-source-record-bulk-create-jobs \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2025-07-15' \
--data '
{
"data": {
"type": "data-source-record-bulk-create-job",
"attributes": {
"data-source-records": {
"data": [
{
"type": "data-source-record",
"attributes": {
"record": {"id": 1, "name": "Buddy", "type": "dog", "breed": "Golden Retriever", "age": 3, "birth_datetime": "2023-03-15T00:00:00Z", "email": "[email protected]"}
}
},
{
"type": "data-source-record",
"attributes": {
"record": {"id": 2, "name": "Whiskers", "type": "cat", "breed": "Siamese", "age": 2, "birth_datetime": "2024-06-01T00:00:00Z", "email": "[email protected]"}
}
},
{
"type": "data-source-record",
"attributes": {
"record": {"id": 3, "name": "Nibbles", "type": "rabbit", "breed": "Holland Lop", "age": 1, "birth_datetime": "2025-01-10T00:00:00Z", "email": "[email protected]"}
}
}
]
}
},
"relationships": {
"data-source": {
"data": {
"type": "data-source",
"id": "<DATA_SOURCE_ID>"
}
}
}
}
}
'
This endpoint creates data source records asynchronously, so it may be up to ~15 minutes before records appear in your account.
Step 6: Activate the schema
Once everything is configured, activate the schema:
curl --request PATCH \
--url https://a.klaviyo.com/api/object-schemas/<DRAFT_SCHEMA_ID> \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-01-15.pre' \
--data '
{
"data": {
"type": "object-schema",
"id": "<DRAFT_SCHEMA_ID>",
"attributes": {
"status": "ACTIVE"
}
}
}
'
The response will show status PUBLISHING while records are being processed — this typically transitions to ACTIVE within a few minutes.
Activation validates your entire configuration. If validation fails, the schema remains in DRAFT status so you can make corrections and try again. See Troubleshooting for common errors.
That's it. After activation, Buddy, Whiskers, and Nibbles will appear as separate custom object records linked to their owners' profiles. You can now use this data in templates, flows, and segments.
Working with nested data
The quick start above uses flat records where each record maps directly to one object. But your source data doesn't need to be flat — source mappings support JSONPath expressions that can reach into nested objects and arrays.
For example, instead of sending three separate pet records, you could send a single record containing an owner and an array of pets:
{
"owner": {
"email": "[email protected]"
},
"pets": [
{"id": 1, "name": "Buddy", "type": "dog", "breed": "Golden Retriever", "age": 3, "birth_datetime": "2023-03-15T00:00:00Z"},
{"id": 2, "name": "Whiskers", "type": "cat", "breed": "Siamese", "age": 2, "birth_datetime": "2024-06-01T00:00:00Z"},
{"id": 3, "name": "Nibbles", "type": "rabbit", "breed": "Holland Lop", "age": 1, "birth_datetime": "2025-01-10T00:00:00Z"}
]
}
The only difference is in the source mapping. Use array wildcards ([*]) to extract each item from the pets array, and nested paths to reach the owner's email:
{
"data": {
"type": "source-mapping",
"id": "<DRAFT_SCHEMA_ID>",
"attributes": {
"property_mappings": [
{"id": "1", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['pets'][*]['id']", "data_path": "$['pets'][*]['name']"}},
{"id": "2", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['pets'][*]['id']", "data_path": "$['pets'][*]['type']"}},
{"id": "3", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['pets'][*]['id']", "data_path": "$['pets'][*]['breed']"}},
{"id": "4", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['pets'][*]['id']", "data_path": "$['pets'][*]['age']"}},
{"id": "5", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['pets'][*]['id']", "data_path": "$['pets'][*]['birth_datetime']"}}
],
"relationship_mappings": [
{
"relationship_id": "<RELATIONSHIP_ID>",
"type": "simple",
"source": {
"source_id": "<DATA_SOURCE_ID>",
"id_path": "$['pets'][*]['id']",
"type": "profile",
"related_id_paths": [
{
"identifier_type": "email",
"path": "$['owner']['email']"
}
]
}
}
]
}
}
}
You then pass the entire nested structure as a single data source record — no flattening or preprocessing needed. Klaviyo extracts three pet objects from the single record, each linked to the owner's profile.
Common patterns
Flat data — one object type
If your data is flat (one record per object, profile identifier at the top level), a single object type is all you need. This covers use cases like coupon codes, reservations, or product records where each record is self-contained. Follow the Quick start directly.
Parent-child data — two object types, shared data source
If your data has a parent entity with child items (e.g., a Subscription with Line Items, or an Order with Order Items), model these as two separate object types. You can use the same data source for both — each object type has its own source mapping that extracts different fields from the same data source records. When you ingest a single data source record, every object type whose source mapping references that data source will independently process the record.
Follow the Quick start once for the parent object type and once for the child, reusing the same data source from Step 1. Specifically: run Steps 2-4 for the parent, then Steps 2-4 for the child, then ingest your data in Step 5, then activate both schemas in Step 6. Activation order does not matter — you can activate the parent and child schemas in any order.
Since object-to-object relationships are not yet available, store the parent's ID as a string property on the child object type (e.g., a subscription_id property on each line item). This lets you cross-reference between the two object types.
Here is an example using a Subscription (parent) with Line Items (child). The source data looks like:
{
"subscription_id": "sub_123",
"status": "active",
"next_billing_date": "2026-03-01T00:00:00Z",
"email": "[email protected]",
"line_items": [
{"id": "li_001", "product_id": "prod_001", "price": 29.99, "quantity": 2, "subscription_id": "sub_123"},
{"id": "li_002", "product_id": "prod_002", "price": 15.00, "quantity": 1, "subscription_id": "sub_123"}
]
}
Parent source mapping (Subscription): Uses top-level JSONPaths. The id_path is $['subscription_id'].
"property_mappings": [
{"id": "1", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['subscription_id']", "data_path": "$['status']"}},
{"id": "2", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['subscription_id']", "data_path": "$['next_billing_date']"}}
]
Child source mapping (Line Item): Uses array wildcards for id_path and data_path. Note that subscription_id (property id "3") uses a top-level data_path ($['subscription_id']) paired with an array-based id_path — the top-level value is applied to every record extracted from the array. This works in both property mappings and relationship mappings.
"property_mappings": [
{"id": "1", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['line_items'][*]['id']", "data_path": "$['line_items'][*]['product_id']"}},
{"id": "2", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['line_items'][*]['id']", "data_path": "$['line_items'][*]['price']"}},
{"id": "3", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['line_items'][*]['id']", "data_path": "$['subscription_id']"}},
{"id": "4", "type": "simple", "source": {"source_id": "<DATA_SOURCE_ID>", "id_path": "$['line_items'][*]['id']", "data_path": "$['line_items'][*]['quantity']"}}
],
"relationship_mappings": [
{
"relationship_id": "<RELATIONSHIP_ID>",
"type": "simple",
"source": {
"source_id": "<DATA_SOURCE_ID>",
"id_path": "$['line_items'][*]['id']",
"type": "profile",
"related_id_paths": [
{"identifier_type": "email", "path": "$['email']"}
]
}
}
]
Both object types reference the same <DATA_SOURCE_ID>. You ingest the full nested payload as a single data source record, and Klaviyo extracts one Subscription record and two Line Item records from it.
UI-based setup
If you prefer using the UI instead of the API:
- Navigate to the Object Manager to create a new data source and object.
- Give your object a unique name and either select an existing data source or create a new one.
- Define the object's schema and relationships using the Object Manager. For more information, check out Getting started with custom objects.
- Use the provided sample payload to begin ingesting records with the Bulk Create Data Source Records endpoint.
Data ingestion reference
Create Data Source Record
Use the Create Data Source Record endpoint to ingest records one at a time. Ideal for real-time or event-driven workloads such as processing webhooks. The request body uses the same record structure as the bulk endpoint — wrap your JSON payload inside a single data-source-record object with a data-source relationship.
Rate limits and throughput:
- Rate limit tier: LARGE (75 requests/second burst, 700 requests/minute steady)
- Throughput: Up to 75 records/second and 700 records/minute
Bulk Create Data Source Records
Use the Bulk Create Data Source Records endpoint for batch imports.
Rate limits and throughput:
- Rate limit tier: SMALL (3 requests/second burst, 60 requests/minute steady)
- Throughput: Up to ~1,500 records/second and 30,000 records/minute (with 500 records per request)
- Records per request: Up to 500 records
Choosing between single and bulk endpoints:
- Use the single record endpoint for real-time, event-driven ingestion where you need to process records individually as they arrive.
- Use the bulk endpoint for batch imports where you can group multiple records together. While it has a lower rate limit, it supports much higher overall throughput when batching up to 500 records per request.
Separating object data from profile identifiers
If you're preprocessing your data before sending it to Klaviyo, you can separate object properties from profile identifiers within each record for clarity:
{
"type": "data-source-record",
"attributes": {
"record": {
"object_record": {
"id": 1,
"name": "Buddy",
"type": "dog",
"breed": "Golden Retriever",
"age": 3,
"birth_datetime": "2023-03-15T00:00:00Z"
},
"relationships": {
"email": "[email protected]"
}
}
}
}
With this structure, your source mapping property paths would use $['object_record']['id'], $['object_record']['name'], etc., and the profile identifier path would use $['relationships']['email']. This makes it explicit which fields are object data and which are used for profile linking, and can assist with updating object record attributes independent of profile links.
Payload limits
Bulk Create Data Source Records:
- Max payload size — 4 MB
- Max records per request — 500 records
All custom object records:
- Max object record size — 8 KB
- Max object property size — 2 KB
Troubleshooting
Schema activation errors
When activating a schema, the API validates your entire configuration. If validation fails, the schema remains in DRAFT status so you can fix the issue and retry.
| Error | Cause | Fix |
|---|---|---|
| Property IDs do not match | Schema properties don't have a 1:1 match with source mapping properties. | Ensure every schema property (except the built-in id at ID 0) has exactly one corresponding property mapping, and vice versa. |
| Relationship IDs do not match | Schema relationships don't match source mapping relationships. | Ensure the relationship_id in each relationship mapping matches a relationship on the schema. Retrieve relationship IDs from the schema's relationships endpoint. |
| Schema not in draft | The schema is not in DRAFT status. | Only draft schemas can be activated. To modify an active schema, create a new draft version first. |
| Data source not found | A source mapping references a data source ID that doesn't exist. | Create the data source first, or correct the source_id in your source mapping. |
Data ingestion errors
| Status code | Reasons |
|---|---|
| 400 | Number of records exceeded the limit (>500), or a data source identifier was not provided. |
| 413 | Request payload size is too large (>4 MB). |
| 429 | Rate limit exceeded. Learn more. |
Profile relationship errors
| Error | Cause | Fix |
|---|---|---|
| 400: Relationship must include meta | The meta field is required when creating a profile relationship. | Include a meta object with at least a name field. |
| 400: Invalid type | Wrong type value. | Use "profile-object-schema", not "object-schema". |
| 400: Invalid ULID | A relationship_id was provided in an invalid format. | Relationship IDs must be ULIDs (26-character strings, e.g., 01K5YKKQKM819VVGCPZZKGE6D4). When creating a new relationship, omit relationship_id — it is server-generated. |
Additional resources
- Getting started with custom objects
- How to use custom objects in templates
- How to use custom objects in flows
- How to use custom objects in segments
- Use Klaviyo's Postman collections
If you have feedback or encounter issues using this API, please let us know using this form.