Skip to main content
POST
Upsert one physical store

Authorizations

X-API-Key
string
header
required

Path Parameters

retailer_id
string<shortuuid>
required

Unique identifier of the retailer this store belongs to.

Required string length: 22
Pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$

Body

application/json

A single store plus the same reconciliation knobs the batch form takes.

store_number
string
required

The key this source uses for the store — the retailer's own store number where it publishes one, otherwise a stable synthesized key (e.g. SYN-…). This is the highest-confidence match tier, so a source that can supply a real store number should: it is what makes a repeated call idempotent rather than proximity-matched.

Maximum string length: 50
source
string
required

Stable name of the dataset this row came from (e.g. ulta_site, sephora_csv, osm). Load-bearing, not a label: per-column authorship is recorded against it, and a source may later correct a value it wrote itself but never one another source wrote. Sending a different name on a rerun of the same dataset therefore forfeits that source's own right to update the columns it authored.

Required string length: 1 - 100
name
string
default:""

Store or branch display name.

Maximum string length: 255
address_line_1
string
default:""

Street address, number and street name.

Maximum string length: 255
city
string
default:""

City, town or locality.

Maximum string length: 255
region
string
default:""

State, province or region.

Maximum string length: 255
postal_code
string
default:""

ZIP, postal code or postcode.

Maximum string length: 20
country
string<iso3166-alpha2>
default:US

ISO 3166-1 alpha-2 country code for the store. An assigned code: this value is written to the place's country column, and a store filed under a code that names no country is the same class of defect C-5969 found 75 instances of. A bare two-letter string would have admitted 427 unassigned combinations (C-5975).

Required string length: 2
Pattern: ^[A-Za-z]{2}$
lat
number | null

WGS84 latitude in decimal degrees. Null is accepted — a store with a good address and no coordinates still matches on the address tier and is still worth having.

Required range: -90 <= x <= 90
lon
number | null

WGS84 longitude in decimal degrees. Null is accepted; see lat.

Required range: -180 <= x <= 180
phone
string
default:""

Store phone number; normalised to E.164 on write.

Maximum string length: 50
website
string
default:""

The store's own page on the retailer's site.

Maximum string length: 1000
opening_hours
string
default:""

Opening hours in OSM opening_hours syntax (e.g. Mo-Sa 10:00-20:00; Su 11:00-18:00). There is no hours column on a location, so this is stored in Place.source_extras under this row's source — sending it here is equivalent to sending extras.opening_hours, and an explicit extras.opening_hours wins. A structured schema.org openingHoursSpecification can be sent alongside it in extras.

Maximum string length: 1000
source_url
string
default:""

Where this specific row was read from.

Maximum string length: 1000
first_party
boolean
default:false

True when the row comes from the retailer's own site or feed. Only a first-party row may overwrite an existing non-placeholder value or rename a storefront; community mapping data enriches blanks instead.

allow_create
boolean
default:true

Whether a row that matches nothing may create a new store. Set false for a secondary source: it may enrich a store the retailer confirms exists, but a location only a community extract believes in is not evidence enough to seed a storefront customers would be sent to.

country_derived
boolean
default:false

True when country came from a coordinate lookup rather than from a feed label or address field. This is a provenance assertion, and it grants the row power: only a derived country may correct a country already stored on the place. A country that came from a label can fill a blank and nothing more.

Earn it by calling GET /locations/api/v1/countries/resolve?lat=&lon= with the row's own coordinates and sending the returned country as country: that is the same rule the server applies, so you need no polygon dataset of your own. Set it only when the answer is not flagged and method is not none. The reason it exists is concrete — Sephora's North America feed labels 482 of its 483 rows US, and trusting that label would have filed 75 Canadian stores as American (C-5969). A label is not evidence; a coordinate is. Asserting this on a label-derived country re-opens exactly that defect, so leave it false when in doubt: the cost is a country that stays wrong until a better row arrives, not one that gets overwritten with a worse value.

extras
Extras · object

Everything this source carries that has no column of its own — raw hours specifications, service lists, OSM tags, source-conflict flags. Stored in Place.source_extras under this row's source. Nesting is allowed here, unlike the flat-string metadata field on other resources. A secondary source's payload does not belong here — send it in merged_extras under that source's name.

merged_extras
Merged Extras · object

