Migrate from v3 to v2026-03-15

Migrate from v3 to v2026-03-15

This guide outlines the key changes when migrating your integration from the v3 API to the v2026-03-15 API.

If you use an AI coding assistant, see Migrate from v3 with an AI agent for a ready-made skill that drives the migration from this guide.


Generic changes

Base URL

  • v3: https://api.omnisend.com/v3/
  • v2026-03-15: https://api.omnisend.com/api/

All endpoints, including contacts, products, categories, batches, and events, have moved to the /api/ base path.

Authentication

The API key header name has changed:

v3v2026-03-15
HeaderX-API-KEY: {key}Authorization: Omnisend-API-Key {key}
Version headerNot requiredOmnisend-Version: 2026-03-15 (required on every request)

v3 example:

curl --request GET \
  --url 'https://api.omnisend.com/v3/contacts' \
  --header 'X-API-KEY: YOUR-API-KEY'

v2026-03-15 example:

curl --request GET \
  --url 'https://api.omnisend.com/api/contacts' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15'

OAuth client credentials (Authorization: Bearer {access-token}) are also supported and are what scope names refer to.

HTTP methods

Methodv2026-03-15 semantics
POSTInserts a resource
PATCHChanges only the supplied fields
PUTReplaces the full resource
DELETERemoves the resource

Where v3 offered PUT and v2026-03-15 offers only PATCH (product categories), omitted fields are no longer erased.

Pagination

Contacts, campaigns, segments, and automations use cursor-based pagination:

v3v2026-03-15
Next pageoffset / paging.next (full URL)paging.cursors.after (opaque token)
Previous pageoffset / paging.previous (full URL)paging.cursors.before (opaque token)
More results indicatorValue in paging.nextpaging.hasMore: true

v2026-03-15 response:

{
  "contacts": [...],
  "paging": {
    "limit": 100,
    "hasMore": true,
    "cursors": {
      "after": "eyJpZCI6IjEyMyJ9",
      "before": "eyJpZCI6IjEifQ=="
    }
  }
}

Pass the cursor token as a query parameter: GET /api/contacts?after=eyJpZCI6IjEyMyJ9. Cursors are opaque — never construct or decode them, and do not combine after and before. Filters and sorting are encoded inside the cursor; changing them mid-pagination returns 400 Bad Request.

Products, product categories, and batches remain offset-based (offset, limit, sort), so v3 paging loops can be reused for those resources.

Error responses

v3v2026-03-15
Format{error, fields}RFC 9457 {type, title, status, detail, instance, errors}
{
  "type": "https://problems.omnisend.com/not-found",
  "title": "Not found",
  "status": 404,
  "detail": "Segment '000000000000000000000001' not found.",
  "instance": "urn:omnisend:request:550e8400-e29b-41d4-a716-446655440000",
  "errors": []
}

The errors array carries field-level failures (field, code, message) and is empty when the problem is not a validation failure. Validation failures use type: https://problems.omnisend.com/validation-failed and return every invalid field at once. Rate-limit problems may add retryAfter in seconds. Branch on type, which is a stable URI, and log instance for support requests.

410 Gone is a new status code, returned when an API version has been retired.

See Responses for the full error format and status code reference.

Rate limits

v3v2026-03-15
ModelOne global limitPer brand, per endpoint, on a sliding window
Values400 requests/minute globally; 1 RPS per client for campaigns400 requests/minute by default; /segments 100/minute for GET and DELETE and 15/minute for POST and PUT; /contacts/tags 60/minute; render endpoints 40/minute; analytics 10/minute and 55/24 hours
HeadersX-Rate-Limit-Limit / -Remaining / -ResetUse 429 responses and retryAfter for backoff

See Rate limit, timeouts, errors for the current per-endpoint limits and enforcement details.


Resource changes

Contacts

Base path: /v3/contacts/api/contacts

Endpoint changes

v3v2026-03-15Notes
GET /v3/contactsGET /api/contactsCursor pagination, new filters
GET /v3/contacts/{contactID}GET /api/contacts/{id}Path parameter renamed
POST /v3/contactsPOST /api/contactsUpserts: 201 created, 200 updated
PATCH /v3/contacts/{contactID}PATCH /api/contacts/{id}Path parameter renamed
PATCH /api/contacts?email={email}New: update by email
POST /api/contacts/tagsNew: add tags to contacts selected by ID, email, phone, or segment
DELETE /api/contacts/tagsNew: remove tags from the same selection

