---
title: "Stamp every unassigned record carrying a signal with a site (idempotent)"
method: POST
path: "/sites/assign"
tags: ["Sites"]
---

# Stamp every unassigned record carrying a signal with a site (idempotent)

`POST /sites/assign`

Stamps every live source record of the organization that carries the signal (the value is
normalized like a matcher value) and has neither a site nor an entity, and its emission rows,
with the site, in batches, each audited with reason assign. A record is skipped (and counted
in skippedConflictingCount) only when its best matched site (that of its highest-ranked signal
with an active site's matcher) comes from a signal kind ranked above the assigned kind and is
not the target site; a site matched only by an equal or lower-ranked signal does not block it.
Kinds rank supply_point > meter_number > account_number > location_text.
Each batch commits on its own: if the call fails part-way, records already stamped stay
stamped, and re-running the same call stamps the rest (a full re-run stamps nothing new).
Each batch's statements (and the final skipped count) run under the server's statement
timeout; a batch that exceeds it rolls back alone and the call answers 503 ASSIGN_TIMEOUT.
The batches before it stay stamped, so re-running the same call is safe and continues.
A bulk back-fill is a change an admin confirms, so the call always needs access.manage
(checked before the body is validated); a single record's correction is
POST /emissions/site-assignment, which needs org.write_data. createMatcher also creates the
matcher (or reuses the one this value already has for the same site).
Error codes: 400 VALIDATION_ERROR; 403 FORBIDDEN (no access.manage); 409 MATCHER_EXISTS (the
value maps to another site); 422 SITE_NOT_ASSIGNABLE (unknown, archived or another
organization's site); 503 ASSIGN_TIMEOUT (re-run the same call to continue).

## Request body

- SiteAssignRequest
  - `createMatcher` boolean — Also create the matcher (needs access.manage)
  - `kind` 'supply_point' | 'meter_number' | 'account_number' | 'location_text', required
  - `siteId` string, required
  - `value` string, required

## Response `200`

Records assigned

- SiteAssignResponse
  - `data` object, required
    - `assignedCount` integer, required
    - `matcherId` string, nullable, required
    - `skippedConflictingCount` integer, required

## Other responses

- `400` — Bad Request
- `401` — Unauthorized
- `403` — Forbidden
- `409` — MATCHER_EXISTS: createMatcher asked for a value that already maps to another site
- `422` — A referenced id is not an active record of the selected organization (ENTITY_NOT_ASSIGNABLE, SITE_NOT_ASSIGNABLE), or the reference would be invalid for it, such as an entity parent that forms a cycle (ENTITY_CYCLE)
- `503` — ASSIGN_TIMEOUT: a batch took too long; batches already stamped stay stamped, so re-run the same call to continue

## Changes

- **2026-10-02** `766c2a40e369` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/greentally/apis/esgai-api/changes/sites/assign/post.md)

---

[API](https://skmtc.dev/greentally/apis/esgai-api.md) · [All operations](https://skmtc.dev/greentally/apis/esgai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/greentally/apis/esgai-api/revisions/4189686230ca?raw)
