Skip to main content

E-Commerce API

Manage your product catalog, track orders, monitor cart activity, and analyze customer RFM segments.

All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com/api - see API Overview.

Permissions​

Reading catalog, cart and order data needs analytics:read. Creating or updating products, carts and orders needs ecommerce:write. Both are grantable on an API key under Settings → API Keys.

These write routes previously required integrations:write, which is not offered on API keys because it also governs connector OAuth and warehouse-sync configuration. They still accept it, so existing callers are unaffected.

ID Naming Convention​

Joryio uses a clear naming convention for identifiers:

Your IDs (use these when creating/tracking data)​

FieldDescriptionExample
productIdYour product identifier (SKU, product ID from your e-commerce platform)"SKU-12345"
orderIdYour order identifier (order number from your platform)"ORD-2024-001"
userIdYour user identifier"user-123"

Joryio IDs (returned in API responses)​

FieldDescriptionWhen to use
idJoryio's internal ID for products/ordersReturned in API responses, use for updates/deletes
joryioUserIdJoryio's internal user ID (the 24-hex id returned in user API responses)Alternative to userId when you want to reference by Joryio's ID

Flexible User Identification​

When creating orders or tracking events, you can identify users using either:

  • userId - Your user identifier (most common)
  • joryioUserId - Joryio's internal user ID
// Using your user ID (recommended)
{ "orderId": "ORD-001", "userId": "user-123", ... }

// Using Joryio's internal user ID
{ "orderId": "ORD-001", "joryioUserId": "66a1f2c3d4e5f6a7b8c9d0e1", ... }

Authentication​

All requests require JWT authentication:

Authorization: Bearer your_jwt_token
Content-Type: application/json
X-Workspace-Id: your_workspace_id

Product Catalog​

Create Product​

Create a new product in your catalog.

POST /catalog/products

Request Body:

FieldTypeRequiredDescription
productIdstringYesYour unique product identifier (SKU, product ID)
namestringYesProduct name
pricenumberYesProduct price
descriptionstringNoProduct description
compareAtPricenumberNoOriginal price (for discounts)
currencystringNoCurrency code (default: USD)
categoriesstring[]NoProduct categories
tagsstring[]NoProduct tags
brandstringNoBrand name
imageUrlstringNoMain product image URL
urlstringNoProduct page URL
inStockbooleanNoStock availability (default: true)
skustringNoStock keeping unit
variantsobject[]NoProduct variants
customFieldsobjectNoCustom attributes

Example Request:

curl -X POST https://api-eu1.joryio.com/api/catalog/products \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productId": "SKU-12345",
"name": "Classic Blue T-Shirt",
"price": 29.99,
"compareAtPrice": 39.99,
"currency": "USD",
"categories": ["Clothing", "T-Shirts"],
"tags": ["sale", "bestseller"],
"brand": "Acme Apparel",
"imageUrl": "https://example.com/images/blue-tshirt.jpg",
"url": "https://example.com/products/blue-tshirt",
"inStock": true,
"sku": "BTS-001-BL",
"variants": [
{
"id": "var-s",
"name": "Small",
"sku": "BTS-001-BL-S",
"price": 29.99,
"inStock": true,
"options": { "size": "S", "color": "Blue" }
},
{
"id": "var-m",
"name": "Medium",
"sku": "BTS-001-BL-M",
"price": 29.99,
"inStock": true,
"options": { "size": "M", "color": "Blue" }
}
]
}'

Response:

{
"id": "prod_abc123",
"productId": "SKU-12345",
"name": "Classic Blue T-Shirt",
"price": 29.99,
"createdAt": "2024-01-20T10:00:00.000Z"
}

Bulk Upsert Products (array body)​

There is no separate bulk endpoint: POST /catalog/products accepts either a single product object or a bare JSON array of product objects (no wrapper object). The array form creates or updates up to 500 products in one request, upserted by productId.

POST /catalog/products

Request Body:

A JSON array (max 500 elements; an empty array or more than 500 returns 400). Each element follows the same format as the single-object form, including the optional per-product source field.

Every element receives full validation. An invalid element is reported in failed by its array index - it is never silently accepted - and the remaining valid elements are still upserted.

[
{ "productId": "SKU-001", "name": "Product 1", "price": 19.99 },
{ "productId": "SKU-002", "name": "Product 2", "price": 29.99, "source": "shopify" }
]

Response:

Unlike the object form (which returns the created product), the array form returns an aggregate summary:

