Skip to content

Locations

Only partner organizations can create locations, and doing so means claiming that physical store. Verve’s job is to converge every partner’s claim on the same store (same externalId) into one consistent record so targeting stays precise — when two partners claim the same store with different details, that’s a conflict, and it gets resolved into a single canonical version.

Listing returns that tenant-wide canonical view, not just the locations your own organization or project claimed — use onlyOwn to narrow it down to yours.

POST /v1alpha2/inventory/locations returns one row per physical location (externalId). When more than one partner has uploaded the same physical store, the row merges every partner’s version instead of returning a duplicate per partner.

POST /v1alpha2/inventory/locations HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
X-Verve-Namespace: project:01ABCDEFGHIJKLMNOPQRSTUVWX
Content-Type: application/json
{
"page": 1,
"pageSize": 25,
"filters": {
"onlyConflicts": false,
"onlyOwn": false,
"externalId": "store-1",
"name": "edeka",
"modifiedSince": "2026-09-01T00:00:00Z"
},
"sort": { "field": "LOCATION_SORT_FIELD_CITY", "direction": "SORT_DIRECTION_ASC" }
}
  • page / pageSize — pageSize defaults to 50 and is capped at 100.
  • filters.onlyConflicts — only return locations whose versions disagree (see Conflicts below).
  • filters.onlyOwn — only return locations directly created under your organization/project.
  • filters.externalId — only return locations whose externalId contains this value (case-insensitive substring match).
  • filters.name — only return locations whose metadata.name contains this value (case-insensitive substring match).
  • filters.modifiedSince — only return locations changed strictly after this instant (RFC 3339). See Listing only what changed below.
  • sort.field — the field you want to sort, like LOCATION_SORT_FIELD_STORE_SUBCHAIN, see all the options in the API Reference.
  • sort.direction is SORT_DIRECTION_ASC or SORT_DIRECTION_DESC. Omit sort to order by externalId ascending.

Each returned row looks like this:

{
"locations": [
{
"resource": {
"summary": {
"ids": { "location": "01HHXTNJ3K1QZ8V6X6Y7Z8A9B0", "organization": "*", "project": "*" },
"externalId": "store-123",
"name": "Store 123",
"status": "STATUS_ACTIVE",
"storeChain": "Edeka",
"storeSubchain": "Edeka Center",
"city": "Berlin",
"country": "DE",
"address": { "country": "DE", "city": "Berlin", "street": "Karl-Marx-Straße", "number": "12", "zipCode": "12043" }
},
"source": "LOCATION_SOURCE_LATEST"
},
"versions": [
{ "summary": { /* same shape as `locations.resource.summary` above */ }, "source": "LOCATION_SOURCE_CANONICAL" },
{ "summary": { /* same shape as `locations.resource.summary` above */ }, "source": "LOCATION_SOURCE_LATEST", "organization": "Acme Retail" }
],
"hasConflict": false,
"deviceCount": 3,
"activeDeviceCount": 1
}
],
"totalCount": 1,
"totalPages": 1,
"currentPage": 1
}
  • resource — the row’s display version: your own claim on that location when you have one, otherwise the canonical (tenant-wide consensus) version.
  • versions — every stored version of that physical location — the canonical record plus each partner’s latest copy — canonical first.
  • hasConflict — true when the versions carry more than one distinct content checksum.
  • deviceCount — the amount of devices associated with this location.
  • activeDeviceCount — how many of those devices are active (STATUS_ACTIVE, see Device status). Like deviceCount, it covers every partner’s devices at that externalId, not only your own — so a zero here means no live device anywhere depends on this location.

summary.address carries the location’s full address (country, city, street, number, zipCode, region, state); summary.city / summary.country remain as flattened convenience fields with the same values.

filters.modifiedSince narrows the listing to locations changed strictly after the instant you give, as an RFC 3339 timestamp. It is for incremental sync: record the highest lastUpdatedAt you have seen, then send it back on the next poll instead of walking every location again.

{ "filters": { "modifiedSince": "2026-09-01T00:00:00Z" } }

It compares the canonical record, not the individual versions. A row appears when the agreed values changed; an edit to one partner’s copy that leaves the canonical untouched does not bring the location back. That is what you want when syncing the view this listing returns, since the row is keyed on the physical location rather than on any one partner’s claim.

Two partners can upload the same physical store (same externalId) with diverging content — a different address, store name, or opening hours. Set filters.onlyConflicts: true to list only the locations where that’s happened, then compare each entry’s versions to see exactly which fields diverge and who claims each version.

A location’s operating schedule is expressed with two fields on the location:

  • weeklyHours — recurring weekly rules, one entry per day/window.
  • dateOverrides — date-specific rules that override the weekly schedule (holidays, one-off closures or reduced hours).

