# Push Locations

> Upsert locations (opening_hours → hours_source = platform).

Source: https://shopassist.coec.dk/docs/api/reference/catalog/push-locations · Updated 2026-11-03

`POST /v1/stores/{store_id}/catalog/locations:batch`

- Scope: `catalog:write`
- Rate limit: 600 requests/min per API key (test keys: 60/min); catalog batches 120/min per store

Upsert locations (`opening_hours` → `hours_source = platform`).

## Parameters [#parameters]

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `store_id` | path | `string` | yes |  |
| `Idempotency-Key` | header | `string` | yes | Replays return the original |

## Request body (LocationsBatch) [#request-body]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `locations` | `CatalogLocationInput[]` | yes | CatalogLocationInput items (validated individually; invalid items are reported in rejected[]) · ≥ 1 items · ≤ 500 items |
| `locations[].address` | `CatalogAddressInput` | yes | `Address` (03 §2.4) as pushed by the merchant. |
| `locations[].address.city` | `string \| null` | yes |  |
| `locations[].address.country_code` | `string \| null` | yes | pattern `^[A-Z]{2}$` |
| `locations[].address.formatted` | `string` | yes |  |
| `locations[].address.line1` | `string \| null` | yes |  |
| `locations[].address.line2` | `string \| null` | yes |  |
| `locations[].address.postal_code` | `string \| null` | yes |  |
| `locations[].address.region` | `string \| null` | yes |  |
| `locations[].email` | `string` | no | ≤ 254 characters |
| `locations[].external_id` | `string` | yes | 1–128 characters |
| `locations[].has_pickup` | `boolean` | no |  |
| `locations[].kind` | `"warehouse" \| "retail_store" \| "pickup_point" \| "virtual"` | yes |  |
| `locations[].latitude` | `number` | no | ≥ -90 · ≤ 90 |
| `locations[].longitude` | `number` | no | ≥ -180 · ≤ 180 |
| `locations[].name` | `string` | yes | 1–255 characters |
| `locations[].opening_hours` | `OpeningHoursInput` | no | `locations.opening_hours` (02 §16.10): `{ weekly; exceptions }`. |
| `locations[].opening_hours.exceptions` | `OpeningHoursException[]` | no |  |
| `locations[].opening_hours.exceptions[].date` | `string` | yes | pattern `^\d{4}-\d{2}-\d{2}$` |
| `locations[].opening_hours.exceptions[].intervals` | `unknown[][]` | no |  |
| `locations[].opening_hours.exceptions[].is_closed` | `boolean` | no |  |
| `locations[].opening_hours.exceptions[].note` | `string` | no | ≤ 500 characters |
| `locations[].opening_hours.weekly` | `OpeningHoursWeekday[]` | yes |  |
| `locations[].opening_hours.weekly[].day` | `string (enum)` | yes | one of 1, 2, 3, 4, 5, 6, 7 |
| `locations[].opening_hours.weekly[].intervals` | `unknown[][]` | yes |  |
| `locations[].phone` | `string` | no | ≤ 64 characters |
| `locations[].timezone` | `string` | no | ≤ 64 characters |

## Example request [#example-request]

```bash
curl -X POST "https://api.shopassist.coec.dk/v1/stores/sto_01j8000000e008000000000000/catalog/locations:batch" \
  -H "Authorization: Bearer $SHOPASSIST_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"locations":[{"address":{"city":"string","country_code":"string","formatted":"string","line1":"string","line2":"string","postal_code":"string","region":"string"},"email":"string","external_id":"string","has_pickup":true,"kind":"warehouse","latitude":-90,"longitude":-180,"name":"string","opening_hours":{"exceptions":[{"date":"string","intervals":[null],"is_closed":true,"note":"string"}],"weekly":[{"day":1,"intervals":[null]}]},"phone":"string","timezone":"string"}]}'
```

## Response 202 (BatchAccepted) [#response]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `accepted` | `integer` | yes | ≥ 0 |
| `rejected` | `ItemRejection[]` | yes |  |
| `rejected[].error` | `ItemError` | yes | `{ code; message }` of a rejected item. |
| `rejected[].error.code` | `"validation_error" \| "currency_mismatch" \| "duplicate_external_id" \| "too_many_variants" \| "url_not_allowed"` | yes |  |
| `rejected[].error.message` | `string` | yes |  |
| `rejected[].index` | `integer` | yes | ≥ 0 |
| `sync_run_id` | `string` | yes | pattern `^sync_[0-9a-hjkmnp-tv-z]{26}$` |

```json
{
  "accepted": 0,
  "rejected": [
    {
      "error": {
        "code": "validation_error",
        "message": "string"
      },
      "index": 0
    }
  ],
  "sync_run_id": "sync_01j8000000e008000000000000"
}
```

## Errors [#errors]

| Status | Meaning |
| --- | --- |
| 400 | Bad Request ([error codes](/docs/api/errors)) |
| 401 | Unauthorized ([error codes](/docs/api/errors)) |
| 403 | Forbidden ([error codes](/docs/api/errors)) |
| 404 | Not Found ([error codes](/docs/api/errors)) |
| 409 | Conflict ([error codes](/docs/api/errors)) |
| 413 | Content Too Large ([error codes](/docs/api/errors)) |
| 422 | Unprocessable Content ([error codes](/docs/api/errors)) |
| 429 | Too Many Requests ([error codes](/docs/api/errors)) |