{
"processed": 2,
"upserted": 2,
"failed": []
}
FieldDescription
processedNumber of elements received in the request array
upsertedProducts actually created or updated
failedPer-element failures: index (position in the request array), productId / sku (when present on the element), reason

List Products​

Query products with filtering and pagination.

GET /catalog/products

Query Parameters:

ParameterTypeDescription
searchstringSearch by name, description, or SKU
categoriesstring[]Filter by categories
tagsstring[]Filter by tags
brandstringFilter by brand
inStockbooleanFilter by stock status
minPricenumberMinimum price
maxPricenumberMaximum price
limitnumberResults per page (default: 50)
offsetnumberPagination offset
sortBystringSort field: name, price, createdAt, updatedAt
sortOrderstringasc or desc

Example:

curl "https://api-eu1.joryio.com/api/catalog/products?categories=T-Shirts&inStock=true&limit=20" \
-H "Authorization: Bearer $TOKEN"

Fields filled by store integrations. Product responses also carry fields that a connected store (WooCommerce) fills and the create and update routes do not accept: gtin (GTIN, UPC, EAN or ISBN, or null), attributes (attribute name to its values, for example {"Material": ["Linen"]}, or null), variants[].gtin, and sourceModifiedAt (when the store last changed the product). brand and description (plain text) are filled the same way for synced products.

Get Categories​

List all product categories.

GET /catalog/categories

Get Brands​

List all product brands.

GET /catalog/brands

Catalogue Stats​

Totals and catalogue health, counted over every product - never over a page.

GET /catalog/stats

Response:

{
"productCount": 8214,
"categoryCount": 37,
"brandCount": 12,
"outOfStockCount": 341,
"lowStockCount": 96,
"unclassifiedCount": 18,
"lowStockThreshold": 10
}
FieldDescription
outOfStockCountProducts with quantity 0, or inStock: false when no quantity is tracked
lowStockCountProducts with quantity between 1 and lowStockThreshold
unclassifiedCountProducts missing a category or a brand
lowStockThresholdUnits-remaining boundary lowStockCount was computed at

Orders​

Create Order​

Track a new order.

POST /orders

Request Body:

FieldTypeRequiredDescription
orderIdstringYesYour order ID (order number from your platform)
userIdstringEither/OrYour customer user ID
joryioUserIdstringEither/OrJoryio's internal user ID (alternative to userId)
totalnumberYesOrder total
itemsobject[]YesLine items
statusstringNoOrder status (default: pending)
paymentStatusstringNopaid or unpaid: whether the order has been paid. A Cash-on-delivery or bank-transfer order is processing and unpaid until the money arrives, and shows Awaiting payment on the Orders page instead of Paid. Leave it out and a processing / shipped / delivered order counts as paid (an existing order keeps the value it has). Revenue does not depend on it. See Paid or awaiting payment.
currencystringNoCurrency code
subtotalnumberNoSubtotal before discounts
discountnumberNoDiscount amount
shippingnumberNoShipping cost
taxnumberNoTax amount
totalRefundednumberNoAmount refunded so far, in the order currency (default 0). For a partial refund send the partial amount with status: partiallyRefunded; for a full refund set it to total with status: refunded. Refunds are netted out of attributed revenue.
refundsobject[]NoEvery refund so far (up to 100): { "id": "RF-1", "amount": 29.99, "createdAt": "2026-06-03T09:00:00Z", "reason": "Wrong size", "items": [{ "productId": "SKU-1", "quantity": 1 }] } - only amount is required. Each new refund emits one Order Refunded event (by id; without one, by the refunded total rising) and a refund already reported emits nothing again. Sent without totalRefunded, their sum is the refunded total. A move to status: cancelled (or POST /orders/:id/cancel) emits one Order Cancelled. See Refund and cancellation events.
couponCodestringNoApplied coupon
couponsobject[]NoEvery coupon code the order used (up to 50): { "code": "SUMMER10", "amount": 10.0, "type": "fixed_amount" }. amount (the discount the code gave) and type are optional. On upsert, coupons replaces the stored codes ([] clears them); leave it out to keep them. When both are sent, coupons wins over couponCode
shippingAddressobjectNoShipping address
campaignIdstringNoAttribution campaign
canvasIdstringNoAttribution canvas
sourcestringNoOrder source (email, sms, direct)
utmSourcestringNoUTM source
utmMediumstringNoUTM medium
utmCampaignstringNoUTM campaign
salesChannelstringNoWhere the order was placed: online, pos, app, phone, marketplace or other (any letter case). Any other value is a 400. Left out, a new order is online and an existing one keeps its channel. See Store and sales channel
storeIdstringNoYour own id for the store the order belongs to (a shop, a branch, a second website), up to 100 characters. The Store filter on the Orders page and the "Bought in store" segment condition match it
storeNamestringNoThe store's display name, up to 255 characters, shown with storeId
User Identification

