BoxLatviaDevelopers
Get access
Guides

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:

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.

Missing If-Match
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"
}'
428 Precondition Required
{
  "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"
}
Outdated ETag
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"
}'
412 Precondition Failed
{
  "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"
}
Tip
If you want your system's data to win, for example in a nightly feed, use the 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.

Use a fresh key for every new operation, and the same key only for retries of the same one.

A create with an idempotency key
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"
  ]
}'
201 CreatedETag: W/"a15c541949287f3563009a45"Location: https://api.boxlatvia.lv/v1/locations/9d80d5f4-d17a-4f3e-99f6-4ab43b132bc3
{
  "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.

Dry run
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"
}'
200 OKX-Validate-Only: true
{
  "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.

A first page
curl "$API/v1/locations?limit=2" \
  -u "$CLIENT_ID:$CLIENT_SECRET"
200 OK
{
  "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:

ObjectNeeds before it countsValues in missing
Locationan address, a map position, a district and at least 5 amenities, to be found by customersaddress, coordinates, district, features
Boxa description, a price and price unit, a size and at least one amenity, to be submitted for reviewdescription, 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