POST /contacts is an upsert

POST /api/contacts returns 201 when a contact is created and 200 when an existing contact matching the identifier is updated. Code that treated a repeated POST as a duplicate error must branch on the status code instead.

Field changes

Fieldv3v2026-03-15Notes
contactIDResponse fieldidRenamed
segments, statuses, optIns, consents, updatedAtResponse fieldsNew

Identifiers, channel statuses (subscribed, unsubscribed, nonSubscribed), and customProperties name/value rules are unchanged, including removing a custom property by sending "" or null.

Filters

GET /api/contacts adds updatedAtFrom and supports sort=createdAt|updatedAt with direction. tag and status cannot be combined, and updatedAtFrom cannot be combined with email, phone, status, segmentID, or tag. URL-encode + in emails as %2B.

Contacts are also written by events

POST /api/events creates or updates the contact supplied in the contact object, including customProperties, tags, optIns, optOuts, and consents. Send only the contact fields you intend to persist.

v3 example:

curl --request POST \
  --url 'https://api.omnisend.com/v3/contacts' \
  --header 'X-API-KEY: YOUR-API-KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "identifiers": [{"type": "email", "id": "[email protected]",
      "channels": {"email": {"status": "subscribed", "statusDate": "2019-05-30T14:11:12Z"}}}],
    "firstName": "Jane"
  }'

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/contacts' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'Content-Type: application/json' \
  --data '{
    "identifiers": [{"type": "email", "id": "[email protected]",
      "channels": {"email": {"status": "subscribed", "statusDate": "2019-05-30T14:11:12Z"}}}],
    "firstName": "Jane"
  }'

Carts

Carts are no longer a resource. Cart activity is sent as events.

Endpoint changes

v3v2026-03-15Notes
POST /v3/cartsPOST /api/eventseventName: "started checkout"
PUT /v3/carts/{cartID}POST /api/eventsRe-send the cart event with the full cart
PATCH /v3/carts/{cartID}POST /api/eventsRe-send the cart event with the full cart
POST /v3/carts/{cartID}/productsPOST /api/eventseventName: "added product to cart"
PUT /v3/carts/{cartID}/products/{cartProductID}POST /api/eventsRe-send with the full lineItems array
PATCH /v3/carts/{cartID}/products/{cartProductID}POST /api/eventsRe-send with the full lineItems array
GET /v3/cartsNo replacement: cart state is not readable through the API
GET /v3/carts/{cartID}No replacement
DELETE /v3/carts/{cartID}No replacement: a cart cannot be retracted
DELETE /v3/carts/{cartID}/products/{cartProductID}No replacement: re-send the cart event with the remaining lineItems

added product to cart and started checkout both contribute to the Cart abandonment automation.

Behavior changes

Cart state is no longer stored or readable through the API — the events stream is append-only, and your system is the source of truth for current cart contents. Send the full cart in lineItems on every change instead of deltas. Cart line data moves into properties: cartID, abandonedCheckoutURL, value, currency, addedItem, and lineItems[]. eventVersion is empty for cart events.

Each cart update is a separate event, so give every send its own eventID — reusing one eventID across cart updates is not a way to replace a previous cart state, and omitting it makes Omnisend generate one for you. Deduplication of the eventID and eventTime pair applies to historical events only, so retried real-time sends of the same cart can be processed more than once.

Prices and money fields change unit

v3 cartSum and cart product prices were integers in cents. The cart event properties value and lineItems[].productPrice are floats in the store currency, so 1999 becomes 19.99. Copying values unchanged inflates every amount 100×, which affects cart-abandonment emails and value-based segmentation.

Field mapping

v3 cart fieldv2026-03-15 event propertyNotes
cartIDproperties.cartIDUnchanged
currencyproperties.currencyUnchanged
cartSumproperties.valueInteger cents become a float
cartRecoveryUrlproperties.abandonedCheckoutURLRenamed
products[]properties.lineItems[]cartProductID has no target; identify lines by productID and productVariantID
updatedAteventTimeEach update is its own event

Identifying the contact