Either userId or joryioUserId must be provided. Use userId with your own user identifiers (most common). Use joryioUserId when you have Joryio's internal user ID (the 24-hex id returned in user API responses).

Line Item Structure:

{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"sku": "BTS-001",
"quantity": 2,
"price": 29.99,
"total": 59.98,
"imageUrl": "https://example.com/image.jpg"
}

Example Request:

curl -X POST https://api-eu1.joryio.com/api/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "ORD-2024-001",
"userId": "user-123",
"total": 89.97,
"subtotal": 99.97,
"discount": 10.00,
"shipping": 5.99,
"tax": 4.99,
"currency": "USD",
"coupons": [
{ "code": "SAVE10", "amount": 10.00, "type": "fixed_amount" }
],
"items": [
{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"quantity": 2,
"price": 29.99,
"total": 59.98
},
{
"productId": "prod_def456",
"name": "Black Jeans",
"quantity": 1,
"price": 49.99,
"total": 49.99
}
],
"source": "email",
"salesChannel": "pos",
"storeId": "tlv-dizengoff",
"storeName": "Tel Aviv - Dizengoff",
"campaignId": "camp_xyz789"
}'

Store and sales channel​

Every order carries where it was sold (salesChannel) and, when it names one, which store it belongs to (storeId). A merchant with a shop and a website, or with two stores, can then filter and split orders by each and build segments such as "bought in the shop in the last 90 days".

salesChannelMeaning
onlineYour own website or web checkout (the default)
posA point-of-sale register in a physical shop
appYour own mobile app
phoneEntered by staff: phone, email or manual orders, drafts
marketplaceA third-party marketplace
otherAnything else

The values are fixed and the same everywhere: in this API, in event properties (sales_channel), in segment conditions and in the Orders CSV export.

Connected stores fill both in for you. The store is the store connection itself; the channel comes from what the platform records:

StoreSales channel
ShopifyFrom the order's source_name: web, online_store, checkout and subscription orders are online; pos is pos; shopify_draft_order, iphone and android (orders staff create in the admin or the Shopify mobile app) are phone; anything else is other
WooCommerceFrom created_via: checkout and store-api are online; admin is phone; a POS plugin is pos; rest-api and anything else is other
MagentoAn order created in the admin (no shopper IP) is phone; every other order is online
PrestaShopAlways online - PrestaShop does not record where an order was placed

An SDK Order Completed event may send sales_channel (and store_id, store_name). Without it, an order from an iOS or Android app is app and one from a website is online. Order Completed, Order Refunded and Order Cancelled events carry sales_channel and, when the order has a store, store_id, so a journey can trigger on them.

Fulfill Order​

Mark an order as shipped.

POST /orders/:id/fulfill

Request Body:

{
"trackingNumber": "1Z999AA10123456784",
"carrier": "UPS"
}

Cancel Order​

Cancel an order.

POST /orders/:id/cancel

Request Body:

{
"reason": "Customer requested cancellation"
}

Refund Order​

Process a refund.

POST /orders/:id/refund

Request Body:

{
"refundAmount": 29.99,
"reason": "Product defective",
"partial": true
}

Upsert Orders (bulk)​

POST /orders/upsert

The primary way a store that is not on Shopify, WooCommerce or Magento sends its orders. Idempotent on orderId: sending the same order again updates it rather than creating a duplicate, so a retry after a network failure is safe.

Request Body:

{
"orders": [
{
"orderId": "1001",
"userId": "user_123",
"email": "[email protected]",
"total": 89.90,
"currency": "EUR",
"placedAt": "2026-08-27T10:15:00Z",
"status": "completed",
"items": [
{ "productId": "prod_abc123", "quantity": 2, "price": 29.95 }
]
}
]
}

Response:

{ "upserted": 1, "created": 1, "updated": 0 }

Send each order in the currency it was paid in. You do not convert anything: Joryio converts total into the workspace's reporting currency at the European Central Bank rate of the order's placedAt date, and reports sum that converted amount.

