Biklo for developersTESTsandbox-api.biklo.comchange

Create or update a product

POST/v1/products

scope products.write

Upsert keyed on your external_id: the same call twice updates rather than duplicating, which makes a retried push safe. Required category attributes are enforced exactly as they are for a seller using the dashboard. Send images in the same call — one request per product, images included.

Request body#

FieldTypeRequiredDescription
external_idstringrequiredYour own product id, and the key the upsert matches on — the same call twice updates rather than duplicating. Required: it is the only thing tying a listing here back to a row in your system, and without it a retry after a timeout would create a second listing you have no way to find. Unique per seller.
titlestringrequiredRequired on create; on update, omit it to leave the current title. Held to the same length limits as the seller dashboard.
descriptionstringrequiredRequired on create; omit on update to keep the current text. Same length limits as the dashboard.
category_idintegerrequiredFrom GET /v1/categories. Required on create. The category decides which attributes are mandatory, so read GET /v1/categories/{id}/attributes before your first push. A translated category id resolves to its Romanian master, so you may send the id you got in any language.
priceobjectrequiredThe asking price.
price.amountintegerrequiredMinor units — 249900 is 2 499,00. Required on create. Never send a decimal.
price.currencystringoptionalShown for readability only.

Not supported Ignored on write. A listing is priced in its country's currency, which you cannot choose here — GET /v1/products/{id} returns the currency the listing actually has. Sending a different one does not fail and does not change anything.

price.negotiablebooleanoptionalWhether you accept price offers on this listing. Defaults to false. Only has an effect if your company has offers switched on.
stockintegerrequiredUnits available. Required on create — send 0 for something you are out of, which is a real answer and is accepted. Must be a non-negative integer (invalid_stock, 422). Afterwards, PATCH /v1/products/{id}/stock is the cheaper way to move it.
skustringoptionalYour stock-keeping unit. Stored and returned as-is; not used for matching — that is external_id.
eanstringoptionalEAN barcode. Stored and returned as-is.
gtinstringoptionalGTIN. Stored and returned as-is.
mpnstringoptionalManufacturer part number. Stored and returned as-is.
attributesarrayoptionalCategory attributes — brand, condition, frame size. Whether this is optional depends entirely on the category: a category with required attributes rejects the create with missing_required_attribute and names each one. Send the array and it REPLACES what the listing had; omit the key entirely and the existing values are kept.
attributes[].field_idintegerrequiredThe attribute, from GET /v1/categories/{id}/attributes. An entry with no field_id is skipped silently. Translated ids resolve to their Romanian master.
attributes[].option_idintegeroptionalThe chosen option, for attributes that have a fixed list. Translated ids resolve to their master.
attributes[].valuestringoptionalA free-text value, for attributes with no option list. Send option_id or value depending on the attribute — GET /v1/categories/{id}/attributes says which.
city_idintegeroptionalWhere the item is. Almost never needed, and best left out: with no city_id the listing takes the city, county and street of your default shipping address, which is where the courier collects from. Send it only when this item really does ship from somewhere else — an override sets the city but no street, so the listing shows no address at all. From GET /v1/geo/cities.
county_idintegeroptionalDefaults to the county the city belongs to. Only send it to override that.
shippingobjectoptionalParcel dimensions and weight. Omit the whole object and the listing is still shipped by us, but with zero weight and dimensions — which quotes the courier wrongly. Send real numbers.
shipping.ship_with_usbooleanoptionalAlways true, and not yours to set.

Not supported Ignored on write. Every listing created through this API ships with Biklo — we quote the courier, the buyer pays delivery at checkout and we book the waybill. A listing with this off cannot be ordered at all, so there is nothing useful to switch off. Sending false does not fail and does not change anything.

shipping.weight_kgnumberoptionalKilograms, decimals allowed. Defaults to 0. Drives the shipping class and the courier quote, so a zero weight quotes the item wrongly.
shipping.width_cmnumberoptionalCentimetres. Defaults to 0.
shipping.height_cmnumberoptionalCentimetres. Defaults to 0.
shipping.length_cmnumberoptionalCentimetres. Defaults to 0.
imagesarrayoptionalImage URLs, first is the cover — plain strings or {"url": "…"} objects. Referenced, not downloaded, exactly like POST /v1/products/{id}/images. Send the array and it REPLACES the listing's images (an identical list changes nothing); omit the key and they are left alone; [] removes them all. Every entry must be an http(s) URL (invalid_image_url, naming the position), at most 20 (too_many_images).

Example

{
    "external_id": "ERP-1001",
    "title": "Bicicleta MTB Hardtail 29 inch",
    "description": "A hardtail mountain bike in very good condition, barely used.",
    "category_id": 4291,
    "price": {
        "amount": 249900,
        "currency": "RON",
        "negotiable": true
    },
    "stock": 4,
    "sku": "MTB-29-001",
    "attributes": [
        {
            "field_id": 32,
            "option_id": 226
        },
        {
            "field_id": 47,
            "option_id": 422
        }
    ],
    "shipping": {
        "weight_kg": 14.5,
        "width_cm": 80,
        "height_cm": 120,
        "length_cm": 180
    },
    "images": [
        "https://cdn.example.com/erp/mtb-front.jpg",
        "https://cdn.example.com/erp/mtb-side.jpg"
    ]
}

Response#

Returns the same object as One product (GET /v1/products/{id}), described in full there.

Example

