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 and at most 2.5 MiB (2,621,440 bytes) of request body — the server's upload ceiling, answered with 413 when exceeded. The byte limit usually binds first: 1,000 first-party rows carrying seven-day opening hours in extras serialise to about 2.7 MB, so size batches by bytes, not rows. 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. To reconcile one dataset split across calls as if it were one pass, send already_claimed_store_ids.

Required array length: 1 - 1000 elements
already_claimed_store_ids
string<shortuuid>[]

For a dataset split across several calls: the store_id of every row result from the other batches of the same run (at most 50,000). No row in this call can match one of these stores, so matching stays one-to-one across the whole dataset rather than per call, and they are left out of unmatched_existing — which makes the last call's unmatched_existing and unmatched_existing_store_ids the dataset-wide figures. Leave out this batch's own earlier results when retrying it, or its rows cannot re-match their stores. Ids that are not this retailer's stores are ignored; already_claimed_stores reports how many were used.

Maximum array length: 50000

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