Revenue Trend​

GET /orders/revenue-trend?days=30&granularity=day

Net revenue over time (refunds and cancellations removed), for pulling into a BI tool. granularity is day, week or month.

Response:

{
"currency": "EUR",
"points": [
{ "date": "2026-08-25", "revenue": 1240.50, "orders": 14 },
{ "date": "2026-08-26", "revenue": 980.00, "orders": 11 }
]
}

Order Statistics​

Get order statistics for a date range.

GET /orders/stats?startDate=2024-01-01&endDate=2024-01-31

Response:

{
"totalOrders": 156,
"totalRevenue": 12450.50,
"averageOrderValue": 79.81,
"ordersByStatus": {
"pending": 5,
"processing": 12,
"shipped": 45,
"delivered": 90,
"cancelled": 4
}
}

Cart Tracking​

Update Cart​

Track or update a user's cart.

PUT /carts

Request Body:

{
"userId": "user_123",
"items": [
{
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"price": 29.99,
"quantity": 2,
"total": 59.98,
"imageUrl": "https://example.com/image.jpg"
}
],
"value": 59.98,
"subtotal": 59.98,
"currency": "USD",
"checkoutUrl": "https://store.example.com/checkout?cart=abc123"
}

value is the cart total as you count it (it can include shipping and tax). subtotal is optional: the items subtotal, before shipping and tax - when it is left out it is computed from the items (each line's total, else price x quantity). The subtotal is the cart's value on E-Commerce → Carts, in the Cart value segment filter, in minValue and in totalAbandonedValue.

Add to Cart​

Add an item to a user's cart.

POST /carts/add

Request Body:

{
"userId": "user_123",
"item": {
"productId": "prod_abc123",
"name": "Blue T-Shirt",
"price": 29.99,
"quantity": 1,
"total": 29.99
}
}

Get Abandoned Carts​

List abandoned carts for recovery campaigns.

GET /carts/abandoned

Query Parameters:

ParameterTypeDescription
minValuenumberMinimum cart value
abandonedMinutesAgonumberMax minutes since abandonment
limitnumberResults per page
offsetnumberPagination offset

Response:

{
"data": [
{
"id": "cart_123",
"userId": "user_456",
"items": [...],
"value": 149.99,
"abandonedAt": "2024-01-20T15:30:00.000Z",
"checkoutUrl": "https://store.example.com/checkout?cart=abc"
}
],
"total": 42,
"hasMore": true
}

Cart Statistics​

Get cart and abandonment statistics.

GET /carts/stats

Response:

{
"totalCarts": 1250,
"abandonedCarts": 312,
"recoveredCarts": 87,
"totalAbandonedValue": 45670.50,
"recoveryRate": 27.88
}

Remove from Cart​

POST /carts/remove
{ "userId": "user_123", "productId": "prod_abc123" }

Clear Cart​

POST /carts/clear

Empties the cart. Call this after a successful checkout so an abandoned-cart journey does not chase someone who has already bought.

{ "userId": "user_123" }

Mark Cart Abandoned​

POST /carts/mark-abandoned

Marks the cart abandoned now, rather than waiting for the inactivity timer. Emits the event abandoned-cart journeys wait on.

{ "userId": "user_123" }

Mark Cart Recovered​

POST /carts/recovered

Records that the cart converted, which stops any recovery journey the contact is in and attributes the order to it.

{ "userId": "user_123", "orderId": "1001" }
Which of the four to call

clear and recovered both end the cart, and they are not interchangeable. recovered says the recovery worked and credits it; clear says the cart is simply gone. Using clear after a purchase silently removes the evidence that your recovery journey earned anything.

RFM Analysis​

RFM (Recency, Frequency, Monetary) analysis segments customers based on purchase behavior.

Get User RFM Data​

Get RFM scores and segment for a specific user.

GET /rfm/user/:userId

:userId is the internal user id or the external id your app identifies the person with; an unknown id returns 404.

Response:

{
"totalOrders": 12,
"totalSpent": 849.50,
"averageOrderValue": 70.79,
"firstOrderDate": "2023-06-15T10:00:00.000Z",
"lastOrderDate": "2024-01-18T14:30:00.000Z",
"daysSinceLastOrder": 2,
"hasActiveCart": false,
"rfmRecency": 5,
"rfmFrequency": 4,
"rfmMonetary": 4,
"rfmScore": "544",
"rfmSegment": "champions"
}

The RFM fields are null for a contact with no counted order (cancelled and refunded orders do not count).

Get RFM Distribution​

Get customer distribution across RFM segments.

GET /rfm/distribution

Response:

{
"champions": { "count": 150, "totalValue": 125000 },
"loyal": { "count": 320, "totalValue": 89000 },
"potential_loyalists": { "count": 180, "totalValue": 32000 },
"new_customers": { "count": 450, "totalValue": 28000 },
"at_risk": { "count": 95, "totalValue": 42000 },
"lost": { "count": 280, "totalValue": 15000 },
"scoring": {
"computedAt": "2026-09-02T02:00:00.000Z",
"method": "quantile",
"buyers": 1475
}
}

Tiers are quintiles within this store (method: "quantile"): each of recency, frequency and net spend is ranked across the store's buyers and split into five equal groups, so a top tier means the top fifth of your customers, whatever the currency. A store with fewer than 25 buyers uses fixed thresholds instead (method: "fixed"). Scores are recomputed nightly and on every order; scoring.computedAt says when.

RFM Segments​

SegmentDescriptionTypical RFM Scores
ChampionsBest customers, buy often, spend most555, 554, 545
LoyalConsistent customers444, 443, 434
Potential LoyalistsRecent with average frequency433, 343, 333
New CustomersJust made first purchase511, 512, 411
PromisingRecent but low frequency422, 322, 312
Needs AttentionAverage, declining engagement332, 322, 233
About to SleepBelow average, at risk211, 212, 221
At RiskWere loyal, haven't bought recently144, 143, 244
Can't LoseWere best customers, now inactive155, 154, 255
HibernatingLow engagement, long inactive122, 121, 112
LostLowest scores, likely churned111

List RFM Segments​

GET /rfm/segments

The RFM segments in this workspace with their current sizes, so an integration can target them without recomputing the scoring itself.

Response:

{
"segments": [
{ "segment": "champions", "users": 412, "avgRecencyDays": 8, "avgOrders": 6.2 },
{ "segment": "at_risk", "users": 1180, "avgRecencyDays": 96, "avgOrders": 2.1 }
],
"computedAt": "2026-08-27T02:10:00Z"
}

Shopify storefront tracking​

Activates (or refreshes) the Joryio Web Pixel on the connected Shopify store - storefront and checkout events - and writes the settings the Joryio app embed reads. Once the pixel is active, the store's old tracking script (stores connected before October 2026) stops sending events - Joryio does not delete it; Shopify removes it when the app is uninstalled and stops loading it on March 1, 2027. Safe to call again. Requires integrations:write.

POST /api/integrations/shopify/storefront-tracking
Authorization: Bearer YOUR_API_KEY
{
"pixel": { "status": "active", "id": "gid://shopify/WebPixel/123", "error": null, "checkedAt": "2026-10-03T10:00:00.000Z" },
"missingScopes": [],
"legacyScriptTag": false,
"appEmbed": {
"configured": true,
"themeEditorUrl": "https://my-store.myshopify.com/admin/themes/current/editor?context=apps&activateAppId=<api-key>/joryio-embed"
}
}
pixel.statusMeaning
activeThe pixel is on the store and sending events
needs_scopesThe store has not approved write_pixels and read_customer_events. Start the Shopify connection again (it asks the store owner to approve them); missingScopes lists them
errorShopify refused the pixel; pixel.error says why
not_registeredNot set up yet

GET /api/integrations/shopify returns the same object as storefrontTracking. See Shopify integration.

It also returns themeEmbed, the Popups on your store row: activateUrl opens the theme editor with the Joryio embed switched on (the store owner clicks Save; null when the server has no Shopify app configured), and status is on when the store's SDK has reported in during the last 7 days, otherwise not_reported. See Turn on popups.

"themeEmbed": {
"activateUrl": "https://my-store.myshopify.com/admin/themes/current/editor?context=apps&activateAppId=<api-key>/joryio-embed",
"status": "not_reported"
}

installUrl (also when no store is connected) is where Install Joryio from Shopify sends a merchant who has not installed Joryio yet: the Joryio app's Shopify App Store listing or install link, or null when the server has none configured (the dashboard then links to the install steps). Joryio never asks for a store's address - a store is installed from Shopify and linked. See Connecting your store.

Shopify app launch​

When a merchant installs or opens the Joryio app from Shopify, Shopify adds a signed query (shop, hmac, timestamp, host) to the app's URL. Send that query unchanged; Joryio checks Shopify's signature and that the link is at most an hour old, and says where the store stands for the current workspace. It never connects anything. Requires integrations:read.

POST /api/integrations/shopify/app-launch
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "query": { "shop": "my-store.myshopify.com", "host": "YWRtaW4uc2hvcGlmeS5jb20vc3RvcmUvbXktc3RvcmU", "timestamp": "1760000000", "hmac": "<from Shopify>" } }
{ "status": "not_connected", "shopDomain": "my-store.myshopify.com" }
statusMeaning
connected_hereThe store is connected to this workspace
connected_elsewhereConnected to another workspace of this organization; workspaceId names it
not_connectedNot connected in this organization - start the connection with POST /api/integrations/shopify/oauth/initiate