POST /api/events identifies the contact from the contact object, which needs at least one of contact.id, contact.email, or contact.phone, so no prior identification step is required for server-side sends. The "captured only for identified contacts" note on the cart event pages applies to the JavaScript API path, where the contact must already be identified in the browser.

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/events' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'Content-Type: application/json' \
  --data '{
    "eventName": "added product to cart",
    "eventID": "f1eeb5bd-736c-42c7-9cfe-6990f7f59b35",
    "origin": "api",
    "eventVersion": "",
    "contact": {"email": "[email protected]"},
    "properties": {
      "cartID": "134035",
      "abandonedCheckoutURL": "https://example.com/cart/134035",
      "value": 19.99,
      "currency": "EUR",
      "addedItem": {"productID": "373", "productTitle": "Super duper product", "productPrice": 19.99,
        "productURL": "https://example.com/p/373", "productImageURL": "https://example.com/i/373.jpg"},
      "lineItems": [{"productID": "373", "productTitle": "Super duper product", "productPrice": 19.99,
        "productURL": "https://example.com/p/373", "productImageURL": "https://example.com/i/373.jpg"}]
    }
  }'

Orders

Orders are no longer a resource. Order activity is sent as events with eventVersion: "v2".

Endpoint changes

v3v2026-03-15Notes
POST /v3/ordersPOST /api/eventseventName: "placed order"
PUT /v3/orders/{orderID}POST /api/eventsRe-send the matching lifecycle event
PATCH /v3/orders/{orderID}POST /api/eventspaid for order, order fulfilled, order canceled, order refunded
Historical importPOST /api/batchesendpoint: "events"
GET /v3/ordersNo replacement: order state is not readable through the API
GET /v3/orders/{orderID}No replacement
DELETE /v3/orders/{orderID}No replacement: order canceled records a cancellation but does not remove the order

Behavior changes

  • Status updates become distinct events instead of a field update. Send every lifecycle event you have — omitting order refunded or order canceled leads to incorrect reporting data.
  • Order properties map onto the placed order property set: orderID, orderNumber, totalPrice, subTotalPrice, totalTax, totalDiscount, currency, paymentStatus, paymentMethod, fulfillmentStatus, shippingMethod, shippingPrice, billingAddress, shippingAddress, discounts, tags, note, orderStatusURL, tracking, createdAt, and lineItems[]. Only orderID is strictly required; totalPrice is required for revenue reporting.
  • Set eventTime to the real order timestamp when importing history. Leaving it empty defaults to the current date and time, which can trigger unwanted automations.
  • Before importing historical orders, make sure no automations are configured that could message contacts based on the imported data.

Prices and money fields change unit

v3 orderSum, subTotalSum, taxSum, discountSum, and shippingSum were integers in cents. The event properties totalPrice, subTotalPrice, totalTax, totalDiscount, and shippingPrice are floats in the store currency, so 3748 becomes 37.48. Copying values unchanged inflates revenue reporting 100×.

Field mapping

v3 order fieldv2026-03-15 event propertyNotes
orderID, orderNumberproperties.orderID, properties.orderNumberUnchanged
orderSum, subTotalSum, taxSum, discountSum, shippingSumproperties.totalPrice, subTotalPrice, totalTax, totalDiscount, shippingPriceRenamed; integer cents become floats
orderUrlproperties.orderStatusURLRenamed
trackingCode, courierTitle, courierUrlproperties.tracking.code, tracking.courierTitle, tracking.courierURLMoved into the tracking object; tracking.code is documented on order fulfilled
discountCode, discountValue, discountTypeproperties.discounts[].code, discounts[].amount, discounts[].typeA single discount becomes a list
contactNoteproperties.noteRenamed
products[]properties.lineItems[]Renamed
source, cancelReason, canceledDateNo documented target property

ordered product complements placed order

Product-level segmentation ("what customers bought") and the totalOrderedProductUnits and attributedOrderedProductUnits analytics metrics are driven by the ordered product event rather than by placed order line items. Its payload carries orderID plus a single product object, so send one ordered product event per ordered line item, with eventVersion: "v2", alongside each placed order you send.

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/events' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'Content-Type: application/json' \
  --data '{
    "eventName": "paid for order",
    "origin": "api",
    "eventVersion": "v2",
    "eventTime": "2026-01-14T09:12:00Z",
    "eventID": "6f1c1d0e-2f0a-4a53-9b56-2d0a3f0b7c11",
    "contact": {"email": "[email protected]"},
    "properties": {"orderID": "4122111", "totalPrice": 37.48, "currency": "EUR", "paymentStatus": "paid"}
  }'

