Core concepts
A few ideas apply to the whole API. Learn them once and every endpoint behaves the way you expect.
#Your ids and BoxLatvia's ids
Every location and box has a BoxLatvia id (a uuid). You can also give it your own external_id: up to 100 characters, unique among your own locations or boxes, set once and then fixed. With it you can:
- find things:
GET /v1/boxes?external_id=SKU-3M - create or update in one call, without storing BoxLatvia's ids:
PUT /v1/boxes/by-external-id/SKU-3M - point a box at its location:
"location_external_id": "WH-IMANTA"
Different suppliers can use the same external id; they never collide.
#Reference codes
Amenities, districts and price units are always sent and received as stable codes such as riga-imanta or menesi, never as names or ids. List them with Reference data. An unknown code is a 422 that names the offending codes.
features is a list of codes that replaces the whole set. Send the complete list you want, not additions; send [] to clear.
#Conditional updates
Every single-object GET returns an ETag. To change or delete that object, send it back in If-Match. This stops you from overwriting a change someone else made, for example in the partner portal, after you read the object.
- No
If-Match→428 Precondition Required - The object changed since you read it →
412 Precondition Failed: read it again and retry - Send the value exactly as you received it (it may look like
W/"…") - Every successful change returns the new
ETag
curl -X PATCH "$API/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"address": "Kurzemes prospekts 3, Rīga"
}'const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3`, {
method: "PATCH",
headers: { Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify({
"address": "Kurzemes prospekts 3, Rīga"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.patch(
f"{API}/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3",
auth=(CLIENT_ID, CLIENT_SECRET),
json={
"address": "Kurzemes prospekts 3, Rīga",
},
)
print(r.status_code, r.json()){
"type": "https://api.boxlatvia.lv/problems/precondition-required",
"title": "Precondition required",
"status": 428,
"detail": "Send the current version in an If-Match header (the ETag from a GET of this resource).",
"request_id": "f61a59eb-da5b-4d6f-8308-caa9ddd62ba6"
}curl -X PATCH "$API/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "If-Match: W/\"a15c541949287f3563009a45\"" \
-d '{
"address": "Cita adrese"
}'const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3`, {
method: "PATCH",
headers: { Authorization: auth, "Content-Type": "application/json", "If-Match": "W/\"a15c541949287f3563009a45\"" },
body: JSON.stringify({
"address": "Cita adrese"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.patch(
f"{API}/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3",
auth=(CLIENT_ID, CLIENT_SECRET),
headers={"If-Match": "W/\"a15c541949287f3563009a45\""},
json={
"address": "Cita adrese",
},
)
print(r.status_code, r.json()){
"type": "https://api.boxlatvia.lv/problems/precondition-failed",
"title": "Precondition failed",
"status": 412,
"detail": "The resource changed since you read it. GET it again and retry with the new ETag.",
"request_id": "98ae51e1-6505-43f4-8589-b524d2dd65c2"
}PUT …/by-external-id/… endpoints: they do not need If-Match.#Safe retries
Add an Idempotency-Key header (1 to 200 printable characters, no spaces; a UUID is perfect) to any POST, PUT, PATCH or DELETE. If your request times out, send it again with the same key and you get the original result, marked Idempotent-Replayed: true, instead of a second object.
- Same key, same request → the stored answer is replayed (kept for 24 hours)
- Same key, different request →
422 - Same key while the first call is still running →
409withRetry-After: 2
Use a fresh key for every new operation, and the same key only for retries of the same one.
curl -X POST "$API/v1/locations" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ebfa60c9-5887-4526-9615-2d2ebdd9a0ed" \
-d '{
"name": "Imanta noliktava",
"external_id": "WH-IMANTA",
"description": "Sausa, apsildāma noliktava ar 24/7 piekļuvi",
"address": "Kurzemes prospekts 1, Rīga",
"district": "riga-imanta",
"latitude": 56.9496,
"longitude": 24.0536,
"features": [
"24-7-piekluve",
"apkure",
"video-noverosana",
"viedatslega",
"autostavvieta"
]
}'const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/locations`, {
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json", "Idempotency-Key": "ebfa60c9-5887-4526-9615-2d2ebdd9a0ed" },
body: JSON.stringify({
"name": "Imanta noliktava",
"external_id": "WH-IMANTA",
"description": "Sausa, apsildāma noliktava ar 24/7 piekļuvi",
"address": "Kurzemes prospekts 1, Rīga",
"district": "riga-imanta",
"latitude": 56.9496,
"longitude": 24.0536,
"features": [
"24-7-piekluve",
"apkure",
"video-noverosana",
"viedatslega",
"autostavvieta"
]
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.post(
f"{API}/v1/locations",
auth=(CLIENT_ID, CLIENT_SECRET),
headers={"Idempotency-Key": "ebfa60c9-5887-4526-9615-2d2ebdd9a0ed"},
json={
"name": "Imanta noliktava",
"external_id": "WH-IMANTA",
"description": "Sausa, apsildāma noliktava ar 24/7 piekļuvi",
"address": "Kurzemes prospekts 1, Rīga",
"district": "riga-imanta",
"latitude": 56.9496,
"longitude": 24.0536,
"features": [
"24-7-piekluve",
"apkure",
"video-noverosana",
"viedatslega",
"autostavvieta",
],
},
)
print(r.status_code, r.json()){
"id": "9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3",
"external_id": "WH-IMANTA",
"name": "Imanta noliktava",
"description": "Sausa, apsildāma noliktava ar 24/7 piekļuvi",
"address": "Kurzemes prospekts 1, Rīga",
"district": "riga-imanta",
"district_name": "Rīga - Imanta",
"latitude": 56.9496,
"longitude": 24.0536,
"box_count": 0,
"features": [
"24-7-piekluve",
"apkure",
"autostavvieta",
"video-noverosana",
"viedatslega"
],
"missing": [],
"created_at": "2026-11-07T09:30:12.418Z",
"updated_at": "2026-11-07T09:30:12.418Z"
}#Dry runs
Add ?validate_only=true to a create, update, delete, upload or submit. The API runs every check, including database rules, and tells you what would happen, but saves nothing. The answer carries X-Validate-Only: true. Use it to test a new payload; there is no separate sandbox.
curl -X POST "$API/v1/locations?validate_only=true" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "Mārupes noliktava",
"district": "marupes-novads-pinki"
}'const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/locations?validate_only=true`, {
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Mārupes noliktava",
"district": "marupes-novads-pinki"
}),
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.post(
f"{API}/v1/locations?validate_only=true",
auth=(CLIENT_ID, CLIENT_SECRET),
json={
"name": "Mārupes noliktava",
"district": "marupes-novads-pinki",
},
)
print(r.status_code, r.json()){
"id": "bf18a79f-f762-4775-a17d-5ffa0623ebd7",
"external_id": null,
"name": "Mārupes noliktava",
"description": null,
"address": null,
"district": "marupes-novads-pinki",
"district_name": "Mārupes novads - Piņķi",
"latitude": null,
"longitude": null,
"box_count": 0,
"features": [],
"missing": [
"address",
"coordinates",
"features"
],
"created_at": "2026-11-07T09:30:12.418Z",
"updated_at": "2026-11-07T09:30:12.418Z"
}#Pagination
List endpoints return pages, oldest first, as { "data": [...], "next_cursor": "..." }. Pass next_cursor back as ?cursor=… and stop when it is null. limit is 1 to 200 (default 50). Cursors are opaque: do not build or inspect them.
curl "$API/v1/locations?limit=2" \
-u "$CLIENT_ID:$CLIENT_SECRET"const API = "https://api.boxlatvia.lv";
const auth = "Basic " + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const res = await fetch(`${API}/v1/locations?limit=2`, {
headers: { Authorization: auth },
});
console.log(res.status, await res.json());import requests
API = "https://api.boxlatvia.lv"
r = requests.get(
f"{API}/v1/locations?limit=2",
auth=(CLIENT_ID, CLIENT_SECRET),
)
print(r.status_code, r.json()){
"data": [
{
"id": "9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3",
"external_id": "WH-IMANTA",
"name": "Imanta noliktava",
"description": "Sausa, apsildāma noliktava ar 24/7 piekļuvi",
"address": "Kurzemes prospekts 1, Rīga",
"district": "riga-imanta",
"district_name": "Rīga - Imanta",
"latitude": 56.9496,
"longitude": 24.0536,
"box_count": 0,
"features": [
"24-7-piekluve",
"apkure",
"autostavvieta",
"video-noverosana",
"viedatslega"
],
"missing": [],
"created_at": "2026-11-07T09:30:12.418Z",
"updated_at": "2026-11-07T09:30:12.418Z"
},
{
"id": "4758ace9-4962-4906-bb6c-736ec772cb37",
"external_id": null,
"name": "Teika noliktava",
"description": null,
"address": null,
"district": null,
"district_name": null,
"latitude": null,
"longitude": null,
"box_count": 0,
"features": [],
"missing": [
"address",
"coordinates",
"district",
"features"
],
"created_at": "2026-11-07T09:30:12.418Z",
"updated_at": "2026-11-07T09:30:12.418Z"
}
],
"next_cursor": "WyIyMDI2LTExLTA2IDEwOjAwOjAwLjAwMDAwMyswMCIsImE5NmM5NmQzLWFiMjAtNDYxYi1hZmExLTUwYWQ3YzU2ZTQ3ZSJd"
}#Completeness: the missing list
The API accepts incomplete locations and boxes so that you can build them up step by step. Each object has a missing list that says what is still needed:
| Object | Needs before it counts | Values in missing |
|---|---|---|
| Location | an address, a map position, a district and at least 5 amenities, to be found by customers | address, coordinates, district, features |
| Box | a description, a price and price unit, a size and at least one amenity, to be submitted for review | description, price, price_unit, size, features |
#Publishing
Boxes have a status that you cannot set directly. You can ask for a review, and BoxLatvia staff publish.
At any point withdraw takes the box back to DRAFT. Changes you make to a published box (price, stock, text) appear on the website straight away; only the status change needs approval.
#Deleting
- A location with boxes cannot be deleted (
409). - A box with orders cannot be deleted (
409): setavailable_unitsto0to stop new bookings instead. - Deleting a box or a location deletes its pictures too.