Target a price list at one customer group
Authorizations
A secret API key. Publishable keys cannot reach this API. A key may carry an expiry, and an expired key is refused exactly like an unknown one, with a 401 that names no reason; check the key's expires_at in the dashboard rather than inferring it from a response. When a merchant rolls a key's secret they choose a grace window of up to 3 days, and for its duration BOTH the new secret and the one it replaced authenticate, so an integration moves over on its own deploy schedule instead of at the instant the button is pressed. Move before the window closes: after it, the old secret is refused. Nothing else about this contract moves with a roll. The key keeps its id and its scopes, so the only thing an integration updates is the credential itself.
Headers
A unique key per logical write. Replaying a request with the same key returns the first response byte for byte instead of applying the write twice.
Path Parameters
Body
WHO the list's prices apply to, which decides what those shoppers are CHARGED without changing a single amount. ONE GROUP PER CALL, never an array, and customer_group_ids is refused BY NAME for that reason: an array on this resource could only be a REPLACEMENT SET, and its destructive value is the EMPTY one, because a list with no targeting rows applies to EVERY customer. A sync client serialising an unset field as an empty array would turn a wholesale tier's prices public and read the 200 as success. A single id per call cannot express that by accident. ADDING THE FIRST GROUP NARROWS THE LIST. An untargeted list is store-wide, so this call takes the list's prices away from every shopper outside the group it names. The DELETE beside it is the mirror of that trap rather than its undo: removing the LAST targeting row does not untarget a list, it applies the list to every customer, so the removal that looks like a narrowing is the widest write in this family. That is why it carries write_pricing:delete on top of write_pricing while this route takes write_pricing alone. 200 rather than 201, because the insert is ON CONFLICT DO NOTHING: a repeat creates nothing and returns the original row with its original created_at, so replaying converges by construction. There is no Location to point at either, since this resource has no id of its own and its primary key is (price_list_id, customer_group_id), both halves of which the caller just sent. The parent list is verified FIRST, inside the same transaction, so a nonexistent list is a 404 rather than a foreign-key violation answered 500. A group belonging to another store gets the same 404 as one that never existed, so this route is not an existence oracle over another merchant's segment ids. Nothing is emitted: this family registers no webhook topics, so a client that needs to know a list was retargeted must poll. id, price_list_id and created_at are refused BY NAME rather than ignored: this resource has no id of its own, the list is in the path, and the server owns the timestamp. group_id is refused too, because the field is customer_group_id. Any other key is a 400 naming the field, so a misspelling is never a silent no-op answered 200.
Required, and refused rather than defaulted: an absent id would reach the service as the empty string, and the caller would be told a group they never sent was not found. It may not be null, because the column is NOT NULL and this route has no clear. It names a customer group in THIS store, opaque here and resolved with GET /api/v1/customer-groups/{customerGroupId}, which is read_customers rather than read_pricing, so a key that syncs prices cannot read the merchant's segment names as a side effect.
Response
Success
