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.

Set it only when you actually resolved the country from lat/lon. 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.

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 already had, i.e. the size of the candidate match set.

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.

Required range: x >= 0
unmatched_existing
integer
required

Existing stores no submitted row claimed. 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.

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
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.