> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.solvimon.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.solvimon.com/_mcp/server.

# Seat assignments

How to record which child customer occupies which seat under a parent, so seat usage is an explicit record rather than an inference from the customer hierarchy.

---

## Why this matters

`seats_values` on a pricing plan schedule records how many seats a parent has paid for. Nothing in that number says *which* child customer occupies one.

Without that record, a processing-only child is linked to its parent only through `parent_resource_ids`, which leaves three questions unanswerable: you cannot exclude a child from counting as a seat, you cannot tell one seat type from another when a parent buys several, and you cannot compare occupied seats against paid seats. Any answer has to be reconstructed outside Solvimon and re-reconstructed whenever the hierarchy changes.

A seat assignment makes occupancy a resource in its own right: who occupies which seat type, under which parent, for which period.

## How it works

An assignment links three things and a period:

```mermaid
erDiagram
    PARENT-CUSTOMER ||--o{ SEAT-ASSIGNMENT : "holds seats"
    CHILD-CUSTOMER ||--o{ SEAT-ASSIGNMENT : occupies
    PRODUCT-ITEM ||--o{ SEAT-ASSIGNMENT : "defines the seat type"
```

Occupancy is keyed on the **product item**, not on the subscription or the schedule. Capacity is already scoped that way: `seats_values` records a seat count per pricing item, so a parent that buys two kinds of seat has two counts. Keying occupancy the same way is what lets one child hold an editor seat while another holds a viewer seat under the same parent, and it means the seat type is read from your catalogue rather than from how the seat happens to be priced.

An assignment changes no billing behaviour on its own. It is the record that [seat-scaled credit grants](/platform-guides/wallets-credits/seat-based-credits) and occupied-versus-paid reporting are built on.

### Status is derived, not set

`status` is read-only and computed from the dates every time the assignment is read:

| Status      | When                                                                  |
| ----------- | --------------------------------------------------------------------- |
| `SCHEDULED` | Now is before `start_at`                                              |
| `ACTIVE`    | Now is at or after `start_at`, and `end_at` is unset or in the future |
| `ENDED`     | Now is at or after `end_at`                                           |

There is no status to patch and no activation call. An assignment that starts next month is created today and becomes `ACTIVE` on its own.

## Assign a seat

`start_at` is required; leaving `end_at` unset creates an open assignment, which is the normal case for a seat someone still holds.

### Request

