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