Biklo for developersTESTsandbox-api.biklo.comchange

List your products

GET/v1/products

scope products.read

Your own catalog only, cursor-paginated. Use updated_since for incremental sync instead of re-reading everything.

Query parameters#

NameExample
updated_since2026-09-01T00:00:00ZISO 8601
statusactiveactive | draft | sold | archived
sku— Exact match
external_id—Your own id
category_id—Master or translated id
limit50Max 100
cursor—next_cursor

Response#

FieldTypeDescription
countintegerHow many listings in THIS page, not in total.
next_cursorstring|nullPass back as ?cursor= for the next page; null on the last. Never construct a cursor yourself — it is opaque and its format may change.
dataarrayYour listings, and only ever yours.
data[].idintegerOUR id for the listing. Use it in the path of the other product endpoints.
data[].external_idstring|nullYOUR id, as you sent it — the key upserts match on and what to reconcile against. null on a listing created in the dashboard rather than through this API.
data[].skustring|nullYour stock-keeping unit, returned as you sent it.
data[].gtinstring|nullGTIN, as you sent it.
data[].mpnstring|nullManufacturer part number, as you sent it.
data[].eanstring|nullEAN barcode, as you sent it.
data[].titlestringThe listing title.
data[].descriptionstring|nullThe listing description, as stored.
data[].category_idinteger|nullThe category, as a MASTER id — the same value you send on a write.
data[].statusstringWhere the listing is in its life. active is the only one buyers can order from. Four database columns collapse into this one word, so read it rather than inferring from anything else.

Accepts draft · active · sold · archived

data[].is_reviewedbooleanWhether a moderator has approved it. A listing can be active and not yet reviewed; the product.approved and product.rejected webhooks fire when this changes.
data[].priceobjectThe asking price.
data[].price.amountinteger|nullMinor units — 249900 is 2 499,00.
data[].price.currencystring|nullDecided by the listing country, not by you. This is the currency an order for it settles in.
data[].price.negotiablebooleanWhether offers are accepted on this listing.
data[].stockinteger|nullUnits available, or null where none was ever set.
data[].shippingobjectDelivery facts about this listing.
data[].shipping.ship_with_usbooleanAlways true on anything this API created — see POST /v1/products. It can still read false on an older listing made in the dashboard before that was settled; the next write through this API repairs it.
data[].shipping.free_shippingbooleanWhether delivery is free on this listing regardless of your bands.
data[].shipping.shipping_classstringDERIVED from weight, dimensions and the category — there is no column for it. It decides how the courier is priced.
data[].shipping.parcel_tierstringWhich of your two shipping-absorption band sets governs this listing. This is the only way to connect a product to its bands, since absorption is configured per tier.

Accepts small · large

data[].shipping.weight_kgnumber|nullDeclared weight.
data[].shipping.width_cmnumber|nullDeclared width.
data[].shipping.height_cmnumber|nullDeclared height.
data[].shipping.length_cmnumber|nullDeclared length.
data[].group_idinteger|nullThe variant group this listing belongs to, or null. Set through POST /v1/product-groups.
data[].imagesarrayPhotos, already in display order.
data[].images[].idintegerWhat you send to PUT /v1/products/{id}/images/order and to the delete endpoint.
data[].images[].positionintegerIts place in the order, 0 being the main photo.
data[].images[].pathstring|nullWhere the file sits in our storage, when we host it.
data[].images[].source_urlstring|nullThe URL we serve it FROM, when you attached it by link and we never re-hosted it. One of these two is set, not both — an image with a source_url breaks if you move the file.
data[].attributesarrayThe category attributes on this listing.
data[].attributes[].field_idintegerThe attribute, as a master id.
data[].attributes[].option_idinteger|nullThe chosen option, for list attributes.
data[].attributes[].valuestring|nullThe free-text value, for attributes with no option list. One of these two is set.
data[].created_atstringISO-8601 UTC.
data[].updated_atstringISO-8601 UTC. This is what ?updated_since= compares against, so store it if you sync incrementally.

Example

{
    "count": 1,
    "next_cursor": "165987",
    "data": [
        {
            "id": 237224,
            "external_id": "ERP-1001",
            "sku": "MTB-29-001",
            "gtin": null,
            "mpn": null,
            "ean": null,
            "title": "Bicicleta MTB Hardtail 29 inch",
            "description": "A hardtail mountain bike in very good condition, barely used.",
            "category_id": 4291,
            "status": "active",
            "is_reviewed": true,
            "price": {
                "amount": 249900,
                "currency": "RON",
                "negotiable": true
            },
            "stock": 4,
            "shipping": {
                "ship_with_us": true,
                "free_shipping": false,
                "shipping_class": "bike",
                "parcel_tier": "large",
                "weight_kg": 14.5,
                "width_cm": 80,
                "height_cm": 120,
                "length_cm": 180
            },
            "group_id": null,
            "images": [
                {
                    "id": 108876841,
                    "position": 0,
                    "path": "files/237224/mtb-front.jpg",
                    "source_url": null
                }
            ],
            "attributes": [
                {
                    "field_id": 32,
                    "option_id": 226,
                    "value": null
                }
            ],
            "created_at": "2026-09-01T08:14:22Z",
            "updated_at": "2026-09-11T16:02:51Z"
        }
    ]
}

Failures use the shared error envelope — a machine-readable code and a request_id to quote to support. Error handling.

curl -X GET "https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer bk_test_YOUR_KEY"
<?php

$ch = curl_init('https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'GET',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer bk_test_YOUR_KEY',
    ],
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);
using System.Net.Http;
using System.Net.Http.Headers;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "bk_test_YOUR_KEY");

var request = new HttpRequestMessage(
    new HttpMethod("GET"),
    "https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z");

var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
import requests

response = requests.get(
    "https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z",
    headers={"Authorization": "Bearer bk_test_YOUR_KEY"},
)

data = response.json()
const response = await fetch("https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z", {
  method: "GET",
  headers: {
    Authorization: "Bearer bk_test_YOUR_KEY"
  }
});

const data = await response.json();
import java.net.http.*;
import java.net.URI;

var client = HttpClient.newHttpClient();

var request = HttpRequest.newBuilder()
    .uri(URI.create("https://sandbox-api.biklo.com/v1/products?updated_since=2026-09-01T00%3A00%3A00Z"))
    .header("Authorization", "Bearer bk_test_YOUR_KEY")
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();

var response = client.send(request, HttpResponse.BodyHandlers.ofString());