REST API v1

Integration via REST API

Fetch La Grana products, stock levels and your individual prices directly into your system. Full automation with JWT authentication.

🔐
JWT Auth
Secure authentication with JWT tokens and automatic refresh.
REST + JSON
Standard REST endpoints with JSON responses. Simple and predictable.
💰
Individual prices
Each account receives individual prices and discount — reflected directly in API responses.
Authentication
La Grana API uses JWT (JSON Web Tokens). First obtain a token, then attach it to every request in the Authorization header.
POST /api/token/ Get access token
FieldTypeDescription
usernamerequiredYour La Grana system login
passwordrequiredYour password
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"}'
Response
{
  "access":  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

The access token is valid for 60 minutes. Refresh via POST /api/token/refresh/ with the refresh token.

Attach token to every request:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Base URL and format
Direct all requests to the address below. The API returns and accepts data in JSON format (UTF-8). All list endpoints are paginated — use the next field to iterate through pages.
https://b2b.lagrana.pl/api/
Paginated response
{
  "count": 1248,        // total number of results
  "next":  "https://b2b.lagrana.pl/api/products/?page=2",
  "previous": null,
  "results": [ /* ... */ ]
}
Products
Full product catalog with descriptions, images, attributes and your individual prices. client_price is the final price after discount; client_price_without_discount is the base price before discount.
GET /api/products/ Product list (paginated)
ParameterTypeDescription
pageoptionalPage number (default 1)
brandoptionalFilter by brand name
categoriesoptionalFilter by category ID
statusoptionalFilter by status ID
activeoptionaltrue / false
searchoptionalSearch by SKU or status

Response fields

id sku name brand ean active vat_rate for_who advantages stock_warehouse stock_supplier added_at updated_at client_price client_price_without_discount categories [ ] status { } prod_image [ ] description { } additional [ ]

price fields   nested objects/arrays

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"
Response
{
  "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",               // price after discount
      "client_price_without_discount": "99.00", // base price
      "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...",
        // also: 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" }
      ]
    }
  ]
}
GET /api/products/{id}/ Single product by ID

Returns a single product object with the same fields as the list. Use the numeric internal ID from the id field, or use the products-sku endpoint to look up by SKU.

Product by SKU
Fetch a single product directly by its SKU code — useful for synchronizing with external systems where the internal ID is unknown.
GET /api/products-sku/{sku}/ Product by 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"

Returns the same fields as GET /api/products/{id}/. Returns 404 if the SKU does not exist.

GET /api/products-sku/ Filterable list

Same filtering parameters as /api/products/ (brand, categories, status, active, search). The detail endpoint uses SKU as the key instead of numeric ID.

Categories
Full category list with multilingual names. Use the parent field to build a category tree.
GET /api/categories/ Category list
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"
Response
[
  {
    "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 of parent category, null = top level
  }
]
Stock levels
Lightweight endpoint for checking availability without fetching full product data. Supports the same filters as /api/products/. Default page size is 1 000 — for full catalog sync use ?all=1.
GET /api/stock/ Stock for all active products
ParameterTypeDescription
alloptionalSet to 1 to disable pagination and return all products in a single response. Recommended for full catalog sync.
page_sizeoptionalNumber of results per page (default: 1000, max: 10000). Ignored when all=1.
brandoptionalFilter by brand
categoriesoptionalFilter by category ID
searchoptionalSearch by 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"
With pagination (default)
GET /api/stock/
GET /api/stock/?page=2
GET /api/stock/?page_size=500

{
  "count":    4432,   // total products
  "next":     "https://.../api/stock/?page=2",
  "previous": null,
  "results": [
    { "sku": "LG-12345", "stock_warehouse": 24, "stock_supplier": 10 },
    ...
  ]
}
All products at once (?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 },
  ...
]  // flat array, no count/next/previous wrapper

stock_warehouse — quantity in La Grana warehouse
stock_supplier — quantity available at supplier

Orders
Create and list your orders. Each account only sees its own orders. You can reference products by internal ID or SKU. Order creation requires delivery address and a shipping label file — use multipart/form-data (not JSON).
GET /api/orders/ List your orders
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"
Response
{
  "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"
        }
      ]
    }
  ]
}
POST /api/orders/ Create an order
Content-Type: multipart/form-data

Delivery address (required)

FieldTypeDescription
first_namerequiredRecipient's first name
last_namerequiredRecipient's last name
addressrequiredStreet and number
postalrequiredPostal code
cityrequiredCity
shipping_labeloptionalShipping label file (PDF/ZPL)
country_codeoptional2-letter country code (default: PL)
phoneoptionalRecipient's phone
delivery_emailoptionalRecipient's e-mail

Order fields

FieldTypeDescription
itemsrequiredJSON string — array of items (see below)
dropshippingoptionalDropshipping order — true / false (default: false). If false, a TRANSPORT-PL line is added in Vendo; if true, a DROPSHIPPING line is added.
external_idoptionalYour own reference number
notesoptionalOrder notes

Each item in the items JSON array:

FieldTypeDescription
productproduct or skuInternal product ID
skuproduct or skuProduct SKU (alternative to product)
qtyrequiredQuantity (min. 1)

Prices, VAT rates and line totals are calculated server-side from the client's assigned price list — do not send price data.

Validation — what returns 400

Product not found
If any SKU or product ID is not found in the catalog, the entire order is rejected.
Insufficient stock
If the requested quantity exceeds warehouse stock for any item, the order is rejected. All stock errors are returned at once.
{
  "non_field_errors": [
    "Product 'LG-99999' does not exist.",
    "Insufficient stock for 'LG-12345': ordered 10, available 3."
  ]
}
All-or-nothing
The order is only created in the database after all items pass validation — no partial orders are saved.
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"
XML Feed
An alternative to JSON API — a product feed in XML format, accessible via a unique token (no JWT required). Useful for platforms that support XML product import (e.g. PrestaShop, IdoSell). Contact us to obtain your token.
GET /api/xml/{token}/ Product XML feed
XML
<?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>   <!-- your price -->
    <stock>24</stock>
    <image>https://cdn.lagrana.pl/img/lg-12345-1.jpg</image>
    <active>true</active>
  </product>
</products>

The feed requires no authentication header — the token in the URL grants access. Prices reflect your individual rates.

Error codes
The API uses standard HTTP codes. Error responses include a detail field with a description.
CodeMeaning
200Success
201Created (e.g. new order)
400Request error — check parameters or required fields
401Missing authentication or expired token
403No permission to access resource
404Resource not found
429Request limit exceeded (rate limiting)
Ready to integrate?

Log in and start using the API

API access is granted on request — contact us or log in if you already have an account.

Log in Request access