A query that is unsigned, altered or older than an hour gets 400. See Shopify integration.

Store checkout re-subscribe setting​

Turns the store connection's Let checkout opt-ins re-subscribe people who unsubscribed setting on or off. :platform is shopify, woocommerce, magento or prestashop. Off by default. When on, a checkout or customer-record opt-in from that store - guest checkouts included - dated after the contact's latest unsubscribe re-subscribes that contact. It never goes past a spam complaint, a hard bounce, an SMS STOP (only a double opt-in confirmation), an organization-wide block or another contact's opt-out. Each change is audit-logged. Requires settings:write.

PUT /api/integrations/shopify/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "checkoutResubscribe": true }
{ "checkoutResubscribe": true }

GET /api/integrations/<platform> returns the current value as checkoutResubscribe. See Store marketing consent.

Checkout opt-in safeguards (WooCommerce, Magento, PrestaShop)​

The same route takes the Record consent immediately safeguards of a WooCommerce or Magento connection, and the same safeguards for PrestaShop's checkout newsletter box, which always reaches Joryio before the order (module 1.2.8 or later - see When a tick counts). :platform is woocommerce, magento or prestashop; Shopify gets 400. A checkout tick that arrives before an order counts at once only for a known buyer or subscriber; anyone else is held as pending until their order or the confirmation link - see When a tick counts. checkoutConsentConfirmEmail (default false) emails such a guest one confirmation link. checkoutConsentTickLimit (1-100, default 3) is how many ticks are accepted per shopper IP address, per checkout and per email address per hour; a tick over it is ignored. Send either or both, alone or with checkoutResubscribe. Each change is audit-logged. Requires settings:write.