Custom Events

Base path: /v3/events/api/events

Endpoint changes

v3v2026-03-15Notes
POST /v3/eventsPOST /api/eventsTrigger by eventName instead of systemName
POST /v3/events/{eventID}POST /api/eventsEvents are addressed by name and origin, not by definition ID
GET /v3/eventsPOST /api/event-metadata/queryDiscover event names, origins, and properties; requires a body with category
GET /v3/events/{eventID}POST /api/event-metadata/queryDefinitions are not addressable by ID; filter by name with events: ["..."] and includeProperties: true
POST /api/event-metadataNew: declare an event schema
PUT /api/event-metadataNew: merge into an existing event schema

Requests are accepted asynchronously

POST /api/events returns 202 Accepted and the event is queued, where v3 returned 204 No Content after synchronous processing. Do not read back state immediately after sending.

Request fields: eventName and origin identify the event, contact needs at least one of id, email, or phone, and properties, eventID (UUID), eventTime (RFC 3339), and eventVersion (used by recommended events) are optional. The swagger does not mark any field as required, so validate against the endpoint reference rather than assuming.

Event discovery requires a query body

GET /v3/events listed every event definition. POST /api/event-metadata/query requires a request body with category, which selects the consumer the list is resolved for: events for the event stream, which is the closest match to the v3 list, automations for events usable as automation triggers, and segments for events usable in segment filters. A missing or unsupported category returns 400.

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/event-metadata/query' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'Content-Type: application/json' \
  --data '{
    "category": "events",
    "includeProperties": true
  }'

Optional body fields narrow the response: events filters by exact event name, origins filters by origin (OR-combined), includeProperties: true adds the nested property tree that is otherwise omitted, and excludeCustomEvents: true leaves out brand-custom events.

Which events a category exposes depends on the requested Omnisend-Version, so the same query can return a different list on another version. Brand-custom events are not version-scoped and are returned regardless of the requested version.

Events are identified by name and origin

origin is a required field. Use api for a custom store integration, or your app name for a third-party app; e-commerce platform integrations must contact Omnisend. Segment filters and automation triggers accept an optional origin, which is required only when the same event name exists under multiple origins.

Segments and automations built against your v3 events were authored without an origin, so events sent with origin: "api" do not match them. Until the rules are aligned, those segments stop filling and those automations stop enrolling contacts, and the event list, contact timeline, and event picker in the app show the same event name once per origin.

To keep existing segmentation and automation rules matching your events during migration, pass the compatibility: no-origin header on your event sends. The header is read per request, so send it on every event; origin stays required in the body. Alternatively, send with origin: "api" and update each affected segment (filters[].origin) and automation trigger (trigger.condition.origin).

Validation moves from send-time to schema-time

v3 rejected a send whose field type conflicted with the stored definition. In v2026-03-15 the send is accepted with 202, and type conflicts are reported as 409 Conflict on POST /api/event-metadata and PUT /api/event-metadata. Declare your event schemas before sending.

Once a property is explicitly defined, its type cannot be changed, and properties are never deleted by omission on PUT.

Property type changes

v3 typesv2026-03-15 types
intinteger, integerList
floatfloat, floatList
boolboolean, booleanList
string, email, urlstring, stringList
date, dateTimedate, dateList
struct, structList

Map email and url fields to string, and dateTime fields to date, before declaring schemas.

Deduplication

The eventID and eventTime pair is processed once for historical events. Real-time automation events are not deduplicated, so retries must assume at-least-once delivery.

v3 example:

curl --request POST \
  --url 'https://api.omnisend.com/v3/events' \
  --header 'X-API-KEY: YOUR-API-KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "My Custom Event",
    "systemName": "myCustomEvent",
    "email": "[email protected]",
    "fields": {"size": "M", "bust": 100, "waterproof": true}
  }'

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/events' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'compatibility: no-origin' \
  --header 'Content-Type: application/json' \
  --data '{
    "eventName": "myCustomEvent",
    "origin": "api",
    "eventTime": "2026-01-14T09:12:00Z",
    "contact": {"email": "[email protected]"},
    "properties": {"size": "M", "bust": 100, "waterproof": true}
  }'

