CDON Marketplace Connector: developer notes
CDON is a trademark of its owner. This add-on integrates with the CDON merchant API; it is not made, endorsed or certified by CDON.
Add-on id kv_cdon, PHP namespace Tygh\Addons\KvCdon. Built on the Kavio Marketplace Hub (Hub\*, channel independent) with one channel
driver: Client\Api (HTTP), Drivers\CDON\Mapper (pure translation, unit tested), Drivers\CDON\Driver (Hub\DriverInterface), plus
Markets (markets, currencies, exchange rates).
Sources (read 2026-11-08)
Section titled “Sources (read 2026-11-08)”Only the documentation of the NEW CDON merchant API is used. The legacy CDON Connect API (and the old ma-docs pages on Read the Docs)
closed in Q1 2026 and is not called anywhere.
Every page is also available as Markdown with its OpenAPI 3.1 block (<page>.md); the request and response examples of those blocks are
the unit test samples in tests/samples/ (copied unchanged).
Hosts, authentication, format
Section titled “Hosts, authentication, format”| Rule | Source | Implementation |
|---|---|---|
Production https://merchants-api.cdon.com/api, sandbox https://merchants-api.sandbox.cdon.com/api; the version is part of the path (/v2/articles/bulk, /v1/orders) |
Introduction, “Base URLs” | Api::LIVE, Api::SANDBOX; Connection page mode Sandbox / Production |
Every endpoint: Authorization: Basic base64(merchantID:token) |
Introduction, “Authentication” | Api::authHeaders(); merchant ID and token are the “Merchant ID” / “API token” of the Connection page, sealed with AES-256-GCM |
| CDON and Fyndiq credentials are not interchangeable | Introduction | The add-on talks to CDON only |
JSON bodies with Content-Type: application/json, TLS 1.2 |
Introduction, “Request format” | BaseClient::request() (cURL with peer verification) |
| Sandbox credentials are given on request; moving to production = new base URL + production credentials | Introduction, “Sandbox” | Separate sandbox and production credential sets |
429 with X-RateLimit-Burst-Reset / X-RateLimit-Quota-Reset (Unix seconds) and X-RateLimit-Policy-Violated (burst / quota); rejected calls count against the limits; burst 9 000 / min, quota 270 000 / h |
Rate Limiting | Api::rateLimitWait(): the job waits until the reset of the violated window (5 s to 1 h) instead of the normal back-off |
Error bodies: {"detail"} (429), {"description", "errors"} (v1), {"message", "errors"} (v2) |
Rate Limiting; OpenAPI V1Error, ErrorResponse |
Api::errorFrom() |
Test override: $config['kv_cdon_base_url'] = 'http://127.0.0.1:11891/api'; in config.local.php replaces the base URL (mock server).
Endpoints used
Section titled “Endpoints used”| Use | Method + path | Fields |
|---|---|---|
| Connection test | GET /v1/orders?limit=1 |
read only; any 2xx = credentials work |
| Categories (Categories page) | GET /v1/categories/{market}/{language}/ |
id, name, path (dotted ancestors -> parent), search_friendly_name; first enabled market, en-US |
| Create articles | POST /v2/articles/bulk {articles: [1..100]} |
202 {batch_id, success: [{id, sku}], failed: [{sku, errors: [{message, field[]}]}]} |
| Content update | PUT /v2/articles/bulk action update_article |
required status, main_image, markets, shipping_time, title, description; price and quantity forbidden |
| Price | PUT /v2/articles/bulk action update_article_price {price} |
only price allowed |
| Stock | PUT /v2/articles/bulk action update_article_quantity {quantity} |
only quantity allowed |
| Results | POST /v1/statuses/batch {batch_ids} |
batches[].statuses[] {sku, action, markets[] {market, status ok/failed/pending, error_code}} |
| New orders | GET /v1/orders?state=CREATED&limit=100&page=n |
plain array; up to 1 000 per page; no “changed since” filter |
| One order | GET /v1/orders/{order_id} |
retry of a failed import, read-back of open orders |
| Acknowledge | PUT /v1/orders/{order_id}/accept |
optional, “not available by default” |
| Ship | PUT /v1/orders/{order_id}/fulfill {tracking_information: [{carrier_name, tracking_number}]} |
tracking number required per item, carrier optional, body optional |
| Cancel | PUT /v1/orders/{order_id}/cancel |
only while CREATED; CDON refunds the customer |
GET /v1/articles/sku/{sku} is implemented (Api::articleBySku) but not needed by the sync: a second create of an existing SKU is answered
with sku_already_exists (status error_code), which the add-on adopts (below).
Not used: v1 single-article write endpoints (deprecated, Fyndiq only), shipping configurations, payment reports, settlement notes,
receipts, delete_article (an unmapped product stays on CDON; set it to 0 stock or delete it in the CDON portal).
Article mapping (Mapper::article, Mapper::content)
Section titled “Article mapping (Mapper::article, Mapper::content)”| CDON field (Article parameters) | Rule | Source in CS-Cart |
|---|---|---|
sku 1-64 |
your SKU, unique | product map SKU (product code by default) |
status for sale / paused |
product status A = for sale, else paused | |
quantity 0-500 000 |
stock after buffer / minimum / maximum rules | |
main_image + images (max 10) URLs |
main image + additional images | |
markets SE DK FI NO |
ISO 3166-1 alpha-2 | ticked markets that have an exchange rate |
price[] {market, value {amount_including_vat, currency}} |
VAT included, market currency (SEK, DKK, EUR, NOK); vat_rate defaults to the market rate |
CS-Cart price (+ market VAT when the product’s CS-Cart taxes are net) x rate, then the price rules in the market currency; vat_rate is not sent |
shipping_time[] {market, min 0-21, max 1-21} |
Rules page, per market | |
title[] 5-150 / description[] 10-4096 {language, value} |
languages sv-SE en-US da-DK fi-FI nb-NO; CDON translates missing ones; description “No HTML” (schema DescriptionText) |
store language (en -> en-US …; a store language CDON does not take is not sent) plus every CDON language the store has a translation in; HTML turned into plain text; cut with mb_substr |
category (string, max 10) |
category mapping | |
brand 1-50 |
free text, optional | category brand, brand feature or default brand |
gtin 8-14 |
GTIN feature (digits only) | |
properties[] {name, value, language?} |
free text 1-36 + language; pre-defined values from a fixed list; numeric 1-50 | Attributes page: CS-Cart feature or fixed value per property |
classifications [{CONDITION: NEW/REFURB}] |
Rules page | |
delivery_type[] {market, mailbox/service_point/home_delivery} |
Rules page | |
shipped_from EU / NON_EU |
Rules page | |
manufacturer {name, address{street_address, city, postal_code, country}, website, email, responsible_person} |
GPSR; responsible person required outside the EU | Rules page (sent when the address is complete) |
Variations (parent_sku, variational_properties) are not sent: each CS-Cart product or variation is its own article.
Never twice
Section titled “Never twice”- Articles. One bulk create per run. When the answer is lost (timeout, 5xx) the row stays “not listed” and the next run sends the
create again; CDON refuses the duplicate SKU (
sku_already_exists, at once infailedor later in the batch status), and the add-on treats that as “already there”: the row is linked and its content goes out once asupdate_article. Unit and e2e tested (504 after save -> one article). - Orders. The order map row is written BEFORE the order is placed (unique key company + CDON order id), the import of one CDON order
runs under
GET_LOCK, aplacing_atmarker and a notes marker[kv_cdon:<account>:<order id>]let a later run adopt an order that an interrupted run placed. Parallel cron runs and repeats import once (e2e). - Fulfilment / acceptance. Rows are claimed with
UPDATE ... WHERE state = old(N -> R); a repeat answered 403 “Order row already has fulfilled as state” counts as done. Refusals of this request (400/403/404/409/422, e.g. past the deadline or cancelled) are final (“E”, with a “Send fulfilment again” button); network errors, 429 and 5xx retry.
Orders
Section titled “Orders”- One CDON order = one SKU with a quantity (Introduction, “The order lifecycle”); the add-on creates one CS-Cart order per CDON order.
customer_order_id(shown as the CDON order number) is kept in the notes so the merchant sees which orders one customer placed together. price/total_price= the price of the whole quantity; the line price isamount / quantity, converted from the market currency to the CS-Cart primary currency with the same rate as the article prices (Markets::rate).- Delivery country = the market code (
shipping_address.countryis a name such as “Sweden”). B2B orders have no first / last name: thefull_nameis used as company and first name. The API has no customer e-mail: a placeholderchannel-<number>@channel.invalidis used. - New orders are read with
state=CREATEDandstate=ACCEPTED(accepted in the CDON portal before the import); the order map skips known ones. CDON states only move forward in the add-on (CREATED < ACCEPTED < FULFILLED / NOT_FULFILLED, terminal): a stale read right after an asynchronous accept / cancel (202) is ignored. A CS-Cart order follows a CDON state only while the merchant has not moved it since (a cancellation at CDON always follows). - Fulfilment: every tracking number of the order’s CS-Cart shipments is sent; with “Wait for a tracking number” only orders whose shipments have one are selected (the others never block the queue). A claim left by a process that died during the call is released after 15 minutes (a repeat is answered “already fulfilled” and counted as done).
- Order date =
created_at(UTC) stored as a Unix time; CS-Cart shows it in the shop time zone. Notes show date and “fulfil by” (fulfillment_deadline) in the shop time zone. - States: CREATED -> new, ACCEPTED -> accepted, FULFILLED -> fulfilled, NOT_FULFILLED -> cancelled (mapped to CS-Cart statuses on the Rules page). Imported orders still CREATED / ACCEPTED are read back every 30 minutes, so a cancellation at CDON reaches CS-Cart. A FULFILLED / ACCEPTED caused by the add-on itself does not change the CS-Cart status again.
- Returns: the CDON merchant API has no return or claim endpoints (documentation index, 2026-11-08); returns stay in the CDON portal.
Multi-Vendor
Section titled “Multi-Vendor”Owner mode (default): one CDON account for the marketplace, vendors have no CDON pages. Owner mode off: each vendor connects their own
CDON merchant account in the vendor panel; every map, order, job and log row carries the company, every vendor action checks it, global
actions (cron, run now, reference refresh) are admin only (schemas/permissions/vendor_multivendor.post.php).
PHP: update_product_post (content changed -> update), update_product_amount (stock changed -> push), delete_product_post (mapping
removed), place_order (links the order map row the moment the order exists), init_templater_post (upgrade synchronizer). Template:
addons/manage.post, addons/tabs_content.pre, index/styles.post (licence card and styles). No storefront output (full page cache
not affected).
Data and uninstall
Section titled “Data and uninstall”Tables kv_cdon_accounts, _order_map, _product_map, _options, _category_map, _attr_map are kept on uninstall unless
“Permanently delete CDON order and article links on uninstall” is ticked (they are the guard against duplicate orders and articles after a
reinstall); _log, _jobs, _cache, _batches are dropped. Settings and the licence key are restored on reinstall.
Bulk calls refused as a whole (400 / 422) are split in halves down to single articles / SKUs, so one bad article cannot stop the others; the product sync runs its phases (batch results, stock and prices, creates, content updates) independently. Batch statuses are read one batch per call (the endpoint takes a list; fewer calls are a possible optimisation).
Unverified (the documentation does not say, or contradicts itself)
Section titled “Unverified (the documentation does not say, or contradicts itself)”- Whether order
price.amountincludes VAT. The two documented examples disagree (299 with VAT 59.80 at 25 % = VAT included; 23.20 with VAT 5.80 at 25 % = VAT excluded). The add-on decides per order (Mapper::gross): vat_amount = amount x rate -> net, VAT is added; otherwise the amount is taken as VAT included. Check one real sandbox order. - Order
vat_rateis a fraction (0.25) in the order examples and a percent (25) in article prices; the add-on accepts both. - How long batch statuses are kept: not documented; the add-on gives up after 24 hours and sends the create again (adopted if it exists).
- The 401 error body shape: not documented; any 401 pauses the account.
- Whether the immediate
failedtext for a duplicate SKU contains “already exists”: only the statuserror_codesku_already_existsis documented; both are handled. - Accept availability: documented as “not available by default”; a 403 “not available” marks the order as not accepted and fulfilment works as usual.
- Rate-limit figures (9 000 / min, 270 000 / h) “may be adjusted”: the add-on follows the response headers.