/v1/public/health(no auth)Unauthenticated heartbeat. Returns server time so you can detect your own clock skew before signing requests. Safe to poll.
curl "https://api.cijena-hr.com/v1/public/health"Read-only access to grocery and drogerie product prices across 20+ Croatian retailers. JSON over HTTPS. Authenticated with a Bearer token; each key is per-minute rate limited.
Email api@cijena-hr.com with your use case and the volume you expect. Keys are issued on a per-partner basis with a custom rate limit. Free tier defaults to 60 requests/minute.
Send your token in the Authorization header on every request.
Authorization: Bearer pck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/v1/public/health(no auth)Unauthenticated heartbeat. Returns server time so you can detect your own clock skew before signing requests. Safe to poll.
curl "https://api.cijena-hr.com/v1/public/health"/v1/public/search?q=<query>Search products by name. Returns up to limit results (default 30, max 100) with all live offers and stock signal.
curl "https://api.cijena-hr.com/v1/public/search?q=mlijeko&limit=5" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/products/:idFull offer list and metadata for a single product UUID.
curl "https://api.cijena-hr.com/v1/public/products/<product-id>" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/products/:id/history?days=30Daily minimum price across all listings for a product, last days days (default 30, max 365). One point per day with at least one observation.
curl "https://api.cijena-hr.com/v1/public/products/<product-id>/history?days=90" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/products/:id/by-storePer-store prices for a product over the last 14 days. Only stores with crawler-attributed observations and lat/lon are returned. Useful for finding the cheapest local branch.
curl "https://api.cijena-hr.com/v1/public/products/<product-id>/by-store" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/usage?days=7Your own consumption over the last days days (default 7, max 90): total request count and a breakdown by status code, plus your current per-minute rate limit.
curl "https://api.cijena-hr.com/v1/public/usage?days=30" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/retailersAll tracked retailers with country code, base URL, and category tags.
curl "https://api.cijena-hr.com/v1/public/retailers" \
-H "Authorization: Bearer $PCK_TOKEN"X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (epoch seconds when the counter resets).Retry-After in seconds. Body is {"limit":60,"window":"1m"}.{
"query": "mlijeko",
"resultCount": 12,
"results": [
{
"productId": "uuid",
"name": "Dukat Mlijeko 2.8% 1L",
"brand": "Dukat",
"ean": "3858881212345",
"imageUrl": "https://...",
"lowestPrice": 1.29,
"highestPrice": 1.59,
"offerCount": 5,
"inStockOfferCount": 4,
"allOutOfStock": false,
"offers": [
{
"retailerName": "Lidl",
"price": 1.29,
"currency": "EUR",
"url": "https://www.lidl.hr/...",
"observedAt": "2026-04-28T05:00:00.000Z",
"availability": "IN_STOCK"
}
]
}
]
}Subscribe to events with a partner-managed URL. We POST a JSON body and sign it with HMAC-SHA256 of the body using your webhook's secret as the key. Verify by computing sha256=<hex> and comparing against the X-Webhook-Signature header in constant time.
Currently emitted: price.drop (fires when a tracked listing drops more than the configured threshold within the alerts window).
/v1/public/webhooksRegister a new webhook. Body: { url, events?: string[] }. Default events list is ["price.drop"]. Response includes the secret exactly once — store it now.
curl -X POST "https://api.cijena-hr.com/v1/public/webhooks" \
-H "Authorization: Bearer $PCK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-app.example/webhooks/pck"}'/v1/public/webhooks / DELETE /v1/public/webhooks/:idList your active webhooks (no secret returned), or revoke one by id.
/v1/public/webhooks/:id/testFires a synthetic price.drop delivery to your webhook so you can verify signature handling end-to-end without waiting for a real alert. Uses the same retry semantics and writes a row to the delivery history.
curl -X POST "https://api.cijena-hr.com/v1/public/webhooks/<id>/test" \
-H "Authorization: Bearer $PCK_TOKEN"/v1/public/webhooks/:id/deliveriesLast 50 delivery attempts (capped at 200 via ?limit=) for one of your webhooks. Returns event, status, attempts, error text, duration. 404 if it doesn't belong to you.
curl "https://api.cijena-hr.com/v1/public/webhooks/<id>/deliveries" \
-H "Authorization: Bearer $PCK_TOKEN"import crypto from "node:crypto";
app.post("/webhooks/pck", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.header("X-Webhook-Signature") ?? "";
const expected =
"sha256=" + crypto.createHmac("sha256", process.env.PCK_WEBHOOK_SECRET)
.update(req.body) // raw Buffer
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(401);
}
const { event, data } = JSON.parse(req.body.toString());
// … handle event …
res.sendStatus(200);
});Drop this into a script — it'll fetch the cheapest offer for a query and bail out gracefully on rate-limit responses.
const BASE = "https://api.cijena-hr.com";
const TOKEN = process.env.PCK_TOKEN;
if (!TOKEN) throw new Error("Set PCK_TOKEN env var");
async function findCheapest(query) {
const url = new URL("/v1/public/search", BASE);
url.searchParams.set("q", query);
url.searchParams.set("limit", "1");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${TOKEN}` },
});
if (res.status === 429) {
const body = await res.json();
throw new Error(`Rate limited (${body.limit}/${body.window})`);
}
if (!res.ok) throw new Error(`API ${res.status}`);
const { results } = await res.json();
return results[0] ?? null;
}
const product = await findCheapest("mlijeko");
console.log(product?.lowestPrice, "EUR @", product?.offers[0]?.retailerName);401 — missing, malformed, or revoked token400 — invalid query parameter404 — product UUID not found429 — rate limit exceeded