PUT /api/integrations/magento/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "checkoutConsentConfirmEmail": true, "checkoutConsentTickLimit": 5 }
{ "checkoutConsentConfirmEmail": true, "checkoutConsentTickLimit": 5 }

GET /api/integrations/woocommerce, GET /api/integrations/magento and GET /api/integrations/prestashop return both values.

newShopperOptIns - Opt-ins from new shoppers (not yet buyers) - is hold (default) or immediate. It governs every opt-in that arrives without an order from someone Joryio does not know yet: a checkout tick sent before the order, and a registration, account-page, newsletter or customer-sync opt-in. hold keeps it pending until their order or the confirmation link; immediate counts it at once (less strict - any visitor can enter any email). Known buyers and existing subscribers count at once either way, an unsubscribe always applies at once, and checkoutConsentTickLimit stays in force. Same route and platforms, alone or with the other fields; each change is audit-logged; requires settings:write. GET /api/integrations/<platform> returns it as newShopperOptIns.

PUT /api/integrations/woocommerce/consent-settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "newShopperOptIns": "immediate" }
{ "newShopperOptIns": "immediate" }

GET /api/integrations/woocommerce and GET /api/integrations/magento also return storeCheckoutOptIn: the plugin's own checkout opt-in settings as it last reported them (WooCommerce plugin 1.6.16 or later, Magento extension 1.4.17 or later). They are read-only - set in the store, never through this API - and null until the plugin reports them. recordMode is order (only when the order is placed) or immediate (record consent immediately).

{ "storeCheckoutOptIn": { "pluginVersion": "1.6.16", "checkoutBox": true, "preTicked": false, "recordMode": "order", "reportedAt": "2026-10-27T10:00:00.000Z" } }

Visitor identity: recognise visitors by the email they type​

