Biklo for developersTESTsandbox-api.biklo.comchange

Sync your catalog

The shape of a working catalog integration, in the order you should build it.

1. Map your categories, once#

curl "https://sandbox-api.biklo.com/v1/categories?lang=ro" -H "Authorization: Bearer $KEY"

Store our id against your own category. Ids are language-independent — they are the Romanian master ids — so a mapping built while reading Romanian keeps working if you later read the tree in English. tid is the translated row; it changes with ?lang= and is informational only.

Only is_leaf: true categories accept listings.

2. Map the attributes for each category you use#

curl "https://sandbox-api.biklo.com/v1/categories/4291/attributes?lang=ro" -H "Authorization: Bearer $KEY"

Each attribute carries required, a type, and its options. Map your own values to option_ids now — this is where integration time actually goes, and a product is rejected until every required attribute is present.

If you send an attribute id you read in another language, we resolve it back to the master for you. Forgiving on input, strict on output.

3. Push products#

For a handful, POST /v1/products per product. For a catalog, use the batch:

curl -X POST https://sandbox-api.biklo.com/v1/products/batch \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"products": [ { … }, { … } ]}'
{ "batch_id": "batch_v4lfngtgb6sxqkzwagrqv8c2", "status": "queued", "total": 1000 }

Nothing is written when that responds. Poll the batch:

curl "https://sandbox-api.biklo.com/v1/products/batch/batch_v4l…?failed_only=1" -H "Authorization: Bearer $KEY"

Per-item results, failures first, each with the reason and the position in the array you sent — so you fix rows, not the whole batch.

A batch holds at most 1 000 products, and over that the call is rejected with batch_too_large — nothing is written, so split and resend. A batch is ONE request against your 5 000-per-day rate limit, which is the point of batching: a 10 000-product catalogue costs ten requests, not ten thousand.

4. Keep price and stock fresh#

Do not re-push the whole product to change a number:

curl -X PATCH https://sandbox-api.biklo.com/v1/products/237224/stock \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"stock": 2}'

One update, no attribute or image rewrite. Cheap enough to run hourly.

5. Images#

Send them with the product — in POST /v1/products, PUT /v1/products/{id} or each item of a batch:

{ "external_id": "ERP-1001", "…": "…",
  "images": [ "https://cdn.example.com/erp/mtb-front.jpg",
              "https://cdn.example.com/erp/mtb-side.jpg" ] }

The first is the cover. Like attributes, the list replaces what the listing has, and an identical list changes nothing — so re-pushing an unchanged catalogue does not touch its images. Omit the key to leave them alone.

We reference the image — nothing is downloaded, so pushing a 5 000-product catalogue (five batches of 1 000) is five requests, not 5 000 downloads, and re-photographing a product on your side updates it for us for free.

POST /v1/products/{id}/images is still there for what a payload cannot carry: files[] as multipart uploads stored on our side, or adding to a listing's images without resending the rest.

6. Variants#

Each size or colour is its own product with its own price and stock. Group them so the storefront shows one card with a picker:

{
  "external_group_id": "ERP-1001-GROUP",
  "products": [
    { "external_id": "ERP-1001-S", "size": "S" },
    { "external_id": "ERP-1001-M", "size": "M" }
  ]
}

All variants must sit in the same category. Pushing the same group again re-links it rather than creating a second one.

7. When a product leaves your catalogue#

Archive it:

curl -X POST https://sandbox-api.biklo.com/v1/products/237224/archive -H "Authorization: Bearer $KEY"

Stock 0 that YOU send is not the same thing: the listing stays visible, just unbuyable. (When Biklo orders take the last pieces, the listing is sold and archived for you — product.sold — and sending stock above 0 puts it back on sale.) Archived, it is out of search, the storefront and every cart, as if you had switched it off in the dashboard — so an order that is cancelled later never puts it back on sale. The listing keeps its id and history; POST /v1/products/{id}/unarchive brings it back.

Keeping in sync afterwards#

Pull changes with ?updated_since=, and let external_id do the work: the same id always updates the same product, so your sync never has to ask "does this exist yet".