Skip to content

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.

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

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

  • 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_batches with status O right after POST /offer-packages and 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 with GET /offers before 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 /offers and 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 from GET /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_state N -> R before the call; a second click after a lost answer adopts the request found by GET /order-cancellation-requests instead of sending another.

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.

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.

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)”
  1. Offer result items. The schema of GET /offer-packages/{id}/offer-requests-results lists only {message, resultCode}; the example shows {sellerExternalReference, integrationStatus, results[]}. The add-on reads the example shape and looks offers without a result line up with GET /offers.
  2. Tax code spelling. The schema enum is Ecotax / Deatax; the examples write EcoTax, DeaTax and EcoPart. The add-on sends the enum values.
  3. Update price without taxes. The schema marks price.taxes required, an Update example sends price alone. The add-on always sends the complete tax list.
  4. carrierName. The shipments schema asks for carrierName; GET /carriers says its code is “requested for tracking POST”. The add-on sends the carrier code chosen on the Rules page (listed from GET /carriers).
  5. orderSellerId. The cancellation request calls the order id orderSellerId (“The order unique identifier”); the add-on sends the Octopia orderId.
  6. 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.
  7. offer.sellerProductId is assumed to be the seller external reference (fallback: EAN).
  8. 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.
  9. gtin as integer in the product submission (schema type integer): a GTIN with leading zeros is sent as its number.
  10. Rate limits besides the token quota are not documented: 429 waits for Retry-After, or one minute without the header.
  11. Seller Sandbox. SELLZZ is 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).
  12. 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).