Recognise visitors by the email they type - see WooCommerce and Shopify. On by default: an email typed at checkout or in a form identifies the visitor's browser as the contact with that email. Off (strict identity): a typed email never identifies the browser; carts, orders and form submissions still go to the email's contact.

GET /api/integrations/<platform> returns visitorIdentity: identifyByTypedEmail, source (default - nothing reported or set yet, which means on; plugin - reported by the WooCommerce plugin 1.6.22 or later, the Magento extension 1.4.21 or later or the PrestaShop module 1.2.10 or later; joryio - set in Joryio), pluginVersion and reportedAt. For WooCommerce, Magento and PrestaShop the setting lives in the store's plugin and is read-only here. For Shopify, set it with the route below; other platforms get 400. Each change is audit-logged. Requires settings:write.

PUT /api/integrations/shopify/visitor-identity
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "identifyByTypedEmail": false }
{ "visitorIdentity": { "identifyByTypedEmail": false, "source": "joryio", "pluginVersion": null, "reportedAt": "2026-11-01T10:00:00.000Z" } }

PrestaShop connection (early access)​

Read and manage the workspace's PrestaShop connection (one PrestaShop shop per connection; see PrestaShop integration). GET returns { "connected": false, "platform": "prestashop" } when nothing is connected. The api-key route returns the jry_ps_ key you paste into the module (it needs integrations:write, like settings, sync and DELETE). GET also returns workspaceId, prestashopVersion and moduleVersion. sync takes type: full (default), delta (products, customers and orders changed since the last clean sync), products, customers, orders or carts (open carts of the last 24 hours), and returns at once while the sync runs. DELETE disconnects the shop and revokes the key.

GET /api/integrations/prestashop
Authorization: Bearer YOUR_API_KEY
{
"connected": true,
"status": "connected",
"platform": "prestashop",
"storeName": "My Shop",
"storeUrl": "https://shop.example.com",
"shopId": 1,
"workspaceId": "6f1c2d3e-...",
"prestashopVersion": "8.2.1",
"moduleVersion": "1.0.0",
"syncSettings": { "syncProducts": true, "syncOrders": true, "syncCustomers": true, "syncCarts": true, "historicalDays": 90 },
"checkoutResubscribe": false,
"syncStats": { "productCount": 120, "orderCount": 860, "customerCount": 540 },
"lastSyncAt": "2026-10-21T10:00:00.000Z",
"connectedAt": "2026-10-21T09:58:00.000Z"
}

connectedAt is when the store last connected (every reconnect moves it); GET /api/integrations/shopify, /woocommerce and /magento return it too. A connection made before it was recorded returns the row's creation time.

GET /api/integrations/prestashop/api-key
Authorization: Bearer YOUR_API_KEY
{ "apiKey": "jry_ps_...", "isNew": false }
PUT /api/integrations/prestashop/settings
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "syncCarts": true, "historicalDays": 90 }
POST /api/integrations/prestashop/sync
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "type": "full" }
DELETE /api/integrations/prestashop
Authorization: Bearer YOUR_API_KEY

Webhooks​

E-commerce events emit webhooks that can trigger canvas flows:

EventDescription
ecommerce.order.createdNew order placed
ecommerce.order.fulfilledOrder shipped
ecommerce.order.cancelledOrder cancelled
ecommerce.order.refundedOrder refunded
ecommerce.order.status_changedOrder status changed
ecommerce.cart.abandonedCart marked as abandoned
ecommerce.cart.recoveredAbandoned cart recovered
ecommerce.cart.updatedCart contents changed

Lookup by Your IDs​

Get Product by Your Product ID​

Retrieve a product using your product identifier.

GET /catalog/products/by-product-id/:productId

An identifier with no matching record answers 404, not 200 with an empty body.

Example:

curl "https://api-eu1.joryio.com/api/catalog/products/by-product-id/SKU-12345" \
-H "Authorization: Bearer $TOKEN"

Get Order by Your Order ID​

Retrieve an order using your order identifier.

GET /orders/by-order-id/:orderId

An identifier with no matching record answers 404, not 200 with an empty body.

Example:

curl "https://api-eu1.joryio.com/api/orders/by-order-id/ORD-2024-001" \
-H "Authorization: Bearer $TOKEN"

Error Responses​

{
"statusCode": 404,
"message": "Product SKU-12345 not found",
"error": "Not Found"
}
Status CodeDescription
400Invalid request body
401Authentication required
403Insufficient permissions
404Resource not found
409Conflict (duplicate external ID)
500Internal server error