Campaigns

Base path: /v3/campaigns/api/campaigns

Endpoint changes

v3v2026-03-15Notes
GET /v3/campaignsGET /api/campaignsCursor pagination, new filters
GET /v3/campaigns/{campaignID}GET /api/campaigns/{id}Path parameter renamed
DELETE /v3/campaigns/{campaignID}DELETE /api/campaigns/{id}Direct replacement

v2026-03-15 additionally exposes campaign create, update, send, cancel, and copy operations that v3 did not have.

Create, update, and send constraints

  • content.email.templateID is required when creating a regular email campaign; creation is rejected without it, even when the campaign is only saved as a draft. Boosters inherit the parent campaign's template, and A/B test campaigns carry a templateID per variant under abTest.variants.
  • Leave content.email.senderEmail and content.email.replyToEmail out unless a specific address is required. Omnisend then sends from the address configured for the brand and replies go to it. Which addresses a brand may send from follows from its sender setup and is not exposed by the API, so an address that is guessed or copied from another campaign is rejected. When the brand has no verified sender to fall back to, creation returns 422 with code sender-email-not-available, which retrying cannot resolve.
  • PATCH /api/campaigns/{id} and POST /api/campaigns/{id}/send accept campaigns in draft status only; any other status returns 409 Conflict.
  • POST /api/campaigns/{id}/copy creates a new draft campaign with its own ID, named Copy of: {name}, and leaves the original unchanged. Use it to edit or send again a campaign that is no longer a draft.

Statistics moved to Analytics

v3 campaign objects embedded sent, opened, clicked, bounced, complained, unsubscribed, abTestWinner, and byDevices. The v2026-03-15 campaign object contains none of them:

v3v2026-03-15
Counters on the campaign objectPOST /api/analytics/reportssent, failed, openedUnique, opened, openRate, clickedUnique, clicked, clickRate, unsubscribedUnique, markedAsSpamUnique, attributedOrders, attributedRevenue, totalOrders, totalRevenue
byDevices breakdownPOST /api/analytics/statistics — device breakdown via clientDeviceType

Reports are grouped by send date and are complete up to the last completed hour.

Field and enum changes

Fieldv3v2026-03-15Notes
campaignIDResponse fieldidRenamed
typestandartregularRenamed
content, audience, sendingSettings, abTest, boosterSettings, languageObjectsNew
startedAt, endedAtstartDate, endDateTimestampsRenamed

New list filters: nameContains, status, channel, type, parentCampaignID, createdAtFrom, createdAtTo, updatedAtFrom, updatedAtTo. sort accepts createdAt, updatedAt, or name, and direction (asc or desc) is only valid together with sort. Only documented query parameters are accepted; an unknown parameter returns 400.


Products

Base path: /v3/products/api/products

Endpoint changes

v3v2026-03-15Notes
GET /v3/productsGET /api/productsOffset pagination retained
GET /v3/products/{productID}GET /api/products/{productID}Direct replacement
POST /v3/productsPOST /api/productsReturns 201
PUT /v3/products/{productID}PUT /api/products/{productID}Still replaces the full product
DELETE /v3/products/{productID}DELETE /api/products/{productID}Direct replacement

Field changes

v3v2026-03-15Notes
productIDidRenamed, required
productUrlurlRenamed, required
defaultImageUrlNew
images[]{imageID,url,isDefault,variantIDs}images[] array of URL stringsType changed
variants[].variantIDvariants[].idRenamed
variants[].productUrlvariants[].urlRenamed
variants[].price (integer cents)variants[].price (float)Type changed
variants[].oldPrice (integer cents)variants[].strikeThroughPrice (float)Renamed, type changed
variants[].imageIDvariants[].images[] (URLs)Replaced
variants[].description, variants[].defaultImageUrlNew

Prices live on variants only: the product object has no price or strikeThroughPrice. A product requires currency, id, status, title, and url; each variant requires id, price, title, and url.

Date format

createdAt and updatedAt are documented as UTC timestamps in the form YYYY-MM-DDTHH:MM:SSZ, for example 2021-01-01T00:00:00Z. v3 accepted any RFC 3339 value, including a UTC offset such as 2021-01-01T00:00:00+02:00, so convert timestamps to UTC before sending them.

Prices change unit