Provenance from other sources whose data was folded into this row before upload, keyed by each source's stable name — the same kind of name source takes. Use it when your importer merged a secondary dataset (e.g. an OSM extract) into a primary one: put the primary's payload in extras and each secondary's payload here, e.g. {"osm": {"licence": "ODbL", "tags": {...}}}. Each key is stored in Place.source_extras as its own sibling of source, so the sources stay separable and a later run of either one replaces only its own entry. Do not invent a convention such as an extras["merged:osm"] key: that buries the secondary under the primary's namespace where no other reader will look for it. A key equal to this row's source is rejected, because it would overwrite the row's own extras. Only the payload is recorded — per-column authorship still goes to source alone.

dry_run
boolean
default:true

When true (the default) the write is rolled back. Send false to commit.

proximity_m
number
default:100

Match radius in metres for the proximity tier.

Required range: x <= 500
phone_region
string<iso3166-alpha2>
default:US

ISO 3166-1 alpha-2 region for phone normalisation.

Required string length: 2
Pattern: ^[A-Za-z]{2}$

Response

OK

Batch census plus the per-row results.

created + enriched + unchanged + not_created + errors equals the number of submitted rows for any 200 response. The endpoint returns 200 even when individual rows fail: a per-row failure is reported here rather than aborting the batch, so a single malformed row cannot cost the other 999. Request-level failures (auth, an unwritable retailer, a batch over the row cap) are 4xx instead.

committed
boolean
required

False when this was a dry run — every path ran and the transaction was rolled back.

submitted_rows
integer
required

Number of rows in the request.

Required range: x >= 0
existing_stores
integer
required

Stores this retailer had when this call started, including any in already_claimed_store_ids. Measured per call: on a committed split run it grows by the stores earlier batches created.

Required range: x >= 0
created
integer
required

Rows that produced a new store.

Required range: x >= 0
enriched
integer
required

Rows that matched and changed something.

Required range: x >= 0
unchanged
integer
required

Rows that matched and needed no change.

Required range: x >= 0
not_created
integer
required

Rows that matched nothing and were not allowed to create a store.

Required range: x >= 0
errors
integer
required

Rows that raised. Equals the length of row_errors.

Required range: x >= 0
matched_store_number
integer
required

Rows matched on the exact store-number tier.

Required range: x >= 0
matched_proximity
integer
required

Rows matched on the proximity tier.

Required range: x >= 0
matched_address
integer
required

Rows matched on the normalised-address tier.

Required range: x >= 0
near_misses
integer
required

Pairs beyond proximity_m but within 500 m that were deliberately NOT matched. A judgement call for a human, never auto-matched by widening the radius. Per call: a split run's sum can include a pair whose store a later batch went on to claim.

Required range: x >= 0
unmatched_existing
integer
required

Existing stores no submitted row claimed and not in already_claimed_store_ids. For a full-dataset run these are stores the retailer's own list no longer carries, i.e. probably closed — reported, never deactivated or deleted. Per call, measured against the whole retailer: for a split run, read it from the last call, sent with every other batch's store_ids; summing per-call values double-counts.

Required range: x >= 0
already_claimed_stores
integer
required

How many already_claimed_store_ids were this retailer's stores and so were held out of matching. Lower than the number sent means some ids were ignored — for example created ids from a dry run, whose stores never persisted.

Required range: x >= 0
phones_written
integer
required

Phone numbers written or refreshed.

Required range: x >= 0
urls_written
integer
required

Store URLs written or refreshed.

Required range: x >= 0
fields_superseded
integer
required

Existing non-placeholder values a first-party row overwrote. The previous value is preserved under Place.source_extras['superseded'] rather than discarded.

Required range: x >= 0
legacy_keys_preserved
integer
required

Stores whose pre-existing store number was replaced by this source's real one, with the old key kept in Place.source_extras['legacy_store_key'].

Required range: x >= 0
rekey_skipped_conflict
integer
required

Stores left on their existing key because the evidence for re-keying was contradictory.

Required range: x >= 0
unmatched_existing_store_ids
string<shortuuid>[]

The ids of the stores counted in unmatched_existing, so a human can review exactly which stores the dataset no longer carries.

URL-safe 22-character shortuuid encoding of the row's UUID primary key. Stable across the row's lifetime; suitable for sharing in URLs, log lines, and external SDK clients. Accepted on input as either the shortuuid form or the canonical UUID form (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Required string length: 22
Pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$
coverage_after
Coverage After · object

Per-column coverage for this retailer's stores after the call — stores, with_point, with_real_address, with_phone, with_url, with_source_extras. On a dry run these are the post-rollback figures the run would have produced, measured inside the transaction.

rows
StoreRowResultOut · object[]

One result per submitted row, in request order.

row_errors
string[]

Batch-level restatement of every row that raised, for callers that read only the census.