Bulk CSV
The bulk CSV diff-and-apply workflow is PlaceOptimizer's flagship way to
edit many locations at once. You export every location to a CSV, edit it in a
spreadsheet, import it back, review a field-level diff, and apply only the
changes you select. It lives at Locations → Bulk edit
(/locations/bulk) in the operator console.
The workflow
- Export. Download every managed location as a CSV — one row per location, a fixed 16-column header.
- Edit. Open the CSV in a spreadsheet, change the values you want to
correct. Each column maps to one field on the location; leave a cell empty
to clear that field (or mark a day
closed). - Import and preview. Upload the edited CSV. The engine parses it, validates every row, and computes a field-level diff between the current locations and your proposed values — it never applies anything automatically.
- Review. The preview shows, per location, exactly which fields would change (before → after), plus any rows that could not be matched and any rows with validation errors.
- Apply. Select the locations whose changes you want (or all of them) and apply. Each location is applied independently — one failing location never aborts the rest of the batch.
The diff engine is pure and runs before anything touches your listings. The preview is honest: it reports matched, changed, unchanged, unknown, and erroring rows in one summary.
The 16-column format
The header row is fixed and human-readable for spreadsheet editing. Columns appear in this exact order:
| # | Column | Field | Required | Notes |
|---|---|---|---|---|
| 1 | Store Code | storeCode | Yes | The join key that matches a row to an existing location (case-insensitive). Cannot be empty. |
| 2 | Title | title | Yes | The location name. Cannot be empty. |
| 3 | Phone | phonePrimary | No | Primary phone number. |
| 4 | Address Line | addressLine | No | Full street address (lines joined with , ). |
| 5 | Locality | locality | No | City or locality. |
| 6 | Admin Area | adminArea | No | State or admin area. |
| 7 | Postal Code | postalCode | No | PIN / postal code. |
| 8 | Website | website | No | Website URI. |
| 9 | Primary Category | primaryCategory | No | The location's primary GBP category. |
| 10 | Monday | monday | No | Weekly hours: HH:MM-HH:MM, closed, or empty (see below). |
| 11 | Tuesday | tuesday | No | Same format as Monday. |
| 12 | Wednesday | wednesday | No | Same format as Monday. |
| 13 | Thursday | thursday | No | Same format as Monday. |
| 14 | Friday | friday | No | Same format as Monday. |
| 15 | Saturday | saturday | No | Same format as Monday. |
| 16 | Sunday | sunday | No | Same format as Monday. |
That is 9 profile columns + 7 day columns = 16 columns.
Weekly hours format
Each day column accepts one of:
HH:MM-HH:MM— e.g.09:00-21:00(a single open period; hours 0–24).closed— any case; the location is closed that day.- (empty) — treated as
closedfor a day with no trading hours.
The value is parsed with the same hours helpers the wire mapper uses, so what validates in the CSV is exactly what is applied.
Column → data model mapping
The diff engine maps each CSV column to a dotted path on the managed location model:
| CSV column | Data path |
|---|---|
Title | title |
Phone | phones.primary |
Address Line | address.lines |
Locality | address.locality |
Admin Area | address.adminArea |
Postal Code | address.postalCode |
Website | websiteUri |
Primary Category | categories.primary |
Monday…Sunday | regularHours.monday…regularHours.sunday |
The preview reports changes as these dotted paths with before and after
values, so what you approve is unambiguous.
Matching and validation rules
- Store Code is the join key. A row matches a location when its Store Code matches an existing location's store code (trimmed, case-insensitive). Rows whose Store Code matches nothing are reported as unknown and are never silently skipped.
Store CodeandTitleare required — a row missing either is a validation error.- Column count is enforced. A row with the wrong number of columns is reported with its row number.
- Hours are validated per cell. Anything that is not
HH:MM-HH:MMorclosedis an error on that row. - Parsing is RFC 4180. The exporter writes CRLF rows and quotes fields containing commas, quotes, or newlines; the importer tolerates BOM, CRLF or LF, quoted fields with embedded commas/newlines, and doubled quotes (Excel output shapes).
Errors and unknown rows
The preview never fails silently. It reports, per row number:
- Validation errors — a required field missing, a malformed hours cell, or a wrong column count.
- Unknown rows — valid rows whose Store Code does not match any existing location.
You see all of them before applying anything, and you can choose which matched locations to apply changes to.
Behind the scenes: the preview and apply endpoints
The console calls two REST endpoints (console-internal, session-guarded):
POST /api/v1/gbp/locations/import/previewwith{ "csv": "<csv text>" }returns the parsed preview: per-location diffs, unknown rows, issues, and a summary of rows, matched, changed, unchanged, unknown, and errors.POST /api/v1/gbp/locations/import/applywith{ "changes": [{ "locationRef": "...", "changes": [...] }] }applies the confirmed change sets (up to 1000), isolating per-location failures.
These endpoints back the console UI and are not part of the public supported API surface — use the bulk workflow through the console, or the authenticated MCP tools for programmatic access.
API Reference
The PlaceOptimizer programmatic surface — the MCP server is primary; REST endpoints are console-internal, and the bulk CSV format for location edits.
Outbound Webhooks
Receive PlaceOptimizer events in your own services — endpoint registration, event types, payload shape, HMAC signature verification, retry behavior, and the test endpoint.