v3 prices were integers in cents; v2026-03-15 prices are floats in the store currency. 1999 becomes 19.99. Copying values unchanged inflates every price 100×, which affects product blocks in emails and price-based segmentation.

v3 example:

curl --request POST \
  --url 'https://api.omnisend.com/v3/products' \
  --header 'X-API-KEY: YOUR-API-KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "productID": "123", "title": "Book1", "currency": "EUR", "productUrl": "https://shop/book1",
    "images": [{"imageID": "img1", "url": "https://shop/book1.jpg", "isDefault": true}],
    "variants": [{"variantID": "abc", "title": "Book1", "price": 500, "oldPrice": 700,
      "productUrl": "https://shop/book1", "imageID": "img1"}]
  }'

v2026-03-15 example:

curl --request POST \
  --url 'https://api.omnisend.com/api/products' \
  --header 'Authorization: Omnisend-API-Key YOUR-API-KEY' \
  --header 'Omnisend-Version: 2026-03-15' \
  --header 'Content-Type: application/json' \
  --data '{
    "id": "123", "title": "Book1", "currency": "EUR", "url": "https://shop/book1",
    "defaultImageUrl": "https://shop/book1.jpg", "images": ["https://shop/book1.jpg"],
    "variants": [{"id": "abc", "title": "Book1", "price": 5.00, "strikeThroughPrice": 7.00,
      "url": "https://shop/book1", "images": ["https://shop/book1.jpg"]}]
  }'

Product Categories

Base path: /v3/categories/api/product-categories

Endpoint changes

v3v2026-03-15Notes
GET /v3/categoriesGET /api/product-categoriesOffset pagination, sort by title, updatedAt, createdAt
GET /v3/categories/{categoryID}GET /api/product-categories/{categoryID}Direct replacement
POST /v3/categoriesPOST /api/product-categoriesReturns 201
PUT /v3/categories/{categoryID}PATCH /api/product-categories/{categoryID}Replace becomes partial update
DELETE /v3/categories/{categoryID}DELETE /api/product-categories/{categoryID}Direct replacement

Fields are unchanged: categoryID, title, createdAt, updatedAt.

Replace becomes partial update

v3 PUT /v3/categories/{categoryID} overwrote omitted fields, including resetting createdAt to the current date. PATCH /api/product-categories/{categoryID} changes only the fields you supply and preserves the rest.


Batches

Base path: /v3/batches/api/batches

Endpoint changes

v3v2026-03-15Notes
POST /v3/batchesPOST /api/batchesReturns 201 with {batchID, totalCount}
GET /v3/batchesGET /api/batchesendpoint query parameter required
GET /v3/batches/{batchID}GET /api/batches/{batchID}Direct replacement
GET /v3/batches/{batchID}/itemsGET /api/batches/{batchID}/itemsPer-item errors read from the list
GET /v3/batches/{batchID}/items/{itemID}No replacement for reading a single item: read it from the item list

Field and limit changes

v3v2026-03-15
Items per batch1000100
endpoint valuescontacts, products, orders, events, categoriescontacts, products, events, categoriesorders removed
Event batchesRequired a top-level eventIDEach item is a full event payload (eventName, origin, contact, properties); a top-level origin is supported
Statusespending, inProgress, finished, stoppedUnchanged

Import order history as events batches. Before sending a batch of events, make sure no automations are configured that could message contacts based on the imported data, to avoid duplicate messages.

GET /api/batches requires endpoint and accepts contacts, products, or events, so categories batches can be created but not listed.


No longer available in v2026-03-15

Seven v3 operations have no replacement, all of them reads or deletes of cart and order state that is now an append-only event stream:

v3 operationConsequence
GET /v3/cartsCart state is not readable through the API; your system is the source of truth
GET /v3/carts/{cartID}As above
DELETE /v3/carts/{cartID}A cart cannot be retracted
DELETE /v3/carts/{cartID}/products/{cartProductID}Re-send the cart event with the remaining lineItems instead
GET /v3/ordersOrder state is not readable through the API
GET /v3/orders/{orderID}As above
DELETE /v3/orders/{orderID}An order cannot be removed; order canceled records a cancellation, which is not the same thing

Two further v3 operations have only a partial replacement: GET /v3/events/{eventID} becomes a filtered POST /api/event-metadata/query, and a single batch item is read from GET /api/batches/{batchID}/items.


Did this page help you?