Skip to content

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).

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.

Source URL
Documentation index (docs.cdon.com redirects here) https://cdon-api-documentation.readme.io/llms.txt
Introduction: authentication, base URLs, sandbox, request format, order lifecycle, markets / currencies / languages https://cdon-api-documentation.readme.io/reference/introduction
Rate limiting https://cdon-api-documentation.readme.io/reference/rate-limiting
Article parameters https://cdon-api-documentation.readme.io/reference/article-parameters
Variations https://cdon-api-documentation.readme.io/reference/copy-of-article-parameters
Create Articles Bulk https://cdon-api-documentation.readme.io/reference/createarticlesbulkv2
Update Articles Bulk https://cdon-api-documentation.readme.io/reference/updatearticlesbulkv2
Retrieve an article by SKU https://cdon-api-documentation.readme.io/reference/getarticlebyskuv1
List Statuses By Batch Ids https://cdon-api-documentation.readme.io/reference/liststatusesbybatchids
List Categories https://cdon-api-documentation.readme.io/reference/listcategories
List orders / Retrieve an order https://cdon-api-documentation.readme.io/reference/listorders, …/getorder
Accept / Fulfill / Cancel an order https://cdon-api-documentation.readme.io/reference/acceptorder, …/fulfillorder, …/cancelorder
Create a test order (sandbox only) https://cdon-api-documentation.readme.io/reference/createtestorder

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).

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).

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.

  • 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 in failed or later in the batch status), and the add-on treats that as “already there”: the row is linked and its content goes out once as update_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, a placing_at marker 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.
  • 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 is amount / 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.country is a name such as “Sweden”). B2B orders have no first / last name: the full_name is used as company and first name. The API has no customer e-mail: a placeholder channel-<number>@channel.invalid is used.
  • New orders are read with state=CREATED and state=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.

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).

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)”
  1. Whether order price.amount includes 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.
  2. Order vat_rate is a fraction (0.25) in the order examples and a percent (25) in article prices; the add-on accepts both.
  3. 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).
  4. The 401 error body shape: not documented; any 401 pauses the account.
  5. Whether the immediate failed text for a duplicate SKU contains “already exists”: only the status error_code sku_already_exists is documented; both are handled.
  6. Accept availability: documented as “not available by default”; a 403 “not available” marks the order as not accepted and fulfilment works as usual.
  7. Rate-limit figures (9 000 / min, 270 000 / h) “may be adjusted”: the add-on follows the response headers.