--- title: "Upsert User Data API in Insider One" slug: "upsert-user-data-api" description: "Insider One's Upsert User Data API reference: POST endpoint to insert or update user attributes/events, up to 1,000 records per request, with skip_hook control." tags: ["crm integration", "how to upsert data", "import data", "import data via api", "upsert api", "upsert data api", "upsert user data API", "user api"] updated: 2026-10-26T17:16:41Z published: 2026-10-26T17:16:41Z canonical: "academy.insiderone.com/upsert-user-data-api" --- > ## Documentation Index > Fetch the complete documentation index at: https://academy.insiderone.com/llms.txt > Use this file to discover all available pages before exploring further. # Upsert User Data > [!NOTE] > You can also use the [Upsert User Data API Wizard](/docs/upsert-user-data-api-integration-wizard) to generate an API key, configure headers, and create API payloads based on your planned user data. The Upsert User Data API creates or updates user profiles in Insider One by sending attributes and events from your backend systems. Upsert means insert or update: if a profile already exists for the identifier you send, the API updates it; if not, it creates one. This makes it the primary integration path for systems that don't use the web or mobile SDKs, offline data sources, CRM platforms, and external tools, feeding into the same unified profile Insider One uses for segmentation and personalization. You can use the Upsert User Data API for: - **Offline and historical events.** Import purchases and other events that occurred in-store, over the phone, or before the user was tracked online. Use **skip_hook** to control whether imported history triggers active Architect journeys or data streams. - **Leads from external platforms.** Push leads captured on landing pages, forms, or your platforms so you can follow up through Insider One channels. - **Segments prepared outside Insider One.** Add users directly to a static segment with **static_segment_id**, for example, an audience you built in your data warehouse. - **CRM and loyalty attributes.** Keep profile fields such as loyalty tier, age, or language in sync with your source systems, and update them whenever they change. - **Consent and channel opt-ins.** Pass GDPR opt-in and channel permissions for email, SMS, and WhatsApp from your consent or preference center, so campaigns always use the latest permissions. > [!NOTE] > You can implement IP restrictions to limit access to the Upsert API for users with a Unified Customer Database (UCD) API Key, or to control data transmission through this API. This ensures that only Upsert API requests from approved IP addresses will be accepted. If you want to make such a custom adjustment, please get in touch with the Insider One team. > [!NOTE] > To update a user’s identifier (such as email or phone number) listed in your [Identity Resolution Management Settings](/docs/identity-resolution-management-settings), use the [Update Identifiers API](https://academy.insiderone.com/docs/update-identifiers-api). The Upsert API can add or modify attributes, but will not change an existing identifier. > [!NOTE] > To clear an existing attribute through this API, set **delete_null** to **true** and send the attribute value as the string "****". An empty string or JSON null does not delete anything. You can also use the [Delete User Attribute API](/v1/docs/delete-user-attribute-api). ## Endpoint and Headers > **POST** [https://unification.useinsider.com/api/user/v1/upsert](https://unification.useinsider.com/api/user/v1/upsert) [](https://developers.insiderone.com/#453ca8fb-cd71-4900-9973-cc452416f820) > Visit our Postman collection to test this request. | Header | Sample Value | Description | | --- | --- | --- | | X-PARTNER-NAME | mybrand | This is your partner name. Navigate to **Inone Settings** > [**Account Settings**](https://academy.insiderone.com/docs/inone-account-settings) to copy your partner name. The partner name should be lowercase. | | X-REQUEST-TOKEN | 1a2b3c4e5d6f | This key is required to authorize your request. Refer to [API Authentication Tokens](https://academy.insiderone.com/docs/api-authentication-tokens#generate-api-key) to generate your token. | | Content-Type | application/json | This header specifies the media type of the resource. | ## Body Parameters You can send two types of data to our data ingestion API: attributes and events. > [!WARNING] > Each user object must include an **identifier** and at least one non-empty **attributes** object, **events array**, or **object_attributes** object. #### Attributes - An [**attribute**](/docs/default-attributes) is a single, static piece of information about a user, such as gender, age, or loyalty class. - For array attributes, new values are appended by default. - To overwrite instead of append, include **"not_append": true** or **"append": false** in the request. - If both are included, "append" takes priority. - To remove an attribute from the profile, send its value as **""** together with **"delete_null": true**. > [!NOTE] > To update an Array of Objects attribute, such as a list of subscriptions or loyalty records, use action-based operations (add, merge, replace, and remove) instead of setting the value directly. See [Manage Array of Objects Attributes via the Upsert API](/v1/docs/manage-array-of-objects-attributes-via-the-upsert-api) for the full syntax and examples. > [!NOTE] > No default attribute is mandatory. If you use the **attributes** object in the request, you need to have at least one attribute in your object. > [!NOTE] > When the same attribute is sent again via Upsert API, Unified Customer Database (UCD) stores the latest written value for that attribute on the [User Profile](/v1/docs/user-profiles-overview). Earlier values are not versioned on the profile and are not included in raw profile exports after they are overwritten. #### Events - An [**event**](/docs/default-events) is a collection of information, such as purchased products or visited stores. - If you send multiple updates for the same user with the same event name, the new event will be added to the existing collection. > [!NOTE] > You can use the **event group ID** to group events of the same type when sending multiple events of this type. > [!NOTE] > When sending events via the Upsert API, ensure that the values fall within the TTL (Time-to-Live) of the events. Events with timestamps exceeding the TTL will not be written. For further information, refer to the [Data Retention](https://academy.insiderone.com/docs/data-retention) article. ### Import historical data with the Upsert API The Upsert API allows you to **import** historical data. When you do this, keep in mind: If an active [Architect On Event](/v1/docs/architect-journey-starter-on-event) element or a set data stream is present, and the imported data meets their conditions, the journey or data stream can be triggered. For example, if you send purchase events from one year ago via the Upsert API and an Architect journey is active with an On Event condition set to 'purchase', those users will enter the journey, even though the events occurred a year ago. A boolean flag called **skip_hook** is available in the Upsert request body. This flag controls whether historical data should trigger journeys or data streams. This flag is not mandatory. Its rules are: - **skip_hook: *false*** → Default if not specified. Imported data can trigger Architect On Event elements and data streams. - **skip_hook: *true*** → Prevents imported data from triggering Architect journeys or data streams. ### Remove attributes with the Upsert API A boolean flag called **delete_null** is available in the Upsert request body. When it is **true**, every attribute sent with the value **""** is removed from the user profile. This lets you keep Insider One in sync with your source system in a single request, without a separate deletion call. The flag is not mandatory and defaults to false. | delete_null | Value sent | Result | | --- | --- | --- | | true | "" | The attribute is removed from the profile. | | true | Any other value | The attribute is updated as usual. | | false or not sent | "" | The text is stored as the attribute value. | | true or false | JSON null or empty string | Ignored. The existing value is kept. | - The value must be the string **""** for every data type, including number, date, boolean, and URL attributes. - To clear an array attribute, send the string **""** as the value, not an array that contains it. - Custom attributes are supported. Send them inside the **custom** object in the same way. - One request can update some attributes and remove others. - **delete_null** applies to attributes only. It does not change events or event parameters. - To clear a single key of an object attribute, see Object Type Attributes. - Removal cannot be undone. Send **"delete_null": true** only in requests that are meant to remove data > [!NOTE] > Identifiers must be placed under the **identifiers** object only. They should not be sent within attributes, events, or any other part of the payload. This structure ensures accurate identity resolution and consistent data processing across the platform. Insider One allows these parameters to be sent: | Parameter | Description | Data Type | Required | | --- | --- | --- | --- | | users | Array of user information, such as identifiers, attributes, and events. | Array | Yes | | identifiers | User's identifier information. | Object | Yes | | attributes | Attributes object of user information, such as gender and age. | Object | No | | email | Attribute. User's email address; can be used as an **identifier**. | String | No | | phone_number | Attribute. User's phone number in **E.164 format** (e.g., +6598765432), can be used as an **identifier.** | String | No | | email_optin | Attribute. User's permission for marketing emails: **True** = emails allowed; **False** = email not allowed | Boolean | No | | gdpr_optin | Attribute. User's permission for Insider One campaigns, data collection, and processing: **False** = user will not see any Insider One campaign or receive any message from any channel; **True** or empty = Insider One may interact with the user through personalization campaigns | Boolean | No | | sms_optin | Attribute. User's permission for SMS: **True** = SMS allowed; **False** = SMS not allowed | Boolean | No | | whatsapp_optin | Attribute. User's permission for WhatsApp Message: **True** = WhatsApp Message allowed **False** = WhatsApp Message not allowed | Boolean | No | | name | Attribute. User's name. | String | No | | surname | Attribute. User's surname. | String | No | | birthday | Attribute. User's birthday in **RFC 3339 format** (e.g. 1993-03-12T00:00:00Z). **Note:** Ensure the birthday ends with **Z** to indicate UTC time; no other time offset is included. | Date/Time | No | | gender | Attribute. Gender of the user | String | No | | age | Attribute. Age of the user | Number | No | | language | Language information of the user | String | No | | country | Attribute. The user's country information in **ISO 3166-1 alpha-2 format**. | String | No | | city | Attribute. City information of the user. | String | No | | static_segment_id | Attribute. Static segment IDs (users are added directly). | Numbers (Number array) | No | | uuid | Attribute. User’s UUID, can be used as an identifier. | String | No | | error_callback_endpoint | An endpoint to notify you if your upsert request fails on our end due to a unification error. Requests that receive an error are not retried. | URL | No | | delete_null | When **true**, attributes sent with the value **""** are removed from the user profile. Default is **false**. | Boolean | No | | locale | User's locale information | String | No | | events | List of events | Array | No | | event_name | Name of the event | String | Yes | | timestamp | Event time, the purchase date for the purchase event in **RFC3339 format**. | Datetime | Yes | | event_params | Event parameters for purchase-related properties. Each string value can be up to 50,000 characters. See [Limitations](/v1/docs/upsert-user-data-api#limitations). | Object | No | | custom | Object consisting of custom event parameters | Object | No | | event_group_id | Event group ID. Keep one shared **event_group_id** per order, but avoid sending multiple purchase lines with the same timestamp when they must be stored as separate rows. | String | No (Yes only when the event_name is **purchase** or **cart_page_view**) | | product_id | Event parameter. Unique product ID. | String | No | | name | Event parameter. Name of the product. | String | No | | taxonomy | Event parameter. Category tree of the product. | Array | No | | url | Event parameter. Product URL for purchase events | URL | No | | currency | Event parameter. Currency used for product pricing, in **ISO 4217 format** (e.g., USD). | String | No (Yes only when the event_name is **purchase** or **cart_page_view**) | | quantity | Event parameter. Quantity of the product. | Integer | No (Yes, only when the event_name is **purchase**) | | unit_price | Event parameter. Price of the product without any discount(s). | Float | No | | unit_sale_price | Event parameter. Price of a single unit of the product, after discounts. Insider One multiplies it by quantity to calculate purchase totals. | Float | No (Yes only when the event_name is **purchase** or **cart_page_view**) | | locale | Event parameter. Locale of the user. | String | No | | color | Event parameter. Color of the product (selected by user). | String | No | | size | Event parameter. Size of the product (selected by user). | String | No | | shipping_cost | Event parameter. Shipping cost of the items in basket. | String | No | | promotion_name | Event parameter. Name of the promotion. | String | No | | promotion_discount | Event parameter. Total amount of discount applied by promotions. | Float | No | | object_attributes | Array of Objects attributes associated with the user. Use action-based operations such as add, merge, replace, or remove. See [Manage Array of Objects Attributes via the Upsert API](/v1/docs/manage-array-of-objects-attributes-via-the-upsert-api) for the supported structure and operations. | Object | No | > If your company obtains your users' consent in compliance with the **local regulations**, make sure to pass the GDPR opt-in field to Insider One. If you want to **stop** collecting and processing the data of an identified user, GDPR opt-in field should have the *false* value. Insider One cannot be held liable if these obligations are not fulfilled and reserves the right to seek compensation from your company. > [!NOTE] > The **timestamp** determines the event’s occurrence date in Insider One. It does not default to the date when the API request is received. If you send a historical timestamp, the event is recorded in the past and might not match segments that filter for recent activity. Ensure that the timestamp reflects the event’s actual occurrence time and falls within the event’s TTL. ## Sample Requests > [!NOTE] > Keep the following [limitations](/v1/docs/upsert-user-data-api#limitations) in mind when you send requests through the Upsert API > [!NOTE] > If you send a custom attribute or parameter that doesn’t exist in the panel, it will be automatically created. However, if it’s not being created during your request, make sure that your custom attribute/parameter limit has not been reached. Once the limit is full, no new [custom attributes or parameters](/v1/docs/create-a-custom-attribute) can be created. > [!NOTE] > Use one identifier per user object where possible. If you include multiple identifiers, such as email and uuid, that belong to different existing profiles, attributes may be applied to the wrong profile. > > When you need to send multiple identifiers, verify they all belong to the same person before sending. Use **insider_id** when available; it resolves directly to a known profile without requiring cross-identifier lookup. Check **succes.count** and **fail.count** in the response body to confirm per-user outcomes, and configure **error_callback_endpoint** to be notified of any processing issues > [!NOTE] > Before you send a request: > > Identifiers and attributes are handled differently by Insider One's matching engine. Read [How Identifiers Work in Insider One](/v1/docs/how-identifiers-work-in-insider-one) to make sure your identifier fields are enabled in Identity Resolution Management (IRM) and placed in the identifiers object. This prevents the most common validation and duplicate-profile errors. The request body contains a **users** array. Each user must include an **identifiers** object. ### One user with attributes and events The sample request below shows a **purchase event** with user attributes. For more supported events and parameters, see [Default Events & Attributes](/docs/events-attributes-guide). ```bash curl --location --request POST 'https://unification.useinsider.com/api/user/v1/upsert' \ --header 'X-PARTNER-NAME: mybrand' \ --header 'X-REQUEST-TOKEN: 1a2b3c4e5d6f' \ --header 'Content-Type: application/json' \ --data-raw '{  "skip_hook": false,  "users": [    {      "identifiers": {        "email": "sample@insiderone.com",        "uuid": "1x2y3z",        "custom": {          "user_loyalty_id": "xyz123"        }      },      "attributes": {        "email_optin": true,        "age": 30,        "language": "en_US",        "static_segment_id": [          1,          2        ],        "birthday": "1980-01-01T00:00:00Z",        "custom": {          "favorite_color": [            "green"          ]        }      },      "events": [        {          "event_name": "purchase",          "timestamp": "2021-01-10T21:35:20Z",          "event_params": {            "product_id": "ABC21000",            "name": "Your Product Name",            "unit_price": 990.9,            "unit_sale_price": 890.9,            "event_group_id": "ORDER123",            "taxonomy": [              "Electronic",              "Phone"            ],            "currency": "USD",            "quantity": 1          }        },        {          "event_name": "account_created",          "timestamp": "2021-01-17T14:41:21+03:00",          "event_params": {            "custom": {              "customer_type": "trial",              "trial_start_date": "2021-01-17T14:39:21+03:00",              "trial_end_date": "2021-01-24T14:41:21+03:00"            }          }        }      ]    }  ] }' ``` ### Profile by insider_id, with an error callback and array overwrite The request body below includes **insider_id**, an internal ID maintained by Insider One for each user. You can find this value as the Profile ID on the [User Profiles](/docs/user-profiles-overview) page. It also includes **error_callback_endpoint,** which is triggered if the upsert request fails due to a unification error. In such cases, Insider One will resend the same payload you initially submitted for that user. ```bash curl --location --request POST 'https://unification.useinsider.com/api/user/v1/upsert' \ --header 'X-PARTNER-NAME: mybrand' \ --header 'X-REQUEST-TOKEN: 1a2b3c4e5d6f' \ --header 'Content-Type: application/json' \ --data-raw '{  "skip_hook": false,  "error_callback_endpoint":"http://your-error-callback-endpoint.yourdomain.com",  "users": [    {      "insider_id": "sampleinsiderid",      "attributes": {        "email_optin": true,        "age": 30,        "language": "en_US",        "static_segment_id": [          1,          2        ],        "birthday": "1980-01-01T00:00:00Z",        "custom": {          "favorite_color": [            "green"          ]        }      },      "not_append": true,      "events": [        {          "event_name": "purchase",          "timestamp": "2021-01-10T21:35:20Z",          "event_params": {            "product_id": "ABC21000",            "name": "Your Product Name",            "unit_price": 990.9,            "unit_sale_price": 890.9,            "event_group_id": "ORDER123",            "taxonomy": [              "Electronic",              "Phone"            ],            "currency": "USD",            "quantity": 1          }        },        {          "event_name": "account_created",          "timestamp": "2021-01-17T14:41:21+03:00",          "event_params": {            "custom": {              "customer_type": "trial",              "trial_start_date": "2021-01-17T14:39:21+03:00",              "trial_end_date": "2021-01-24T14:41:21+03:00"            }          }        }      ]    }  ] }' ``` ### Several users in one request The request below sends data for two users, each with their own attributes, events, and custom parameters. If the request fails, the same payload is returned to your **error_callback_endpoint**. ```shell curl --location --request POST 'https://unification.useinsider.com/api/user/v1/upsert' \  --header 'X-PARTNER-NAME: mybrand' \  --header 'X-REQUEST-TOKEN: 1a2b3c4e5d6f' \  --header 'Content-Type: application/json' \  --data-raw '{    "skip_hook": false,    "error_callback_endpoint":"http://your-error-callback-endpoint.yourdomain.com",    "users": [      {        "insider_id": "sampleinsiderid",        "attributes": {          "email_optin": true,          "age": 30,          "language": "en_US",          "static_segment_id": [            1,            2          ],          "birthday": "1980-01-01T00:00:00Z",          "custom": {            "favorite_color": [              "green"            ]          }        },        "not_append": true,        "events": [          {            "event_name": "purchase",            "timestamp": "2021-01-10T21:35:20Z",            "event_params": {              "product_id": "ABC21000",              "name": "Your Product Name",              "unit_price": 990.9,              "unit_sale_price": 890.9,              "event_group_id": "ORDER123",              "taxonomy": [                "Electronic",                "Phone"              ],              "currency": "USD",              "quantity": 1            }          },          {            "event_name": "account_created",            "timestamp": "2021-01-17T14:41:21+03:00",            "event_params": {              "custom": {                "customer_type": "trial",                "trial_start_date": "2021-01-17T14:39:21+03:00",                "trial_end_date": "2021-01-24T14:41:21+03:00"              }            }          }        ]      },      {        "insider_id": "sampleinsiderid_2",        "attributes": {          "email_optin": true,          "age": 23,          "language": "en_US",          "static_segment_id": [            1,            2          ],          "birthday": "1996-08-08T00:00:00Z",          "custom": {            "favorite_color": [              "blue"            ]          }        },        "not_append": true,        "events": [          {            "event_name": "purchase",            "timestamp": "2024-01-10T21:35:20Z",            "event_params": {              "product_id": "CYD21000",              "name": "Your Product Name",              "unit_price": 990.9,              "unit_sale_price": 890.9,              "event_group_id": "ORDER123",              "taxonomy": [                "Electronic",                "Phone"              ],              "currency": "USD",              "quantity": 1            }          },          {            "event_name": "account_created",            "timestamp": "2022-02-17T14:41:21+03:00",            "event_params": {              "custom": {                "customer_type": "trial",                "trial_start_date": "2021-04-17T14:39:21+03:00",                "trial_end_date": "2021-04-24T14:41:21+03:00"              }            }          }        ]      }    ]  }' ``` ### Remove attributes The request below removes the **birthday** attribute and the **custom loyalty_tier attribute**, and updates **city** in the same call. ```bash curl --location --request POST 'https://unification.useinsider.com/api/user/v1/upsert' \ --header 'X-PARTNER-NAME: mybrand' \ --header 'X-REQUEST-TOKEN: 1a2b3c4e5d6f' \ --header 'Content-Type: application/json' \ --data-raw '{  "delete_null": true,  "users": [    {      "identifiers": {        "uuid": "1x2y3z"      },      "attributes": {        "birthday": "",        "city": "Paris",        "custom": {          "loyalty_tier": ""        }      }    }  ] }' ``` > [!WARNING] > If an attribute is not removed, confirm that the request includes **"delete_null": true** and that the value is the string **""**, not JSON null or an empty string. > [!WARNING] > If an Upsert User Data API request succeeds but the expected data does not appear on the User Profile, verify field names, data types, identifiers, and event parameters using a minimal single-user payload. > > For **event data**, confirm that the event is active and available for data collection under [Attributes and Events](/v1/docs/attributes-events-page). > > If none of the attributes, events, or parameters are reflected, or if the User Profile is not created after a few hours of sending the request, please reach out to the Insider One team. ## Code Samples These samples are provided as is, as a starting point for your integration. Insider One does not guarantee that they work in every environment, so test them in your own environment before you use them in production. Each sample upserts two users: one with a purchase event and one with an SMS opt-in. Replace the users list with your own records; the samples batch any number of users. 1. Set your partner name in the **INSIDER_PARTNER_NAME** environment variable and your API key in **INSIDER_API_TOKEN**. 2. Copy the sample for your language into a file named **upsert.py** or **upsert.mjs**. 3. Run the file. Each batch prints a line such as Batch of 2: 0 failed, followed by the error for each failed user. ### Python Requires Python 3.8 or later and the requests package (pip install requests). Run python **upsert.py**. ```python """Create or update user profiles with POST /api/user/v1/upsert. Sample code, provided as is. Test it in your own environment before you use it in production. Requires Python 3.8+ and the requests package: pip install requests """ import os import time import requests URL = "https://unification.useinsider.com/api/user/v1/upsert" HEADERS = {    "X-PARTNER-NAME": os.environ["INSIDER_PARTNER_NAME"],  # Lowercase, as shown in Account Settings    "X-REQUEST-TOKEN": os.environ["INSIDER_API_TOKEN"],    "Content-Type": "application/json", } BATCH_SIZE = 1000  # Maximum users per request. Use smaller batches if a request could exceed 5 MB. def post(body, retries=3):    """Send the request. On 429 or 5xx, wait and retry up to `retries` times."""    for attempt in range(retries + 1):        response = requests.post(URL, json=body, headers=HEADERS, timeout=60)        if (response.status_code != 429 and response.status_code < 500) or attempt == retries:            return response        wait = float(response.headers.get("Retry-After", 2 ** attempt))  # 1, 2, 4 s without Retry-After        print(f"HTTP {response.status_code}: retrying in {wait:.0f}s")        time.sleep(wait) users = [    {        "identifiers": {"email": "alice@example.com"},        "attributes": {"name": "Alice", "email_optin": True, "custom": {"loyalty_tier": "gold"}},        "events": [            {                "event_name": "purchase",                "timestamp": "2026-09-30T14:05:00Z",                "event_params": {                    "product_id": "ABC21000",                    "event_group_id": "ORDER123",                    "currency": "USD",                    "quantity": 1,                    "unit_sale_price": 24.9,                },            }        ],    },    {"identifiers": {"phone_number": "+6598765432"}, "attributes": {"sms_optin": True}}, ] for start in range(0, len(users), BATCH_SIZE):    batch = users[start:start + BATCH_SIZE]    response = post({"users": batch})    if response.status_code != 200:        # Stop on any other error, such as a 403 for a wrong token.        raise SystemExit(f"Batch failed ({response.status_code}): {response.text[:300]}")    # A 200 can still contain failed users. users.0 in an error key is the first user of this batch.    fail = response.json().get("data", {}).get("fail") or {}    print(f"Batch of {len(batch)}: {fail.get('count', 0)} failed")    for key, messages in (fail.get("errors") or {}).items():        print(f"  {key}: {'; '.join(messages)}")    if fail.get("message"):  # The response lists at most 10 error groups        print(" ", fail["message"]) ``` ### Node.js Requires Node.js 18 or later. No packages are needed. Run node **upsert.mjs**. ```node-repl // Create or update user profiles with POST /api/user/v1/upsert. // Sample code, provided as is. Test it in your own environment before you use it in production. // Requires Node.js 18+ (built-in fetch). No dependencies. const URL = "https://unification.useinsider.com/api/user/v1/upsert"; const HEADERS = {  "X-PARTNER-NAME": process.env.INSIDER_PARTNER_NAME, // Lowercase, as shown in Account Settings  "X-REQUEST-TOKEN": process.env.INSIDER_API_TOKEN,  "Content-Type": "application/json", }; const BATCH_SIZE = 1000; // Maximum users per request. Use smaller batches if a request could exceed 5 MB. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // Send the request. On 429 or 5xx, wait and retry up to `retries` times. async function post(body, retries = 3) {  for (let attempt = 0; ; attempt++) {    const response = await fetch(URL, {      method: "POST",      headers: HEADERS,      body: JSON.stringify(body),      signal: AbortSignal.timeout(60000),    });    if ((response.status !== 429 && response.status < 500) || attempt === retries) return response;    const wait = Number(response.headers.get("Retry-After") ?? 2 ** attempt); // 1, 2, 4 s without Retry-After    console.warn(`HTTP ${response.status}: retrying in ${wait}s`);    await sleep(wait * 1000);  } } const users = [  {    identifiers: { email: "alice@example.com" },    attributes: { name: "Alice", email_optin: true, custom: { loyalty_tier: "gold" } },    events: [      {        event_name: "purchase",        timestamp: "2026-09-30T14:05:00Z",        event_params: {          product_id: "ABC21000",          event_group_id: "ORDER123",          currency: "USD",          quantity: 1,          unit_sale_price: 24.9,        },      },    ],  },  { identifiers: { phone_number: "+6598765432" }, attributes: { sms_optin: true } }, ]; for (let start = 0; start < users.length; start += BATCH_SIZE) {  const batch = users.slice(start, start + BATCH_SIZE);  const response = await post({ users: batch });  if (response.status !== 200) {    // Stop on any other error, such as a 403 for a wrong token.    throw new Error(`Batch failed (${response.status}): ${(await response.text()).slice(0, 300)}`);  }  // A 200 can still contain failed users. users.0 in an error key is the first user of this batch.  const fail = (await response.json()).data?.fail ?? {};  console.log(`Batch of ${batch.length}: ${fail.count ?? 0} failed`);  for (const [key, messages] of Object.entries(fail.errors ?? {})) console.log(`  ${key}: ${messages.join("; ")}`);  if (fail.message) console.log(`  ${fail.message}`); // The response lists at most 10 error groups } ``` ### What the samples do - **Batch**. Users are sent in requests of up to 1,000 users. A request can be at most 5 MB, so lower BATCH_SIZE when your users carry many attributes or events. - **Retry**. 429 and 5xx responses are retried up to three times. The sample waits for the Retry-After interval when the response includes one, and otherwise waits 1, 2, then 4 seconds. It does not pace requests, so at high volume, count your requests against the limit of 25,000 per minute. A retry after a 5xx does not duplicate events, but it can trigger journeys and data streams again. - **Stop on errors**. Any other error, such as a 403 for a wrong token, stops the sample instead of sending more requests. - **Check every user**. A 200 means Insider One accepted the request, not that every profile was written. The response can still list failed users, and some failures, such as an identifier that is not enabled in IRM, reach only your error_callback_endpoint. The sample prints each error key, such as users.1.identifiers.required, with its message. users.0 is the first user of the batch. When the response hides some errors, the sample prints the message that says so. ## Sample Responses #### 200 OK This response indicates that the **event_group_id** and **unit_sale_price** parameters are included together in the request for the purchase event. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ]            }        }    } } ``` #### 200 OK For the request below, you need to use an identifier for the user, which can be either **insider_id** or **UUID**, **email**, **phone number**, or a **custom attribute**. Besides, you can also send these attributes as attributes if any one of them is not an identifier. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ],                "users.0.identifiers.required": [                    "either insider_id or identifiers must be specified"                ],                "users.0.insider_id.required": [                    "either insider_id or identifiers must be specified"                ]            }        }    } } ``` #### 200 OK For the request below, you must have a **valid email address** in the format specified in the parameters table. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ],                "users.0.identifiers.*.regexp(^.+@.+\\..+$)": [                    "not a valid email address: sample"                ]            }        }    } } ``` #### 200 OK For the request below, you need to have the **valid UUID** in the format stated in the parameters table. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ],                "users.0.identifiers.*.type(string)": [                    "uuid must be string: 12345"                ]            }        }    } } ``` #### 200 OK For the request below, you must have a **valid identifier** in the format specified in the parameters table. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ],                "users.0.identifiers.*.in(defaults_column)": [                    "key must be one of phone_number(pn), email(em) or uuid"                ]            }        }    } } ``` #### 200 OK For the request below, you must provide a **valid phone number** in the format specified in the parameters table. ```json {    "data": {        "successful": {},        "fail": {            "count": 1,            "errors": {                "users.0.events.0.event_params.*.required": [                    "event_group_id parameter is required on purchase event",                    "unit_sale_price parameter is required on purchase event"                ],                "users.0.identifiers.*.regexp(^\\+[1-9]\\d{6,14}$)": [                    "user has an invalid phone number: 12345"                ]            }        }    } } ``` > [!NOTE] > Insider One returns a 200 status code because it indicates that the payload structure is valid and the data has been correctly sent. Since Insider One validates the payload itself, a successful HTTP response is returned as long as there are no issues with the request format. > > However, the actual ingestion of the data is evaluated separately at a profile level and is reflected in the response body, which includes a success count and a fail count. For example, a request may return status 200 with **succes.count: 0** and **fail.count: 1**, meaning that although the request structure was correct, the data could not be fully processed or written to the profile. #### 400 Bad Request If you do not include a **user** in your request, you receive an error shown below: ```json {"error":"users must be defined: bad request"} ``` #### 400 Bad Request The entire request must be no larger than 5 MB in size. If you send more than that limit, you receive an error shown below: ```json {"error": "request body is bigger than maximum allowed size 5,000,000 bytes"} ``` #### 429 Too Many Requests If you exceed the rate limits, you receive an error shown below: ```json {    "error": "rejected: too many requests" } ``` > [!NOTE] > If a 429 error occurs, consider implementing retries and reviewing your connection configuration to remain within the Upsert API's rate limits. Not retrying may prevent some data from being successfully applied. #### Error Codes - 400 Bad Request - 500 Internal Server Error - 403 Forbidden - 429 Too Many Requests > [!NOTE] > If you receive a 403 response instead of 200 OK, stop or pause the current data sending. Check your headers, which are your partner name and token, and correct any errors. Once the issues are fixed, resume your integration and retry the failed requests. If your headers are correct but you continue encountering problems, please [create a ticket](/docs/create-support-tickets) for the Insider One team. > [!NOTE] > For 5XX and 429 errors, implement a retry mechanism with exponential backoff, as these are likely short-term issues. Adding a retry feature would be highly beneficial in handling these errors effectively. ## Error reference Use this table to identify and resolve the errors this endpoint returns. Each row shows the status code, the response body you receive, what causes it, and how to fix it. | Status | JSON response | Cause | Fix | | --- | --- | --- | --- | | **200 OK** | { "data": { "successful": {}, "fail": { "count": 1, "errors": { "users.0.events.0.event_params.*.required": [ "event_group_id parameter is required on purchase event", "unit_sale_price parameter is required on purchase event" ] } } } } | The request was accepted, but one or more records inside it failed validation. In this example, a purchase event was sent without the event_group_id and unit_sale_price parameters. A 200 response does not confirm that every record was written. | Read the fail.errors object. The key gives you the exact position of the failed record, in the format users.[index].events.[index]. Add the missing parameters, then resend only the failed records. Treat any response where fail.count is greater than zero as a partial failure in your integration logic. | | **200 OK (user under fail)** | | An event parameter value is longer than 50,000 characters. | Shorten the value, or send long text through another channel, then resend the user. Characters are counted, not bytes. | | **400 Bad Request** | {"error": "users must be defined: bad request"} | The request body does not contain a users array. This is the most common result of sending a single user object at the top level instead of nesting it. | Wrap every record inside a users array, even when you are sending one user. | | **400 Bad Request** | {"error": "request body is bigger than maximum allowed size 5,000,000 bytes"} | The request body exceeds the 5 MB limit. | Split the payload into smaller batches. Each request supports a maximum of 1,000 user records, and the whole body must stay under 5 MB. Attribute-heavy or event-heavy profiles reach 5 MB well before 1,000 records, so size your batches on bytes, not record count. | | **403 Forbidden** | { "success": false, "message": "Access Denied. You do not have sufficient access to perform this action." } | X-REQUEST-TOKEN is invalid, revoked, or does not belong to the partner in X-PARTNER-NAME. Partner names are case-sensitive and must match your panel name exactly. | Stop or pause data sending before you retry. Confirm that X-PARTNER-NAME is lowercase and matches your panel name exactly, and that X-REQUEST-TOKEN is a current, active token. Resume the integration and retry the failed requests once you correct the headers. If the headers are correct and the response persists, [create a support ticket](/v1/docs/create-a-support-ticket) for the Insider One team. | | **429 Too Many Requests** | {"error": "rejected: too many requests"} | You exceeded the rate limit of 25,000 requests per minute. This limit is shared with the [Delete User Attribute API](/v1/docs/delete-user-attribute-api), so traffic from that endpoint counts against the same budget. | Implement retries with exponential backoff. Batch up to 1,000 records per request to reduce request volume. To request a higher limit, contact the Insider One team. | | **500 Internal Server Error** | { "success": false, "message": "The request processing has failed because of an unknown error." } | An unexpected error occurred on the Insider One side. The Insider One team receives an automatic alert when this response is returned. | Retry the request with exponential backoff. If the response continues, contact the Insider One team with sample requests. | ## Limitations - All functions must be executed with a simple **HTTPS POST** request. - Only new data can be inserted and updated with this API. No data can be retrieved. - The **request token** should be provided in the **request header**. If the token is incorrect, the API will not execute any operation. - The entire request must **not exceed 5 MB**. - Each request can upsert data for **a maximum of 1,000 records** (users). - The rate limit is shared with the [Delete User Attribute](https://academy.insiderone.com/docs/delete-user-attribute-api), allowing a total of 25,000 requests per minute, which means you can process up to **25 million** user records per minute with full batching. - The value of the X-PARTNER-NAME header should be **lowercase**. - Attribute removal with **delete_null** requires the string value **""**. JSON null and empty strings are ignored. - Each string value in an **event parameter** can be up to **50,000 characters**. This applies to every value in **event_params**, including each value under custom and each element of an array. If any value is longer, the whole user is rejected: the response returns 200 with the user under fail, and none of that user's events or attributes in the request are written. Other users in the request aren't affected. > The default limit shown here is a standard baseline. If your use case requires higher capacity, feel free to reach out to the Insider One team. We can adjust it to fit your needs. ## Suggested Next See [Get User Profiles](/docs/get-user-profiles-api) to verify the attributes and events you sent by querying the profile with the same user identifier. ## Related - [API Authentication Tokens](/api-authentication-tokens.md) - [API Rate Limits](/api-rate-limits.md) - [API Status and Error Codes](/api-status-and-error-codes.md) - [Insider One APIs in Postman](/insider-apis-in-postman.md) - [Data Retention](/data-retention.md)