{
    "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 POST "https://sandbox-api.biklo.com/v1/products" \
  -H "Authorization: Bearer bk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "ERP-1001",
    "title": "Bicicleta MTB Hardtail 29 inch",
    "description": "A hardtail mountain bike in very good condition, barely used.",
    "category_id": 4291,
    "price": {
        "amount": 249900,
        "currency": "RON",
        "negotiable": true
    },
    "stock": 4,
    "sku": "MTB-29-001",
    "attributes": [
        {
            "field_id": 32,
            "option_id": 226
        },
        {
            "field_id": 47,
            "option_id": 422
        }
    ],
    "shipping": {
        "weight_kg": 14.5,
        "width_cm": 80,
        "height_cm": 120,
        "length_cm": 180
    },
    "images": [
        "https://cdn.example.com/erp/mtb-front.jpg",
        "https://cdn.example.com/erp/mtb-side.jpg"
    ]
}'
<?php

$ch = curl_init('https://sandbox-api.biklo.com/v1/products');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer bk_test_YOUR_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
      'external_id' => 'ERP-1001',
      'title' => 'Bicicleta MTB Hardtail 29 inch',
      'description' => 'A hardtail mountain bike in very good condition, barely used.',
      'category_id' => 4291,
      'price' => [
        'amount' => 249900,
        'currency' => 'RON',
        'negotiable' => true,
      ],
      'stock' => 4,
      'sku' => 'MTB-29-001',
      'attributes' => [
        0 => [
          'field_id' => 32,
          'option_id' => 226,
        ],
        1 => [
          'field_id' => 47,
          'option_id' => 422,
        ],
      ],
      'shipping' => [
        'weight_kg' => 14.5,
        'width_cm' => 80,
        'height_cm' => 120,
        'length_cm' => 180,
      ],
      'images' => [
        0 => 'https://cdn.example.com/erp/mtb-front.jpg',
        1 => 'https://cdn.example.com/erp/mtb-side.jpg',
      ],
    ]),
]);

$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("POST"),
    "https://sandbox-api.biklo.com/v1/products");

request.Content = new StringContent(
    @"{ ""external_id"": ""ERP-1001"", ""title"": ""Bicicleta MTB Hardtail 29 inch"", ""description"": ""A hardtail mountain bike in very good condition, barely used."", ""category_id"": 4291, ""price"": { ""amount"": 249900, ""currency"": ""RON"", ""negotiable"": true }, ""stock"": 4, ""sku"": ""MTB-29-001"", ""attributes"": [ { ""field_id"": 32, ""option_id"": 226 }, { ""field_id"": 47, ""option_id"": 422 } ], ""shipping"": { ""weight_kg"": 14.5, ""width_cm"": 80, ""height_cm"": 120, ""length_cm"": 180 }, ""images"": [ ""https://cdn.example.com/erp/mtb-front.jpg"", ""https://cdn.example.com/erp/mtb-side.jpg"" ] }",
    System.Text.Encoding.UTF8,
    "application/json");

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

response = requests.post(
    "https://sandbox-api.biklo.com/v1/products",
    headers={"Authorization": "Bearer bk_test_YOUR_KEY"},
    json={
        "external_id": "ERP-1001",
        "title": "Bicicleta MTB Hardtail 29 inch",
        "description": "A hardtail mountain bike in very good condition, barely used.",
        "category_id": 4291,
        "price": {
            "amount": 249900,
            "currency": "RON",
            "negotiable": True
        },
        "stock": 4,
        "sku": "MTB-29-001",
        "attributes": [
            {
                "field_id": 32,
                "option_id": 226
            },
            {
                "field_id": 47,
                "option_id": 422
            }
        ],
        "shipping": {
            "weight_kg": 14.5,
            "width_cm": 80,
            "height_cm": 120,
            "length_cm": 180
        },
        "images": [
            "https://cdn.example.com/erp/mtb-front.jpg",
            "https://cdn.example.com/erp/mtb-side.jpg"
        ]
    },
)

data = response.json()
const response = await fetch("https://sandbox-api.biklo.com/v1/products", {
  method: "POST",
  headers: {
    Authorization: "Bearer bk_test_YOUR_KEY",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
      "external_id": "ERP-1001",
      "title": "Bicicleta MTB Hardtail 29 inch",
      "description": "A hardtail mountain bike in very good condition, barely used.",
      "category_id": 4291,
      "price": {
          "amount": 249900,
          "currency": "RON",
          "negotiable": true
      },
      "stock": 4,
      "sku": "MTB-29-001",
      "attributes": [
          {
              "field_id": 32,
              "option_id": 226
          },
          {
              "field_id": 47,
              "option_id": 422
          }
      ],
      "shipping": {
          "weight_kg": 14.5,
          "width_cm": 80,
          "height_cm": 120,
          "length_cm": 180
      },
      "images": [
          "https://cdn.example.com/erp/mtb-front.jpg",
          "https://cdn.example.com/erp/mtb-side.jpg"
      ]
  })
});

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"))
    .header("Authorization", "Bearer bk_test_YOUR_KEY")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString(
        "{ \"external_id\": \"ERP-1001\", \"title\": \"Bicicleta MTB Hardtail 29 inch\", \"description\": \"A hardtail mountain bike in very good condition, barely used.\", \"category_id\": 4291, \"price\": { \"amount\": 249900, \"currency\": \"RON\", \"negotiable\": true }, \"stock\": 4, \"sku\": \"MTB-29-001\", \"attributes\": [ { \"field_id\": 32, \"option_id\": 226 }, { \"field_id\": 47, \"option_id\": 422 } ], \"shipping\": { \"weight_kg\": 14.5, \"width_cm\": 80, \"height_cm\": 120, \"length_cm\": 180 }, \"images\": [ \"https://cdn.example.com/erp/mtb-front.jpg\", \"https://cdn.example.com/erp/mtb-side.jpg\" ] }"))
    .build();

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