Project · Iterable · REST API · cURL

Purchase & User Update APIs

A working reference for the three Iterable endpoints that carry the most weight in a commerce program: creating or updating a user, recording a purchase so revenue attributes correctly, and firing an abandoned-cart flow. One customer, Jane Doe, runs through every example.

  • Iterable
  • REST API
  • cURL
  • Custom events
  • Commerce
  • Handlebars

Creating or updating a user

Iterable uses a single endpoint for both creation and updates: an existing user has their information updated, and a user who isn't there yet is created. That removes the need to check for existence first, and it means the same call can be used by a signup form, a checkout, and a nightly sync without branching logic.

Requests can be as detailed or as minimal as the situation calls for. The example below is deliberately maximal (nested objects for acquisition, address, preferences and loyalty) to show how much structure a single profile can carry. It omits signupDate and signupSource: Iterable sets those itself when the profile is created, so they don't belong in an update. The account's own creation date and source go in custom fields instead.

POSThttps://api.iterable.com/api/users/update
{
  "userId": "usr_84213",
  "dataFields": {
    "email": "jane.doe@example.com",
    "firstName": "Jane",
    "lastName": "Doe",
    "phoneNumber": "+12065550143",
    "locale": "en-US",
    "timeZone": "America/Los_Angeles",
    "country": "US",
    "accountCreatedAt": "2026-08-19 14:32:00 +00:00",
    "accountSource": "web_checkout",
    "accountStatus": "active",
    "lifetimeValue": 248.50,
    "orderCount": 3,
    "isEmployee": false,
    "acquisition": {
      "channel": "paid_search",
      "campaign": "summer_sale_2026",
      "medium": "cpc",
      "firstTouchDate": "2026-08-12 09:04:11 +00:00"
    },
    "shippingAddress": {
      "line1": "1420 Cherry St",
      "line2": "Apt 3B",
      "city": "Seattle",
      "state": "WA",
      "postalCode": "98104",
      "country": "US"
    },
    "preferences": {
      "preferredChannel": "email",
      "preferredSendWindow": "morning",
      "weeklyDigest": true
    },
    "interests": ["running", "outdoor", "nutrition"],
    "favoriteCategories": ["footwear", "apparel"],
    "loyalty": {
      "tier": "silver",
      "points": 1840,
      "joinDate": "2026-08-19 14:32:00 +00:00",
      "nextTierThreshold": 2500
    }
  },
  "mergeNestedObjects": true
}

mergeNestedObjects is the field to watch. Left off, a nested object is replaced wholesale, so updating one loyalty field would wipe the rest of the loyalty block.

Check the data center. US-hosted projects call api.iterable.com; projects in Iterable's EU data center use api.eu.iterable.com. Paths and payloads are the same.

This assumes a userID-based or hybrid project. There, userId identifies Jane and her email is set like any other field inside dataFields. In an email-based project, email is the identifier: send it as a top-level email (a userId can go inside dataFields) and change it later with /api/users/updateEmail. A request there with only a userId fails for someone who doesn't exist yet, unless it adds "preferUserId": true, which creates the profile with a placeholder email.

Recording a purchase

Purchases don't behave like standard custom events. They use a dedicated commerce endpoint so that revenue attributes back to the campaign and template that drove it, and so the user's purchase history stays intact for segmentation. Here Jane checks out: one pair of trail shoes and two running tees.

POSThttps://api.iterable.com/api/commerce/trackPurchase
{
  "id": "order-10432",
  "user": {
    "userId": "usr_84213",
    "dataFields": {
      "lifetimeValue": 416.00,
      "orderCount": 4,
      "currentCartTotal": 0.00
    }
  },
  "items": [
    {
      "id": "shoe-214",
      "sku": "TR2-W-8",
      "name": "Trail Runner 2",
      "description": "Lightweight trail running shoe",
      "categories": ["footwear", "running"],
      "price": 118.50,
      "quantity": 1,
      "imageUrl": "https://store.example.com/images/trail-runner-2.jpg",
      "url": "https://store.example.com/products/trail-runner-2",
      "dataFields": { "color": "slate", "shoeSize": "8" }
    },
    {
      "id": "tee-088",
      "sku": "MRT-W-S",
      "name": "Merino Running Tee",
      "description": "Lightweight merino blend",
      "categories": ["apparel", "running"],
      "price": 24.50,
      "quantity": 2,
      "imageUrl": "https://store.example.com/images/merino-running-tee.jpg",
      "url": "https://store.example.com/products/merino-running-tee"
    }
  ],
  "total": 167.50,
  "campaignId": 1234567,
  "templateId": 9876543,
  "dataFields": {
    "orderChannel": "web",
    "shippingMethod": "standard"
  }
}

The total is yours to calculate. Iterable doesn't add up item prices, so the caller computes 118.50 + 2 × 24.50 = 167.50 and sends it. The same call rolls Jane's profile forward: lifetime value 248.50 → 416.00, order count 3 → 4.

