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:
| v3 | v2026-03-15 | |
|---|---|---|
| Header | X-API-KEY: {key} | Authorization: Omnisend-API-Key {key} |
| Version header | Not required | Omnisend-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
| Method | v2026-03-15 semantics |
|---|---|
POST | Inserts a resource |
PATCH | Changes only the supplied fields |
PUT | Replaces the full resource |
DELETE | Removes 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:
| v3 | v2026-03-15 | |
|---|---|---|
| Next page | offset / paging.next (full URL) | paging.cursors.after (opaque token) |
| Previous page | offset / paging.previous (full URL) | paging.cursors.before (opaque token) |
| More results indicator | Value in paging.next | paging.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
| v3 | v2026-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
| v3 | v2026-03-15 | |
|---|---|---|
| Model | One global limit | Per brand, per endpoint, on a sliding window |
| Values | 400 requests/minute globally; 1 RPS per client for campaigns | 400 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 |
| Headers | X-Rate-Limit-Limit / -Remaining / -Reset | Use 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
| v3 | v2026-03-15 | Notes |
|---|---|---|
GET /v3/contacts | GET /api/contacts | Cursor pagination, new filters |
GET /v3/contacts/{contactID} | GET /api/contacts/{id} | Path parameter renamed |
POST /v3/contacts | POST /api/contacts | Upserts: 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/tags | New: add tags to contacts selected by ID, email, phone, or segment |
| — | DELETE /api/contacts/tags | New: 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
| Field | v3 | v2026-03-15 | Notes |
|---|---|---|---|
contactID | Response field | id | Renamed |
| — | segments, statuses, optIns, consents, updatedAt | Response fields | New |
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
| v3 | v2026-03-15 | Notes |
|---|---|---|
POST /v3/carts | POST /api/events | eventName: "started checkout" |
PUT /v3/carts/{cartID} | POST /api/events | Re-send the cart event with the full cart |
PATCH /v3/carts/{cartID} | POST /api/events | Re-send the cart event with the full cart |
POST /v3/carts/{cartID}/products | POST /api/events | eventName: "added product to cart" |
PUT /v3/carts/{cartID}/products/{cartProductID} | POST /api/events | Re-send with the full lineItems array |
PATCH /v3/carts/{cartID}/products/{cartProductID} | POST /api/events | Re-send with the full lineItems array |
GET /v3/carts | — | No 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 field | v2026-03-15 event property | Notes |
|---|---|---|
cartID | properties.cartID | Unchanged |
currency | properties.currency | Unchanged |
cartSum | properties.value | Integer cents become a float |
cartRecoveryUrl | properties.abandonedCheckoutURL | Renamed |
products[] | properties.lineItems[] | cartProductID has no target; identify lines by productID and productVariantID |
updatedAt | eventTime | Each 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
| v3 | v2026-03-15 | Notes |
|---|---|---|
POST /v3/orders | POST /api/events | eventName: "placed order" |
PUT /v3/orders/{orderID} | POST /api/events | Re-send the matching lifecycle event |
PATCH /v3/orders/{orderID} | POST /api/events | paid for order, order fulfilled, order canceled, order refunded |
| Historical import | POST /api/batches | endpoint: "events" |
GET /v3/orders | — | No 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 refundedororder canceledleads to incorrect reporting data. - Order properties map onto the
placed orderproperty set:orderID,orderNumber,totalPrice,subTotalPrice,totalTax,totalDiscount,currency,paymentStatus,paymentMethod,fulfillmentStatus,shippingMethod,shippingPrice,billingAddress,shippingAddress,discounts,tags,note,orderStatusURL,tracking,createdAt, andlineItems[]. OnlyorderIDis strictly required;totalPriceis required for revenue reporting. - Set
eventTimeto 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 field | v2026-03-15 event property | Notes |
|---|---|---|
orderID, orderNumber | properties.orderID, properties.orderNumber | Unchanged |
orderSum, subTotalSum, taxSum, discountSum, shippingSum | properties.totalPrice, subTotalPrice, totalTax, totalDiscount, shippingPrice | Renamed; integer cents become floats |
orderUrl | properties.orderStatusURL | Renamed |
trackingCode, courierTitle, courierUrl | properties.tracking.code, tracking.courierTitle, tracking.courierURL | Moved into the tracking object; tracking.code is documented on order fulfilled |
discountCode, discountValue, discountType | properties.discounts[].code, discounts[].amount, discounts[].type | A single discount becomes a list |
contactNote | properties.note | Renamed |
products[] | properties.lineItems[] | Renamed |
source, cancelReason, canceledDate | — | No documented target property |
ordered product complements placed order
ordered product complements placed orderProduct-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
| v3 | v2026-03-15 | Notes |
|---|---|---|
POST /v3/events | POST /api/events | Trigger by eventName instead of systemName |
POST /v3/events/{eventID} | POST /api/events | Events are addressed by name and origin, not by definition ID |
GET /v3/events | POST /api/event-metadata/query | Discover event names, origins, and properties; requires a body with category |
GET /v3/events/{eventID} | POST /api/event-metadata/query | Definitions are not addressable by ID; filter by name with events: ["..."] and includeProperties: true |
| — | POST /api/event-metadata | New: declare an event schema |
| — | PUT /api/event-metadata | New: 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 types | v2026-03-15 types |
|---|---|
int | integer, integerList |
float | float, floatList |
bool | boolean, booleanList |
string, email, url | string, stringList |
date, dateTime | date, 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
| v3 | v2026-03-15 | Notes |
|---|---|---|
GET /v3/campaigns | GET /api/campaigns | Cursor 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.templateIDis 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 atemplateIDper variant underabTest.variants.- Leave
content.email.senderEmailandcontent.email.replyToEmailout 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 returns422with codesender-email-not-available, which retrying cannot resolve. PATCH /api/campaigns/{id}andPOST /api/campaigns/{id}/sendaccept campaigns indraftstatus only; any other status returns409 Conflict.POST /api/campaigns/{id}/copycreates a new draft campaign with its own ID, namedCopy 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:
| v3 | v2026-03-15 |
|---|---|
| Counters on the campaign object | POST /api/analytics/reports — sent, failed, openedUnique, opened, openRate, clickedUnique, clicked, clickRate, unsubscribedUnique, markedAsSpamUnique, attributedOrders, attributedRevenue, totalOrders, totalRevenue |
byDevices breakdown | POST /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
| Field | v3 | v2026-03-15 | Notes |
|---|---|---|---|
campaignID | Response field | id | Renamed |
type | standart | regular | Renamed |
content, audience, sendingSettings, abTest, boosterSettings, language | — | Objects | New |
startedAt, endedAt | startDate, endDate | Timestamps | Renamed |
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
| v3 | v2026-03-15 | Notes |
|---|---|---|
GET /v3/products | GET /api/products | Offset pagination retained |
GET /v3/products/{productID} | GET /api/products/{productID} | Direct replacement |
POST /v3/products | POST /api/products | Returns 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
| v3 | v2026-03-15 | Notes |
|---|---|---|
productID | id | Renamed, required |
productUrl | url | Renamed, required |
| — | defaultImageUrl | New |
images[]{imageID,url,isDefault,variantIDs} | images[] array of URL strings | Type changed |
variants[].variantID | variants[].id | Renamed |
variants[].productUrl | variants[].url | Renamed |
variants[].price (integer cents) | variants[].price (float) | Type changed |
variants[].oldPrice (integer cents) | variants[].strikeThroughPrice (float) | Renamed, type changed |
variants[].imageID | variants[].images[] (URLs) | Replaced |
| — | variants[].description, variants[].defaultImageUrl | New |
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
| v3 | v2026-03-15 | Notes |
|---|---|---|
GET /v3/categories | GET /api/product-categories | Offset pagination, sort by title, updatedAt, createdAt |
GET /v3/categories/{categoryID} | GET /api/product-categories/{categoryID} | Direct replacement |
POST /v3/categories | POST /api/product-categories | Returns 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
| v3 | v2026-03-15 | Notes |
|---|---|---|
POST /v3/batches | POST /api/batches | Returns 201 with {batchID, totalCount} |
GET /v3/batches | GET /api/batches | endpoint query parameter required |
GET /v3/batches/{batchID} | GET /api/batches/{batchID} | Direct replacement |
GET /v3/batches/{batchID}/items | GET /api/batches/{batchID}/items | Per-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
| v3 | v2026-03-15 | |
|---|---|---|
| Items per batch | 1000 | 100 |
endpoint values | contacts, products, orders, events, categories | contacts, products, events, categories — orders removed |
| Event batches | Required a top-level eventID | Each item is a full event payload (eventName, origin, contact, properties); a top-level origin is supported |
| Statuses | pending, inProgress, finished, stopped | Unchanged |
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 operation | Consequence |
|---|---|
GET /v3/carts | Cart 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/orders | Order 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.
Updated 2 days ago