Authorization.| Pole | Typ | Opis |
|---|---|---|
| username | wymagane | Twój login do systemu La Grana |
| password | wymagane | Twoje hasło |
import requests resp = requests.post("https://b2b.lagrana.pl/api/token/", json={"username": "twoj_login", "password": "haslo"}) token = resp.json()["access"] headers = {"Authorization": f"Bearer {token}"}
$res = file_get_contents('https://b2b.lagrana.pl/api/token/', false, stream_context_create(['http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode(['username'=>'twoj_login', 'password'=>'haslo']) ]]) ); $token = json_decode($res)->access; $authHeader = "Authorization: Bearer $token";
curl -X POST https://b2b.lagrana.pl/api/token/ \ -H "Content-Type: application/json" \ -d '{"username": "twoj_login", "password": "haslo"}'
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Token access ważny przez 60 minut. Odśwież przez POST /api/token/refresh/ podając token refresh.
Dołącz token do każdego żądania:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
next do iterowania stron.https://b2b.lagrana.pl/api/
{
"count": 1248, // łączna liczba wyników
"next": "https://b2b.lagrana.pl/api/products/?page=2",
"previous": null,
"results": [ /* ... */ ]
}
client_price to cena końcowa po rabacie; client_price_without_discount to cena bazowa przed rabatem.| Parametr | Typ | Opis |
|---|---|---|
| page | opcjonalny | Numer strony (domyślnie 1) |
| brand | opcjonalny | Filtruj po nazwie marki |
| categories | opcjonalny | Filtruj po ID kategorii |
| status | opcjonalny | Filtruj po ID statusu |
| active | opcjonalny | true / false |
| search | opcjonalny | Szukaj po SKU lub statusie |
Pola odpowiedzi
■ pola cenowe ■ zagnieżdżone obiekty/tablice
url = "https://b2b.lagrana.pl/api/products/" while url: data = requests.get(url, headers=headers).json() for p in data["results"]: print(p["sku"], p["client_price"], p["stock_warehouse"]) url = data["next"]
$url = 'https://b2b.lagrana.pl/api/products/'; while ($url) { $data = json_decode(file_get_contents($url, false, stream_context_create(['http' => ['header' => $authHeader]]) )); foreach ($data->results as $p) { echo $p->sku . ' ' . $p->client_price . "\n"; } $url = $data->next; }
curl "https://b2b.lagrana.pl/api/products/" \ -H "Authorization: Bearer $TOKEN" # kolejna strona curl "https://b2b.lagrana.pl/api/products/?page=2" \ -H "Authorization: Bearer $TOKEN"
{
"count": 1248,
"next": "https://b2b.lagrana.pl/api/products/?page=2",
"previous": null,
"results": [
{
"id": 42,
"sku": "LG-12345",
"name": "Nazwa produktu",
"brand": "BrandName",
"ean": "1234567890123",
"active": true,
"vat_rate": 23,
"for_who": "Dla niej",
"advantages": ["Zaletą jest...", "..."],
"stock_warehouse": 24,
"stock_supplier": 10,
"added_at": "2025-01-15T08:00:00Z",
"updated_at": "2026-03-01T12:00:00Z",
"client_price": "84.15", // cena po rabacie
"client_price_without_discount": "99.00", // cena bazowa
"categories": [
{ "id": 3, "name_pl": "Wibratory", "name_en": "Vibrators", "parent": null }
],
"status": { "id": 1, "name": "Nowość" },
"prod_image": [
{ "image": "https://cdn.lagrana.pl/img/lg-12345-1.jpg", "main_image": true, "hoover_image": false },
{ "image": "https://cdn.lagrana.pl/img/lg-12345-2.jpg", "main_image": false, "hoover_image": true }
],
"description": {
"id": 5, "product": 42,
"lang_pl": "Opis po polsku...",
"lang_en": "Description in English...",
"lang_de": "Beschreibung auf Deutsch...",
// oraz: lang_nl, lang_fr, lang_es, lang_it, lang_gr, lang_cz, lang_sk, lang_ua, lang_ru
},
"additional": [
{ "id": 101, "product": 42, "item": "Kolor", "value": "Czarny" },
{ "id": 102, "product": 42, "item": "Długość", "value": "18 cm" }
]
}
]
}
Zwraca jeden obiekt produktu z tymi samymi polami co lista. Użyj numerycznego ID z pola id, lub użyj endpointu products-sku do wyszukiwania po SKU.
p = requests.get(
"https://b2b.lagrana.pl/api/products-sku/LG-12345/",
headers=headers
).json()
print(p["name"], p["client_price"], p["client_price_without_discount"])$p = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/products-sku/LG-12345/', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
echo $p->name . ' ' . $p->client_price;curl "https://b2b.lagrana.pl/api/products-sku/LG-12345/" \ -H "Authorization: Bearer $TOKEN"
Zwraca te same pola co GET /api/products/{id}/. Zwraca 404 jeśli SKU nie istnieje.
Te same parametry filtrowania co /api/products/ (brand, categories, status, active, search). Endpoint szczegółowy używa SKU jako klucza zamiast numerycznego ID.
parent do budowania drzewa kategorii.cats = requests.get("https://b2b.lagrana.pl/api/categories/", headers=headers).json() for c in cats: print(c["id"], c["name_pl"])
$cats = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/categories/', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
foreach ($cats as $c) echo $c->id . ' ' . $c->name_pl . "\n";curl "https://b2b.lagrana.pl/api/categories/" \ -H "Authorization: Bearer $TOKEN"
[
{
"id": 1,
"name_pl": "Wibratory",
"name_en": "Vibrators",
"name_de": "Vibratoren",
"name_nl": null,
"name_fr": null,
"name_es": null,
"name_it": null,
"name_gr": null,
"name_cz": null,
"name_sk": null,
"name_ua": null,
"name_ru": null,
"parent": null // ID kategorii nadrzędnej, null = najwyższy poziom
}
]
/api/products/. Domyślny rozmiar strony to 1 000 — przy synchronizacji całego katalogu użyj ?all=1.| Parametr | Typ | Opis |
|---|---|---|
| all | opcjonalny | Ustaw na 1 aby wyłączyć paginację i pobrać wszystkie produkty w jednym zapytaniu. Zalecane przy pełnej synchronizacji katalogu. |
| page_size | opcjonalny | Liczba wyników na stronę (domyślnie: 1000, maks: 10000). Ignorowany przy all=1. |
| brand | opcjonalny | Filtruj po marce |
| categories | opcjonalny | Filtruj po ID kategorii |
| search | opcjonalny | Szukaj po SKU |
# wszystkie stany w jednym zapytaniu stock = requests.get("https://b2b.lagrana.pl/api/stock/?all=1", headers=headers).json() for item in stock: print(item["sku"], item["stock_warehouse"], item["stock_supplier"])
$stock = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/stock/?all=1', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
foreach ($stock as $item) {
echo $item->sku . ' ' . $item->stock_warehouse . "\n";
}# wszystkie stany naraz curl "https://b2b.lagrana.pl/api/stock/?all=1" \ -H "Authorization: Bearer $TOKEN"
GET /api/stock/
GET /api/stock/?page=2
GET /api/stock/?page_size=500
{
"count": 4432, // łączna liczba produktów
"next": "https://.../api/stock/?page=2",
"previous": null,
"results": [
{ "sku": "LG-12345", "stock_warehouse": 24, "stock_supplier": 10 },
...
]
}
?all=1)
GET /api/stock/?all=1
[
{ "sku": "LG-12345", "stock_warehouse": 24, "stock_supplier": 10 },
{ "sku": "LG-12346", "stock_warehouse": 0, "stock_supplier": 50 },
...
] // płaska tablica, bez wrappera count/next/previous
stock_warehouse — ilość w magazynie La Grana
stock_supplier — ilość dostępna u dostawcy
multipart/form-data (nie JSON).orders = requests.get("https://b2b.lagrana.pl/api/orders/", headers=headers).json() for o in orders["results"]: print(o["id"], o["status"], o["total_gross"])
$orders = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/orders/', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
foreach ($orders->results as $o) {
echo $o->id . ' ' . $o->status . ' ' . $o->total_gross . "\n";
}curl "https://b2b.lagrana.pl/api/orders/" \ -H "Authorization: Bearer $TOKEN"
{
"count": 3,
"results": [
{
"id": 7,
"external_id": "WC-1234",
"status": "new", // new | confirmed | rejected
"total_gross": "267.00",
"notes": "Uwagi do zamówienia",
"first_name": "Jan",
"last_name": "Kowalski",
"phone": "500100200",
"delivery_email": "jan@sklep.pl",
"address": "ul. Przykładowa 1",
"postal": "00-001",
"city": "Warszawa",
"country_code": "PL",
"shipping_label": "https://b2b.lagrana.pl/media/shipping_labels/etykieta.pdf",
"dropshipping": false,
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:05:00Z",
"items_read": [
{
"product": 42, "sku": "LG-12345", "name": "Nazwa produktu",
"qty": 3, "unit_price_gross": "89.00",
"vat_rate": 23, "line_total_gross": "267.00"
}
]
}
]
}
Adres dostawy (wymagane)
| Pole | Typ | Opis |
|---|---|---|
| first_name | wymagane | Imię odbiorcy |
| last_name | wymagane | Nazwisko odbiorcy |
| address | wymagane | Ulica i numer |
| postal | wymagane | Kod pocztowy |
| city | wymagane | Miasto |
| shipping_label | opcjonalny | Plik listu przewozowego (PDF/ZPL) |
| country_code | opcjonalny | 2-literowy kod kraju (domyślnie: PL) |
| phone | opcjonalny | Telefon odbiorcy |
| delivery_email | opcjonalny | E-mail odbiorcy |
Pola zamówienia
| Pole | Typ | Opis |
|---|---|---|
| items | wymagane | Ciąg JSON — tablica pozycji (patrz niżej) |
| dropshipping | opcjonalny | Zamówienie dropshipping — true / false (domyślnie: false). Przy false do ZO trafia pozycja TRANSPORT-PL, przy true — DROPSHIPPING. |
| external_id | opcjonalny | Twój numer referencyjny |
| notes | opcjonalny | Uwagi do zamówienia |
Każda pozycja w tablicy JSON items:
| Pole | Typ | Opis |
|---|---|---|
| product | product lub sku | Wewnętrzne ID produktu |
| sku | product lub sku | SKU produktu (alternatywa dla product) |
| qty | wymagane | Ilość (min. 1) |
Ceny, stawki VAT i wartości pozycji są naliczane serwerowo z cennika przypisanego do konta — nie przesyłaj danych cenowych.
Walidacja — co zwraca 400
{
"non_field_errors": [
"Produkt o SKU 'LG-99999' nie istnieje.",
"Niewystarczający stan dla 'LG-12345': zamówiono 10, dostępne 3."
]
}
import requests, json with open("etykieta.pdf", "rb") as label_file: order = requests.post( "https://b2b.lagrana.pl/api/orders/", headers=headers, data={ "first_name": "Jan", "last_name": "Kowalski", "address": "ul. Przykładowa 1", "postal": "00-001", "city": "Warszawa", "external_id": "WC-5678", "notes": "Pilne", "items": json.dumps([ {"sku": "LG-12345", "qty": 3}, {"sku": "LG-12346", "qty": 1}, ]), }, files={"shipping_label": ("etykieta.pdf", label_file, "application/pdf")}, ) print(order.json())
$ch = curl_init('https://b2b.lagrana.pl/api/orders/'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"], CURLOPT_POSTFIELDS => [ 'first_name' => 'Jan', 'last_name' => 'Kowalski', 'address' => 'ul. Przykładowa 1', 'postal' => '00-001', 'city' => 'Warszawa', 'external_id' => 'WC-5678', 'notes' => 'Pilne', 'items' => json_encode([ ['sku' => 'LG-12345', 'qty' => 3], ['sku' => 'LG-12346', 'qty' => 1], ]), 'shipping_label' => new CURLFile('/path/to/etykieta.pdf', 'application/pdf', 'etykieta.pdf'), ], ]); var_dump(json_decode(curl_exec($ch))); curl_close($ch);
curl -X POST "https://b2b.lagrana.pl/api/orders/" \ -H "Authorization: Bearer $TOKEN" \ -F "first_name=Jan" \ -F "last_name=Kowalski" \ -F "address=ul. Przykładowa 1" \ -F "postal=00-001" \ -F "city=Warszawa" \ -F "external_id=WC-5678" \ -F "notes=Pilne" \ -F 'items=[{"sku":"LG-12345","qty":3},{"sku":"LG-12346","qty":1}]' \ -F "shipping_label=@/path/to/etykieta.pdf"
403 Forbidden, skontaktuj się z nami. Każde konto widzi tylko swoje zgłoszenia; status i rozstrzygnięcie ustawia zespół La Grana po rozpatrzeniu.| Parametr | Typ | Opis |
|---|---|---|
| status | opcjonalny | Filtruj po statusie — new, in_review, accepted, rejected, resolved |
| kind | opcjonalny | complaint lub return |
| order | opcjonalny | Filtruj po ID zamówienia |
tickets = requests.get("https://b2b.lagrana.pl/api/complaints/?status=new", headers=headers).json() for t in tickets["results"]: print(t["id"], t["kind"], t["status"])
$tickets = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/complaints/?status=new', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
foreach ($tickets->results as $t) {
echo $t->id . ' ' . $t->kind . ' ' . $t->status . "\n";
}curl "https://b2b.lagrana.pl/api/complaints/?status=new" \ -H "Authorization: Bearer $TOKEN"
{
"count": 1,
"results": [
{
"id": 12,
"order": 7,
"item": 18,
"kind": "complaint", // complaint | return
"qty": 1,
"reason": "Urządzenie nie ładuje się.",
"return_tracking_number": "",
"status": "in_review", // new | in_review | accepted | rejected | resolved
"resolution_type": "", // replacement | refund | correction | repair
"resolution_note": "",
"photos": [
{ "id": 4, "photo": "https://b2b.lagrana.pl/media/complaint_photos/img1.jpg", "uploaded_at": "2026-07-20T09:12:00Z" }
],
"created_at": "2026-07-20T09:12:00Z",
"updated_at": "2026-07-21T11:00:00Z",
"resolved_at": null
}
]
}
| Pole | Typ | Opis |
|---|---|---|
| order | wymagane | ID jednego z Twoich zamówień |
| reason | wymagane | Opis zgłoszenia |
| kind | opcjonalny | complaint lub return (domyślnie: complaint) |
| item | opcjonalny | ID konkretnej pozycji zamówienia, której dotyczy zgłoszenie |
| qty | opcjonalny | Ilość, której dotyczy zgłoszenie (domyślnie: 1) |
| return_tracking_number | opcjonalny | Nr przesyłki zwrotnej |
| photos | opcjonalny | Maks. 6 plików zdjęć, powtórz pole dla każdego pliku |
Pola status, resolution_type i resolution_note są tylko do odczytu — ustawia je zespół La Grana po rozpatrzeniu. Zasubskrybuj webhook complaint.status_changed, aby otrzymać powiadomienie o rozstrzygnięciu.
import requests with open("usterka.jpg", "rb") as photo: resp = requests.post( "https://b2b.lagrana.pl/api/complaints/", headers=headers, data={ "order": 7, "kind": "complaint", "item": 18, "qty": 1, "reason": "Urządzenie nie ładuje się.", }, files={"photos": ("usterka.jpg", photo, "image/jpeg")}, ) print(resp.json())
$ch = curl_init('https://b2b.lagrana.pl/api/complaints/'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"], CURLOPT_POSTFIELDS => [ 'order' => 7, 'kind' => 'complaint', 'item' => 18, 'qty' => 1, 'reason' => 'Urządzenie nie ładuje się.', 'photos' => new CURLFile('/path/to/usterka.jpg', 'image/jpeg', 'usterka.jpg'), ], ]); var_dump(json_decode(curl_exec($ch))); curl_close($ch);
curl -X POST "https://b2b.lagrana.pl/api/complaints/" \ -H "Authorization: Bearer $TOKEN" \ -F "order=7" \ -F "kind=complaint" \ -F "item=18" \ -F "qty=1" \ -F "reason=Urządzenie nie ładuje się." \ -F "photos=@/path/to/usterka.jpg"
| Parametr | Typ | Opis |
|---|---|---|
| sku | opcjonalny | Filtruj po dokładnym SKU |
| search | opcjonalny | Szukaj po SKU |
discontinued = requests.get("https://b2b.lagrana.pl/api/discontinued-products/", headers=headers).json() for item in discontinued["results"]: print(item["sku"], item["discontinued_at"])
$discontinued = json_decode(file_get_contents(
'https://b2b.lagrana.pl/api/discontinued-products/', false,
stream_context_create(['http' => ['header' => $authHeader]])
));
foreach ($discontinued->results as $item) {
echo $item->sku . ' ' . $item->discontinued_at . "\n";
}curl "https://b2b.lagrana.pl/api/discontinued-products/" \ -H "Authorization: Bearer $TOKEN"
{
"count": 2,
"next": null,
"previous": null,
"results": [
{ "id": 1, "sku": "LG-12345", "discontinued_at": "2026-06-11T10:32:00Z" },
{ "id": 2, "sku": "LG-12346", "discontinued_at": "2026-06-10T08:15:00Z" }
]
}
Tylko konta administratorów. Data discontinued_at jest ustawiana automatycznie na bieżącą datę i godzinę. Jeśli dane SKU jest już na liście, zwracany jest istniejący wpis bez zmian.
| Pole | Typ | Opis |
|---|---|---|
| sku | wymagany | SKU wycofanego produktu |
resp = requests.post("https://b2b.lagrana.pl/api/discontinued-products/", headers=headers, json={"sku": "LG-12345"}) print(resp.json())
$ch = curl_init('https://b2b.lagrana.pl/api/discontinued-products/'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode(['sku' => 'LG-12345']), ]); var_dump(json_decode(curl_exec($ch))); curl_close($ch);
curl -X POST "https://b2b.lagrana.pl/api/discontinued-products/" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"sku": "LG-12345"}'
{ "id": 3, "sku": "LG-12345", "discontinued_at": "2026-06-11T16:00:00Z" }
Tylko konta administratorów — subskrypcje są konfigurowane przez La Grana na Twoją prośbę.
| Pole | Typ | Opis |
|---|---|---|
| event_type | wymagany | Jedno z: stock.changed, price.changed, product.updated, order.cancelled, complaint.status_changed |
| target_url | wymagany | Twój endpoint HTTPS, który odbierze POST |
{
"event_id": "3f9c2e6a-1b2d-4e5f-8a9b-0c1d2e3f4a5b",
"event_type": "price.changed",
"sku": "LG-12345",
"changes": {
"price_hurt_pln": { "old": "12.50", "new": "13.00" }
},
"timestamp": "2026-07-11T14:32:05.123456+00:00"
}
Każde żądanie ma podpis HMAC-SHA256, dzięki czemu możesz zweryfikować, że naprawdę pochodzi od nas:
| Nagłówek | Opis |
|---|---|
| X-LaGrana-Signature | sha256=<hex-digest> |
| X-LaGrana-Timestamp | Unix seconds — odrzuć jeśli starsze niż 300s (ochrona przed replay) |
| X-LaGrana-Event | event_type |
| X-LaGrana-Delivery | event_id |
import hmac, hashlib expected = hmac.new( SECRET.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256, ).hexdigest() is_valid = hmac.compare_digest(f"sha256={expected}", request.headers["X-LaGrana-Signature"])
<?xml version="1.0" encoding="UTF-8"?> <products> <product> <id>42</id> <sku>LG-12345</sku> <name>Nazwa produktu</name> <description>Opis po polsku...</description> <category>Wibratory</category> <brand>BrandName</brand> <price>84.15</price> <!-- Twoja cena --> <stock>24</stock> <image>https://cdn.lagrana.pl/img/lg-12345-1.jpg</image> <active>true</active> </product> </products>
Feed nie wymaga nagłówka uwierzytelniającego — token w URL stanowi dostęp. Ceny odzwierciedlają Twoje indywidualne stawki.
detail z opisem.| Kod | Znaczenie |
|---|---|
| 200 | Sukces |
| 201 | Zasób utworzony (np. nowe zamówienie) |
| 400 | Błąd w żądaniu — sprawdź parametry lub wymagane pola |
| 401 | Brak uwierzytelnienia lub wygasły token |
| 403 | Brak uprawnień do zasobu |
| 404 | Zasób nie istnieje |
| 429 | Przekroczono limit zapytań (rate limiting) |
Zaloguj się i zacznij korzystać z API
Dostęp do API przyznajemy na wniosek — skontaktuj się z nami lub zaloguj, jeśli masz już konto.