Where campaignId and templateId come from. Iterable's default is time-based attribution, which credits the most recently sent campaign with an open attribution period and needs no IDs. A campaign set to attribution by campaignId gets no credit unless the call carries its campaignId, plus its templateId if the campaign runs an experiment. In a userID-based or hybrid project like this one, those IDs come from link parameters added to the email template; Iterable's iterableEmailCampaignId and iterableTemplateId cookies only work in email-based projects. They're hard-coded here for readability.

Zero out cart fields you own. trackPurchase empties shoppingCartItems, but custom fields like currentCartTotal stay as they were. Setting it to 0 in the same call keeps a stale total from rendering anywhere later.

Don't route purchases through /api/events/track. It will accept the payload and look like it worked, but revenue won't attribute, and that's the number the program gets judged on.

Abandoned cart

Abandoned carts have their own endpoint again, separate from the purchase one. Each call replaces the user's shoppingCartItems with the cart's current contents, so sending an empty items array clears it. A successful trackPurchase empties the cart too, so a buyer's stale cart never renders in a reminder.

These are the same two items Jane went on to buy above. This is the call that ran earlier in her session, while they were still sitting in her cart.

POSThttps://api.iterable.com/api/commerce/updateCart
{
  "user": {
    "userId": "usr_84213",
    "mergeNestedObjects": true,
    "dataFields": {
      "currentCartTotal": 167.50
    }
  },
  "items": [
    {
      "id": "shoe-214",
      "sku": "TR2-W-8",
      "name": "Trail Runner 2",
      "description": "Lightweight trail running shoe",
      "categories": ["footwear", "running"],
      "price": 118.50,
      "quantity": 1,
      "imageUrl": "https://store.example.com/images/trail-runner-2.jpg",
      "url": "https://store.example.com/products/trail-runner-2"
    },
    {
      "id": "tee-088",
      "sku": "MRT-W-S",
      "name": "Merino Running Tee",
      "description": "Lightweight merino blend",
      "categories": ["apparel", "running"],
      "price": 24.50,
      "quantity": 2,
      "imageUrl": "https://store.example.com/images/merino-running-tee.jpg",
      "url": "https://store.example.com/products/merino-running-tee"
    }
  ]
}
Cart changes on site→updateCart→Journey · wait→Cart reminder email

A Journey listens for the cart-update event, waits, then sends the reminder. Give it an exit rule on purchase, and anyone who checks out during the wait drops out before the email goes.

Keep the default entry limit. Every cart change fires updateCart, but by default a contact has to finish a Journey before entering it again, so a shopper who adds three items in a row still gets one reminder. Loosen the Journey's simultaneous-entries setting and each change could start its own. The email still shows the latest cart, because the template reads profile.shoppingCartItems at send time.

Check the cart before sending. Emptying a cart fires updateCart too, with "items": []. A cart-not-empty check ahead of the send step lets that person exit instead of getting a reminder about nothing.

Rendering the cart in the template

The cart arrives as an array, so the template iterates it with Handlebars rather than hard-coding item slots. The each helper repeats the enclosed table row once per item, which means a two-item cart and a nine-item cart both render correctly from one template.

Both cart fields are read with the profile. prefix. The updateCart event that starts the Journey carries its own copy of the cart, updatedShoppingCartItems, frozen at the moment of that call, so a template reading it would miss anything the shopper added after entering. profile.shoppingCartItems is the cart as it stands at send time, which is what Iterable recommends for abandoned-cart emails. The prefix also keeps references unambiguous, since a field on the triggering event wins over a profile field with the same name.

Prices need one more step. JSON numbers don't keep trailing zeros, so a price sent as 118.50 is stored as 118.5 and prints that way. numberFormat with minimumFractionDigits=2 puts the cents back at render time, and price stays a number on the profile. Keep the literal $: the helper's "currency" mode adds its own symbol, so use one or the other, never both.

Handlebars · email template
<p>You left these in your cart:</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
{{#each profile.shoppingCartItems}}
  <tr>
    <td>
      <img src="{{imageUrl}}"
           alt="{{name}}" width="72">
    </td>
    <td>
      <a href="{{url}}">{{name}}</a><br>
      Qty {{quantity}} · ${{numberFormat price minimumFractionDigits=2}}
    </td>
  </tr>
{{/each}}
</table>
<p>Cart total: ${{numberFormat profile.currentCartTotal minimumFractionDigits=2}}</p>
Try it · cart
Price output
Your cart · store.example.comto jane.doe@example.com

You left these in your cart:

Cart total
POSTupdateCart · this cart

Which endpoint, when

GoalEndpointWhy not something else
Create or update a profile/api/users/updateOne endpoint covers both, so no existence check is needed first. Email-based projects need preferUserId to create from a userId alone.
Change an email address/api/users/updateEmailIn an email-based project, users/update with a new address creates a second profile; updateEmail moves the history and fields across. UserID-based and hybrid projects set dataFields.email through users/update instead.
Record revenue/api/commerce/trackPurchaseThe events endpoint accepts it but won't attribute revenue. Also empties the cart.
Trigger abandoned cart/api/commerce/updateCartPurchase and cart are separate states; the cart endpoint is replace-on-write.
Anything else behavioral/api/events/trackCorrect for custom events that aren't commerce.
Back to Projects

© 2026 Cheyne Mosher. All rights reserved.

Back arrow to go back to main home page.