Cdiscount Marketplace Connector: developer notes
Add-on id kv_cdiscount, namespace Tygh\Addons\KvCdiscount. Built on the Kavio Marketplace Hub (channel independent Hub\*: product
and order maps, price rules, stock buffer, category / property mapping, queue, log). The Cdiscount part is Client\Api (HTTP),
Drivers\Cdiscount\Mapper (pure translation, unit tested) and Drivers\Cdiscount\Driver (Hub\DriverInterface), plus the Cdiscount
order flow in Hub\Orders and Hub\Shipments.
Cdiscount and Octopia are trademarks of their owners. This add-on integrates with the Octopia Seller API; it is not made, endorsed or certified by Cdiscount or Octopia.
Sources (read 2026-11-08)
Section titled “Sources (read 2026-11-08)”- Official OpenAPI file “API REST Seller V2”, version 2.0.0, downloaded from the Octopia developer portal
(https://developer.octopia-io.net/sellers-api-yaml-download/ ->
wp-json/octopia/v1/download/sellers). Every path, header, body field and enum below is cited from it as OAS<method> <path>. - Getting started guide (https://developer.octopia-io.net/user-guides/getting-started/): client ID / client secret / seller ID from the API credentials page, token valid 2 hours, token quota 500 per hour.
- Test samples (
tests/samples/*.json) are built from the OAS examples and schema field examples by a script; where the field examples of one schema contradict each other (an order line VAT of 1.00 on 10 x 41.99) the sample keeps the shape with consistent numbers.
Hosts, authentication, format
Section titled “Hosts, authentication, format”| Item | Value | Source |
|---|---|---|
| API base | https://api.octopia-io.net/seller/v2 |
OAS servers |
| Token | POST https://auth.octopia-io.net/auth/realms/maas/protocol/openid-connect/token, form body grant_type=client_credentials, client_id, client_secret -> access_token, expires_in (7200) |
OAS token operation (servers override, Authentication.TokenRequest, Authentication.Token) |
| Every call | Authorization: Bearer <access_token>, SellerId: <seller id> (required header of every route) |
OAS token description, Header.SellerId |
| Sales channel | live CDISFR (Cdiscount), test SELLZZ (Seller Sandbox); “4 first letters of the Marketplace + 2 first letters of the country” |
OAS GET /offers examples, Offer.salesChannelId |
| Errors | application/problem+json {type, title, status, detail, traceId, errors{field: [messages]}}; some routes {description, statusCode, errors[{cause, code, reason}]}; token route {error, error_description} |
OAS responses |
| Quota | token route 500 / h; offer routes answer 429 “you have reached your offer submission quota” | OAS token description, offer-packages 429 examples |
The token is cached encrypted per account and mode until 30 s before it expires; a 401 on a call gets one new token, a second 401 (or
the token route’s 401 / 400 invalid_client) pauses the account (Accounts::pause, no further calls until the form is saved or a test
passes). User-Agent: Cdiscount Marketplace Connector for CS-Cart (https://cs-cart.kaviolabs.com) (no rule in the docs). Test hook:
$config['kv_cdiscount_base_url'] in config.local.php sends the token route and the API to a local mock (tests refuse any other value).
Endpoints used
Section titled “Endpoints used”| Purpose | Call | Source |
|---|---|---|
| Connection test | GET /sellers (shop_name, seller_state) |
OAS GET /sellers |
| Categories | GET /categories?pageIndex&pageSize=100&fields=label,level,isActive,parentReference (Accept-Language fr-FR) |
OAS GET /categories |
| Properties | GET /categories/{categoryReference}/properties -> propertyReference, label, unit, necessity (Mandatory/Recommended/Optional), isRanged, isMultiple, choices |
OAS GET /categories/{categoryReference}/properties |
| Catalogue lookup | GET /products?gtin=&limit=1 (Accept-Language fr-FR) |
OAS GET /products |
| Product creation | POST /products-integration {products:[{gtin (integer), sellerProductReference, title, description, sellerPictureUrls[{index, url}], brand, categoryCode, attributes[{propertyReference, values[]}]}]}, Accept-Language fr-FR -> 202 {packageId} |
OAS POST /products-integration (“Only products with fr-FR language will be available for Cdiscount”) |
| Product report | GET /products-integration-reports?packageId&pageIndex&pageSize=100 -> items {gtin, sellerProductReference, status Refused/Validated/Integrated, errors[{code, field, message}]} |
OAS GET /products-integration-reports |
| Offer package | POST /offer-packages {packageType: Upsert/Update} + SalesChannelId header -> 201, id in Content-Location: offers-packages/{packageId} |
OAS POST /offer-packages |
| Offer upload | POST /offer-packages/{packageId}/offer-requests [offer requests], at most 100 per upload, only while WaitingForCompletion |
OAS POST …/offer-requests (“must not exceed 100 offer requests”) |
| Submit | PATCH /offer-packages/{packageId} {state: Ready} -> 204 |
OAS PATCH /offer-packages/{packageId} |
| Package state | GET /offer-packages/{packageId} -> state WaitingForCompletion/Ready/IntegrationPending/Integrated/Rejected, results{message, resultCode} |
OAS GET /offer-packages/{packageId} |
| Offer results | GET /offer-packages/{packageId}/offer-requests-results?limit=100&Cursor= (next page from the Link header rel=“next”) |
OAS GET …/offer-requests-results |
| Offer lookup | GET /offers?salesChannelId&sellerExternalReferences=a,b&limit |
OAS GET /offers |
| Carriers | GET /carriers -> code, label (“the associated mandatory carrierCode requested for tracking POST”) |
OAS GET /carriers |
| Orders | GET /orders?salesChannelId&status&pageIndex&pageSize=100&sort=createdAt (max 100 per page) |
OAS GET /orders |
| Order | GET /orders/{orderId} |
OAS GET /orders/{orderId} |
| Accept / refuse | POST /orders/{orderId}/approval-status {approval_status: Accepted/Refused} (additionalProperties false) -> 201 |
OAS POST /orders/{orderId}/approval-status |
| Ship | POST /orders/{orderId}/shipments [{parcelNumber, carrierName, trackingUrl, orderLineIds[]}] -> 201; only for lines InPreparation with supply mode Seller |
OAS POST /orders/{orderId}/shipments |
| Cancel | POST /order-cancellation-requests {orderSellerId, reason, shippingCostRefund} -> 201 {totalCancellationRequestId} |
OAS POST /order-cancellation-requests |
| Cancel lookup | GET /order-cancellation-requests?orderSellerId= |
OAS GET /order-cancellation-requests |
| Reasons | GET /cancellation-reasons?salesChannel&userType=Seller&orderStatus=InPreparation -> code, description |
OAS GET /cancellation-reasons |
Not used: XML offer packages (/offer-integration-packages, a zip on a public URL), partial cancellation, commercial gestures, invoices,
discussions, Fulfilment routes (inbound/outbound/stocks/returns), financial reports.
Offer mapping (Mapper::offer, Mapper::inventory)
Section titled “Offer mapping (Mapper::offer, Mapper::inventory)”Upsert offer request (OAS offer request schema, Upsert example): sellerExternalReference (map row seller reference, cut to 64),
product.gtin (EAN 8/12/13/14 digits), condition (enum New, UsedLikeNew, UsedVeryGoodState, UsedAverageState, RefurbishedLikeNew,
RefurbishedVeryGoodState, RefurbishedCorrectState), price.price (“all taxes included”, EUR), price.originPrice (only when higher),
price.taxes [{code VAT, value rate}] plus Ecotax / Deatax amounts when the product has them (enum VAT, Ecotax, Deatax, PrivateCopy,
ChemicalTax), deliveryModes [{code, cost, additionalCost}] (enum NTHD, THD, EHD, SHD, FDHD, SRHD, WSHD, IVP, SVP, PPMR, PPLP),
preparationTime, quantity. Update requests carry the reference plus price (with the complete tax list: “For Update packages, the
complete list of taxes must be expressed”) and / or quantity.
Price: net CS-Cart price + VAT (the product’s first active percentage tax rate of a destination containing FR, else the Rules default 20) when one of its taxes has “price
includes tax” off; then x euro rate; then markup and endings in euros (Listings::euroPrice).
Never twice
Section titled “Never twice”- Offers: Upsert replaces an offer with the same seller reference, EAN and condition (OAS packageType description), so sending an
offer again after a lost answer cannot create a second one. Every package is recorded in
kv_cdiscount_batcheswith status O right afterPOST /offer-packagesand BEFORE any upload (ProductSync::opened); a package left open is read back on the next run (state Integrated -> results applied; WaitingForCompletion older than 6 h -> rows due again). An offer without a result line is looked up withGET /offersbefore it is called refused. - Products: the product submission is keyed by EAN; a repeat updates the same catalogue product (assumed, see Unverified 12); a submission refused as a whole is split down to the bad product.
- Offer key: Upsert keys on seller reference + EAN + condition, which “can not be updated” (OAS offer-requests): a live offer is never re-sent with another EAN, and the condition / EAN source are locked while offers are live. Removed mappings and deleted products are withdrawn with a Delete package before their rows go.
- Result lines without a reference: an offer is looked up with
GET /offersand compared with what was sent (quantity, price, state, acceptance); a refused stock / price update is sent again within the hour. - Acceptance: the order map row is written before the call (unique key company + order id), the decision is claimed N -> R, and a timeout or error answer is reconciled by reading the order back (Accepted / Refused already = done).
- Import: unique map key, per-order
GET_LOCK, placing marker in the order notes, adoption of an order left by an interrupted run. - Shipments: only real CS-Cart shipments with a tracking number (never a status change) report lines; reported line ids are kept in
shipped_lines; the push is claimed N -> R; a refusal or a stale claim is reconciled fromGET /orders/{id}(lines Shipped or later = done) before anything is sent again; rows without anything to send rotate to the back so they never starve newer shipments. - Cancellation:
cancel_stateN -> R before the call; a second click after a lost answer adopts the request found byGET /order-cancellation-requestsinstead of sending another.
Orders
Section titled “Orders”Status order (OAS order status enum): WaitingAcceptance, Accepted, InPreparation, Shipped, Delivered; Cancelled / Refused / Rejected end an order. The map follows forward only; a final status is never left (a lagging read cannot reopen a cancelled order). The CS-Cart status follows only while the merchant has not moved the order (cancellations always follow), with notifications off.
Import happens at InPreparation: “For orders having supply mode = Seller, the shipping address of an order line is only available once
the line has reached … InPreparation” (OAS GET /orders). Orders whose lines all have supply mode Fulfillment are not imported.
Line price = sellingPrice.unitSalesPrice (what the customer paid; offerPrice as fallback); shipping = sum of shippingCost; on a
single order it becomes the order shipping cost, on a Multi-Vendor split it stays in the notes. Refused / cancelled lines are left out
and named in the notes. Service fees the customer paid to Cdiscount are named in the notes, not added to the order. Dates: ISO 8601 ->
Unix time; notes in the shop time zone (date()).
Line mapping: offer.sellerProductId = seller reference of the product map, else offer.productGtin = EAN of the map row.
Multi-Vendor
Section titled “Multi-Vendor”Owner mode on: one account (company 0), vendors see no channel data and cannot act. Owner mode off: per-vendor accounts; every POST that
touches an account, order or mapping checks the row’s company against the vendor (Access::canView, canManageAccount,
listScope); cron, run_now, backfill, reference_refresh are admin only (vendor_multivendor.post.php); listings of another
vendor’s product are refused in Listings::build and ProductMap::save.
Data and uninstall
Section titled “Data and uninstall”Tables: accounts, product_map (gtin, catalog), order_map (shipped_lines, cancel_state), options, category_map, attr_map are KEPT on
uninstall (Retention, retention.json) unless “Permanently delete Cdiscount order and offer links on uninstall” is on; log, jobs, cache,
batches are dropped. A reinstall over kept data pushes nothing until the next change, sends offers waiting for a package result again
and resubmits products waiting for a report. Upgrades: Upgrade\Synchronizer (in-place upgrades apply missing tables and columns).
Unverified (the documentation does not say, or contradicts itself)
Section titled “Unverified (the documentation does not say, or contradicts itself)”- Offer result items. The schema of
GET /offer-packages/{id}/offer-requests-resultslists only {message, resultCode}; the example shows {sellerExternalReference, integrationStatus, results[]}. The add-on reads the example shape and looks offers without a result line up withGET /offers. - Tax code spelling. The schema enum is
Ecotax/Deatax; the examples writeEcoTax,DeaTaxandEcoPart. The add-on sends the enum values. - Update price without taxes. The schema marks
price.taxesrequired, an Update example sendspricealone. The add-on always sends the complete tax list. - carrierName. The shipments schema asks for
carrierName;GET /carrierssays itscodeis “requested for tracking POST”. The add-on sends the carrier code chosen on the Rules page (listed fromGET /carriers). - orderSellerId. The cancellation request calls the order id
orderSellerId(“The order unique identifier”); the add-on sends the OctopiaorderId. - VAT included in order lines. Offer prices are “all taxes included”; order lines do not say it. A line whose VAT amount equals
amount x rate is treated as net (VAT added), anything else as gross (
Mapper::gross). The schema field examples are inconsistent. - offer.sellerProductId is assumed to be the seller external reference (fallback: EAN).
- Product picture indexes and text lengths are not documented (“a picture outside of its accepted indexes” is ignored): the add-on sends at most 6 pictures (index 1-6) and keeps texts within 255 (title) / 5000 (description) characters; description as plain text.
- gtin as integer in the product submission (schema type integer): a GTIN with leading zeros is sent as its number.
- Rate limits besides the token quota are not documented: 429 waits for Retry-After, or one minute without the header.
- Seller Sandbox.
SELLZZis named as “Seller Sandbox” in the examples; whether a sandbox account accepts every route the same way as Cdiscount was not tested against Octopia (owner item: a real sandbox run with the merchant’s credentials). - Product resubmission by EAN is assumed to update the same product (the submission is not recorded before the POST; the
catalogue check is one
GET /products?gtin=per EAN although an example shows comma-separated values).