HomeGuidesAPI Reference
ChangelogHelp CenterCommunityContact Us
API Reference

Custom Objects API overview

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:read
  • custom-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 DRAFT status. Must be set to ACTIVE before data is processed into object records. You can only modify a schema while it's in DRAFT.
  • 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 (called Text in the UI): Max 2 KB. Source data must NOT be a boolean or array type.
  • INT (called Number (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 (called Number (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 are false, '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 integer 123. 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 address
  • phone — phone number: must be in ISO E.164 format
  • external-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:

PatternDescriptionExample
$['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_path in source mappings

The id_path field tells Klaviyo which field uniquely identifies each record. It can reference any field in your data — it doesn't have to be named id, and it can be a string, number, or any scalar type. For example, $['CouponIdentifier'], $['ProductID'], or $['subscription_id'] are all valid. The value extracted by id_path is converted to a string and stored in the built-in id property (property ID 0) on the resulting object record. The id_path is 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 the id_path and the data_path JSONPath values.

📘

Mixing top-level and array paths

When using array wildcards, you can also reference top-level fields (e.g., $['email']) in the data_path of 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 revision revision: 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 visibility to "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-schema ID (01KJ5HFAKYX5N1W2A9TB1RDQ86 in 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: email, 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:

  1. Navigate to the Object Manager to create a new data source and object.
  2. Give your object a unique name and either select an existing data source or create a new one.
  3. Define the object's schema and relationships using the Object Manager. For more information, check out Getting started with custom objects.
  4. 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

📘

Learn more about rate limits.

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.

ErrorCauseFix
Property IDs do not matchSchema 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 matchSchema 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 draftThe 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 foundA 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 codeReasons
400Number of records exceeded the limit (>500), or a data source identifier was not provided.
413Request payload size is too large (>4 MB).
429Rate limit exceeded. Learn more.

Profile relationship errors

ErrorCauseFix
400: Relationship must include metaThe meta field is required when creating a profile relationship.Include a meta object with at least a name field.
400: Invalid typeWrong type value.Use "profile-object-schema", not "object-schema".
400: Invalid ULIDA 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

📘

If you have feedback or encounter issues using this API, please let us know using this form.