Each rule carries an availability — AVAILABILITY_OPEN or AVAILABILITY_CLOSED — for a window given by a start / end time of day. Leaving start / end unset applies the rule to the whole day.

weeklyHours is a list of per-day rules. Several entries for the same day express multiple windows (e.g. a split shift); a day with no open window is closed. For example, open 07:00–22:00 Monday–Saturday and closed Sunday:

POST /v1alpha/inventory/locations:upsert HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
X-Verve-Namespace: project:01ABCDEFGHIJKLMNOPQRSTUVWX
Content-Type: application/json
{
"metadata": { "name": "Store 123" },
"externalId": "store-123",
"supportedLanguages": ["DE"],
"timezone": "Europe/Berlin",
"weeklyHours": [
{ "day": "MONDAY", "availability": "AVAILABILITY_OPEN", "start": { "hours": 7 }, "end": { "hours": 22 } },
{ "day": "SATURDAY", "availability": "AVAILABILITY_OPEN", "start": { "hours": 8 }, "end": { "hours": 20 } },
{ "day": "SUNDAY", "availability": "AVAILABILITY_CLOSED" }
]
}

dateOverrides take precedence over the weekly schedule for a specific date. Set date.year to 0 for a rule that recurs every year (e.g. a fixed holiday), or a concrete year for a one-off. As with weekly hours, an unset start / end covers the whole day, and times are in the location’s timezone.

"dateOverrides": [
{ "date": { "year": 0, "month": 12, "day": 25 }, "availability": "AVAILABILITY_CLOSED" },
{ "date": { "year": 2026, "month": 6, "day": 15 }, "availability": "AVAILABILITY_OPEN", "start": { "hours": 7 }, "end": { "hours": 14 } }
]

The first rule closes the location every 25 December; the second sets reduced hours (07:00–14:00) on 15 June 2026 only.

A location declares the languages its screens render through supportedLanguages — a list of two-letter ISO 639-1 language codes such as "DE" or "EN". Input is case-insensitive and normalized to uppercase, so the stored (and returned) form is uppercase.

POST /v1alpha/inventory/locations:upsert HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
X-Verve-Namespace: project:01ABCDEFGHIJKLMNOPQRSTUVWX
Content-Type: application/json
{
"metadata": { "name": "Store 123" },
"externalId": "store-123",
"supportedLanguages": ["DE", "EN"]
}

Each entry must be a valid two-letter ISO 639-1 code (case-insensitive); anything else — an unknown code or a longer tag such as "en-US" — is rejected as INVALID_ARGUMENT.

A location declares the retailer it belongs to through two fields:

  • storeChain — the retail chain brand the store trades under, e.g. "EDEKA" or "Rewe".
  • storeSubchain — the distribution line / banner within that chain, e.g. "Edeka Center" or "Netto Marken-Discount".

Both are set on a locations:upsert. Each is an object whose name carries the value — set name; the object’s internalId is server-assigned and output-only:

POST /v1alpha/inventory/locations:upsert HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
X-Verve-Namespace: project:01ABCDEFGHIJKLMNOPQRSTUVWX
Content-Type: application/json
{
"metadata": { "name": "Store 123" },
"externalId": "store-123",
"supportedLanguages": ["DE"],
"storeChain": { "name": "Edeka" },
"storeSubchain": { "name": "Edeka Center" }
}

The chain and sub-chain drive targeting and listing: an advertiser can restrict delivery to one or more chains (and optionally specific sub-chains under them) in the audience builder, and locations can be listed sorted by sub-chain (LOCATION_SORT_FIELD_STORE_SUBCHAIN). In listing responses the two come back as plain strings on each row’s summary (storeChain, storeSubchain).

Use these chain and sub-chain names as the reference set. Every sub-chain belongs to exactly one chain — set storeChain to the chain and storeSubchain to the matching sub-chain in the same row:

Chain Sub-chains
Edeka Edeka · Edeka Center · nah & gut · Marktkauf · Netto Marken-Discount · SPAR express · Budni
Rewe REWE · REWE Center · nahkauf · akzenta · Penny · REWE To Go · toom Baumarkt · B1 Discount Baumarkt
Schwarz Gruppe Lidl · Kaufland
Aldi Aldi Süd · Aldi Nord
Metro AG Metro
Salling Group Netto
Bartels-Langness (Bela) famila (Nord-Ost) · Markant (Nord/Ost) · nah & frisch
Bünting Group Combi · famila (Nord-West) · Markant Nordwest
Globus Holding Globus · Globus Baumarkt
Dohle Handelsgruppe HIT
Ceconomy MediaMarkt · Saturn
dm-drogerie markt dm-drogerie markt
Rossmann Rossmann
Müller Müller
Alnatura Alnatura
Denns BioMarkt Denns BioMarkt
tegut tegut
Norma Norma