POST [https://test.api.solvimon.com/v\{version}/seat-assignments](https://test.api.solvimon.com/v\{version}/seat-assignments)

**`SeatAssignments_postSeatAssignments_example`**

```curl SeatAssignments_postSeatAssignments_example
curl -X POST https://test.api.solvimon.com/v1/seat-assignments \
     -H "X-API-KEY: <apiKey>" \
     -H "Content-Type: application/json" \
     -d '{
  "parent_customer_id": "cust_UwDeeN0vxxHxMTCJeF1K",
  "customer_id": "cust_FwDeeN0vxxHxN0COeY1H",
  "product_item_id": "proi_pwDeeN0vxxHxUUCxh81e",
  "start_at": "2026-10-01T00:00:00-05:00"
}'
```

**`SeatAssignments_postSeatAssignments_example`**

```python SeatAssignments_postSeatAssignments_example
import requests

url = "https://test.api.solvimon.com/v1/seat-assignments"

payload = {
    "parent_customer_id": "cust_UwDeeN0vxxHxMTCJeF1K",
    "customer_id": "cust_FwDeeN0vxxHxN0COeY1H",
    "product_item_id": "proi_pwDeeN0vxxHxUUCxh81e",
    "start_at": "2026-10-01T00:00:00-05:00"
}
headers = {
    "X-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

**`SeatAssignments_postSeatAssignments_example`**

```javascript SeatAssignments_postSeatAssignments_example
const url = 'https://test.api.solvimon.com/v1/seat-assignments';
const options = {
  method: 'POST',
  headers: {'X-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"parent_customer_id":"cust_UwDeeN0vxxHxMTCJeF1K","customer_id":"cust_FwDeeN0vxxHxN0COeY1H","product_item_id":"proi_pwDeeN0vxxHxUUCxh81e","start_at":"2026-10-01T00:00:00-05:00"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`SeatAssignments_postSeatAssignments_example`**

```go SeatAssignments_postSeatAssignments_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/seat-assignments"

	payload := strings.NewReader("{\n  \"parent_customer_id\": \"cust_UwDeeN0vxxHxMTCJeF1K\",\n  \"customer_id\": \"cust_FwDeeN0vxxHxN0COeY1H\",\n  \"product_item_id\": \"proi_pwDeeN0vxxHxUUCxh81e\",\n  \"start_at\": \"2026-10-01T00:00:00-05:00\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

**`SeatAssignments_postSeatAssignments_example`**

```ruby SeatAssignments_postSeatAssignments_example
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/seat-assignments")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"parent_customer_id\": \"cust_UwDeeN0vxxHxMTCJeF1K\",\n  \"customer_id\": \"cust_FwDeeN0vxxHxN0COeY1H\",\n  \"product_item_id\": \"proi_pwDeeN0vxxHxUUCxh81e\",\n  \"start_at\": \"2026-10-01T00:00:00-05:00\"\n}"

response = http.request(request)
puts response.read_body
```

**`SeatAssignments_postSeatAssignments_example`**

```java SeatAssignments_postSeatAssignments_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://test.api.solvimon.com/v1/seat-assignments")
  .header("X-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"parent_customer_id\": \"cust_UwDeeN0vxxHxMTCJeF1K\",\n  \"customer_id\": \"cust_FwDeeN0vxxHxN0COeY1H\",\n  \"product_item_id\": \"proi_pwDeeN0vxxHxUUCxh81e\",\n  \"start_at\": \"2026-10-01T00:00:00-05:00\"\n}")
  .asString();
```

**`SeatAssignments_postSeatAssignments_example`**

```php SeatAssignments_postSeatAssignments_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://test.api.solvimon.com/v1/seat-assignments', [
  'body' => '{
  "parent_customer_id": "cust_UwDeeN0vxxHxMTCJeF1K",
  "customer_id": "cust_FwDeeN0vxxHxN0COeY1H",
  "product_item_id": "proi_pwDeeN0vxxHxUUCxh81e",
  "start_at": "2026-10-01T00:00:00-05:00"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

**`SeatAssignments_postSeatAssignments_example`**

```csharp SeatAssignments_postSeatAssignments_example
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/seat-assignments");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"parent_customer_id\": \"cust_UwDeeN0vxxHxMTCJeF1K\",\n  \"customer_id\": \"cust_FwDeeN0vxxHxN0COeY1H\",\n  \"product_item_id\": \"proi_pwDeeN0vxxHxUUCxh81e\",\n  \"start_at\": \"2026-10-01T00:00:00-05:00\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`SeatAssignments_postSeatAssignments_example`**

```swift SeatAssignments_postSeatAssignments_example
import Foundation

let headers = [
  "X-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "parent_customer_id": "cust_UwDeeN0vxxHxMTCJeF1K",
  "customer_id": "cust_FwDeeN0vxxHxN0COeY1H",
  "product_item_id": "proi_pwDeeN0vxxHxUUCxh81e",
  "start_at": "2026-10-01T00:00:00-05:00"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/seat-assignments")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Response (201)

```json
{
  "object_type": "string",
  "id": "string",
  "status": "SCHEDULED",
  "parent_customer_id": "string",
  "customer_id": "string",
  "product_item_id": "string",
  "start_at": "2024-01-15T09:30:00Z",
  "end_at": "2024-01-15T09:30:00Z",
  "created_at": "string",
  "updated_at": "string"
}
```

Three things are checked before the assignment is written:

| Field                | Requirement                                                                           |
| -------------------- | ------------------------------------------------------------------------------------- |
| `parent_customer_id` | An `ACTIVE` customer on your platform                                                 |
| `customer_id`        | An `ACTIVE` customer, a child of that parent, and carrying the `PROCESSING_ONLY` role |
| `product_item_id`    | A product item with model type `PER_SEAT`, in status `ACTIVE` or `DEPRECATED`         |

The `PROCESSING_ONLY` requirement is what makes exclusion possible: a child that bills in its own right is not a seat, and pointing an assignment at one is rejected on `customer_id`. A `DRAFT` product item is rejected because its model type can still change, so `PER_SEAT` is only settled once the item is published.

## End a seat

Ending an assignment is a `PATCH` that sets `end_at`. It must be strictly after `start_at`: a seat occupied for zero time is not an occupancy.

### Request

PATCH [https://test.api.solvimon.com/v\{version}/seat-assignments/\{resourceId}](https://test.api.solvimon.com/v\{version}/seat-assignments/\{resourceId})

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```curl SeatAssignments_patchSeatAssignmentsByResourceId_example
curl -X PATCH https://test.api.solvimon.com/v1/seat-assignments/ \
     -H "X-API-KEY: <apiKey>" \
     -H "Content-Type: application/json" \
     -d '{
  "end_at": "2027-01-01T00:00:00-06:00"
}'
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```python SeatAssignments_patchSeatAssignmentsByResourceId_example
import requests

url = "https://test.api.solvimon.com/v1/seat-assignments/:resourceId"

payload = { "end_at": "2027-01-01T00:00:00-06:00" }
headers = {
    "X-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.patch(url, json=payload, headers=headers)

print(response.json())
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```javascript SeatAssignments_patchSeatAssignmentsByResourceId_example
const url = 'https://test.api.solvimon.com/v1/seat-assignments/:resourceId';
const options = {
  method: 'PATCH',
  headers: {'X-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"end_at":"2027-01-01T00:00:00-06:00"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```go SeatAssignments_patchSeatAssignmentsByResourceId_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/seat-assignments/:resourceId"

	payload := strings.NewReader("{\n  \"end_at\": \"2027-01-01T00:00:00-06:00\"\n}")

	req, _ := http.NewRequest("PATCH", url, payload)

	req.Header.Add("X-API-KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```ruby SeatAssignments_patchSeatAssignmentsByResourceId_example
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/seat-assignments/:resourceId")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Patch.new(url)
request["X-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"end_at\": \"2027-01-01T00:00:00-06:00\"\n}"

response = http.request(request)
puts response.read_body
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```java SeatAssignments_patchSeatAssignmentsByResourceId_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.patch("https://test.api.solvimon.com/v1/seat-assignments/:resourceId")
  .header("X-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"end_at\": \"2027-01-01T00:00:00-06:00\"\n}")
  .asString();
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```php SeatAssignments_patchSeatAssignmentsByResourceId_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('PATCH', 'https://test.api.solvimon.com/v1/seat-assignments/:resourceId', [
  'body' => '{
  "end_at": "2027-01-01T00:00:00-06:00"
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```csharp SeatAssignments_patchSeatAssignmentsByResourceId_example
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/seat-assignments/:resourceId");
var request = new RestRequest(Method.PATCH);
request.AddHeader("X-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"end_at\": \"2027-01-01T00:00:00-06:00\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`SeatAssignments_patchSeatAssignmentsByResourceId_example`**

```swift SeatAssignments_patchSeatAssignmentsByResourceId_example
import Foundation

let headers = [
  "X-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = ["end_at": "2027-01-01T00:00:00-06:00"] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/seat-assignments/:resourceId")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "PATCH"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Response (200)

```json
{
  "object_type": "string",
  "id": "string",
  "status": "SCHEDULED",
  "parent_customer_id": "string",
  "customer_id": "string",
  "product_item_id": "string",
  "start_at": "2024-01-15T09:30:00Z",
  "end_at": "2024-01-15T09:30:00Z",
  "created_at": "string",
  "updated_at": "string"
}
```

`end_at` is the only patchable field. Sending `customer_id`, `parent_customer_id`, `product_item_id` or `start_at` is rejected with `field can not be updated`, because moving a seat to another person, parent, seat type or start date is a different occupancy and belongs in a new assignment. Setting `end_at` back to `null` re-opens the seat, which is how you undo an end date entered by mistake.

`DELETE /v1/seat-assignments/{id}` is reserved for undoing a mis-created assignment, and only works while the assignment is still `SCHEDULED`. Once the seat has actually been occupied, the occupancy is history that later invoices were produced against, so deleting it is rejected with `seat assignment ... is ACTIVE and can no longer be deleted; set end_at to free the seat instead`.

## List who holds a seat

The collection endpoint answers the occupancy questions directly, and combines filters:

| Query parameter      | Answers                                      |
| -------------------- | -------------------------------------------- |
| `parent_customer_id` | Who occupies seats under this parent         |
| `customer_id`        | Which seats this child holds                 |
| `product_item_id`    | Who holds this seat type                     |
| `status`             | Restrict to `SCHEDULED`, `ACTIVE` or `ENDED` |

### Request

GET [https://test.api.solvimon.com/v\{version}/seat-assignments](https://test.api.solvimon.com/v\{version}/seat-assignments)

```curl
curl https://test.api.solvimon.com/v1/seat-assignments \
     -H "X-API-KEY: <apiKey>" \
     -H "Content-Type: application/json"
```

```python
import requests

url = "https://test.api.solvimon.com/v1/seat-assignments"

payload = {}
headers = {
    "X-API-KEY": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.get(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://test.api.solvimon.com/v1/seat-assignments';
const options = {
  method: 'GET',
  headers: {'X-API-KEY': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://test.api.solvimon.com/v1/seat-assignments"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("GET", url, payload)

	req.Header.Add("X-API-KEY", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://test.api.solvimon.com/v1/seat-assignments")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://test.api.solvimon.com/v1/seat-assignments")
  .header("X-API-KEY", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://test.api.solvimon.com/v1/seat-assignments', [
  'body' => '{}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://test.api.solvimon.com/v1/seat-assignments");
var request = new RestRequest(Method.GET);
request.AddHeader("X-API-KEY", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-KEY": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://test.api.solvimon.com/v1/seat-assignments")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Response (200)

```json
{
  "data": [
    {
      "object_type": "seat_assignment",
      "id": "sa_9f3e2b1a-4c7d-4e8f-a1b2-3c4d5e6f7a8b",
      "status": "ACTIVE",
      "parent_customer_id": "cust_parent_7a2b3c4d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
      "customer_id": "cust_child_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "product_item_id": "pi_3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
      "start_at": "2024-01-01T00:00:00Z",
      "end_at": "2024-12-31T23:59:59Z",
      "created_at": "2023-12-15T10:22:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ],
  "page": 1,
  "limit": 25,
  "links": {
    "first": "https://api.solvimon.com/v1/seat-assignments?page=1&limit=25",
    "previous": null,
    "current": "https://api.solvimon.com/v1/seat-assignments?page=1&limit=25",
    "next": "https://api.solvimon.com/v1/seat-assignments?page=2&limit=25"
  },
  "total_number_of_pages": 4
}
```

The response is paginated with the usual `limit`, `page`, `order_by` and `order_direction` parameters.

## Via Desk

On a per-seat pricing item on a subscription, open the context menu next to **Show seats history** to reach seat assignments. The panel lists the seats assigned under that parent for that seat type, with occupant, window and status, and lets you assign a child to a seat, end an assignment, reopen it, or delete it.

Only processing-only children of that parent are offered as occupants, and each action in the panel is gated by the same `SEAT_ASSIGNMENT` permissions as the API.

## Permissions

Four permissions govern the resource, and they are separate from the customer permissions so seat administration can be delegated without granting the ability to change customers:

| Permission               | Allows                                       |
| ------------------------ | -------------------------------------------- |
| `SEAT_ASSIGNMENT.VIEW`   | Read assignments, individually and as a list |
| `SEAT_ASSIGNMENT.CREATE` | Assign a seat                                |
| `SEAT_ASSIGNMENT.UPDATE` | Set or clear `end_at`                        |
| `SEAT_ASSIGNMENT.DELETE` | Remove an assignment                         |

The **Seat Assignment Viewer** role carries the view permission and is part of **Read Only**, so a read-only user sees occupancy without being able to change it. See [roles and permissions](/platform-guides/platform-configuration/users-permissions/roles-permissions).

## Edge cases

* **A customer cannot hold the same seat twice at once.** Overlapping assignments for the same parent, child and product item are rejected naming the assignment that already covers the period. Sequential assignments for the same three are fine, which is what a child who leaves and returns produces.
* **Over-assignment is allowed.** Nothing compares the number of assignments against `seats_values`, so a parent that paid for five seats can have six children assigned. This is deliberate: the record of who occupies a seat is kept accurate first, and reconciling it against what was paid for is a reporting question, not a reason to reject the write.
* **A child under several parents needs an explicit parent.** `parent_customer_id` is required rather than inferred, because a processing-only child can sit under more than one parent and only you know which one the seat is held against.
* **Ending an assignment does not change an invoice.** Seat counts are billed from `seats_values` on the schedule. Ending an assignment records that the seat was vacated; releasing what the parent pays for is a separate change to the subscription.
* **A deprecated seat type still accepts assignments.** `DEPRECATED` product items are allowed so that an existing seat type being phased out keeps working for the customers still on it.