Skip to main content
POST
Upsert physical stores in bulk

Authorizations

X-API-Key
string
header
required

Path Parameters

retailer_id
string<shortuuid>
required

Unique identifier of the retailer these stores belong to.

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

Body

application/json

A batch of store rows plus the reconciliation knobs for the call.

rows
StoreUpsertRowIn · object[]
required

Store rows to reconcile, at most 1,000 per call. Results come back in request order, each carrying its index. Matching is one-to-one within a call, so a store claimed by an earlier call is re-matched on the store-number tier in the next one — which is what makes repeating a batch idempotent rather than duplicative.

Required array length: 1 - 1000 elements
dry_run
boolean
default:true

When true (the default) every path runs against real rows and the transaction is rolled back, so the results are the matcher's real verdict and nothing is written. Send false to commit.

proximity_m
number
default:100

Match radius in metres for the proximity tier (default 100). Capped at 500 m: pairs beyond the match radius but within it are reported as near_misses for a human to judge, and widening the radius to force a match is exactly what the tiered matcher exists to avoid.

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

ISO 3166-1 alpha-2 region used to parse national phone numbers into E.164. An unassigned code parses nothing, so every phone in the batch would be counted invalid rather than written — silently, since a bad parse is a counter and not an error.

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.