{"openapi":"3.0.0","info":{"title":"Coram API","description":"# Introduction\n\nThe Coram API enables seamless programmatic access to your security camera infrastructure, allowing you to build custom integrations and automate workflows.\n\n# Supported Actions\n\nManage and interact with your Coram platform resources:\n\n| Resource | Description |\n| -------- | ----------- |\n| **Camera Groups** | Organize and configure camera collections |\n| **Cameras** | Access camera settings, streams, and metadata |\n| **Locations** | Manage physical site configurations |\n| **NVRs** | Control and monitor network video recorders |\n| **Access Control** | List access control doors and trigger remote unlocks |\n\n# Required Headers\n\nEvery API request must include the following headers:\n\n| Header | Required | Description |\n| ------ | -------- | ----------- |\n| `X-Auth-Token` | Yes | Your API key (from the API key List Page) |\n\n**Example:**\n```\ncurl -X GET \"https://api.coram.ai/developer-api/v1/cameras\" \\\n  -H \"X-Auth-Token: xxxxxxxxxxxxxxx\"\n```\n\n# Response Format\n\nAll API responses follow a consistent structure based on the operation result.\n\n## Success Response\n\n**List Operations** (e.g., `GET /v1/cameras`)\n\nReturns a `results` array with pagination support:\n\n```json\n{\n  \"results\": [\n    { \"id\": \"cam_001\", \"name\": \"Front Door\", ... },\n    { \"id\": \"cam_002\", \"name\": \"Lobby\", ... }\n  ],\n  \"has_more\": true\n}\n```\n\nWhen `has_more` is `true`, additional results are available. Use pagination parameters to fetch more.\n\n**Bulk/Batch Operations** (e.g., `POST /v1/cameras`, `PATCH /v1/cameras`)\n\nReturns a top-level `status` field indicating whether the API request was processed, and a `results` array where each item reflects the outcome of an individual operation:\n\n```json\n{\n  \"status\": \"success\",\n  \"results\": [\n    { \"status\": \"success\", \"data\": { \"id\": \"cam_001\", \"mac_address\": \"...\" } },\n    { \"status\": \"error\", \"error\": { \"code\": \"VALIDATION_ERROR\", \"message\": \"...\" } }\n  ]\n}\n```\n\n| Field | Description |\n| ----- | ----------- |\n| `status` (top-level) | `\"success\"` if the request was processed |\n| `results[].status` | `\"success\"` or `\"error\"` for each operation |\n| `results[].data` | Present when `status` is `\"success\"` |\n| `results[].error` | Present when `status` is `\"error\"` |\n\n## Error Response\n\nWhen an error occurs, the response includes `status` set to `\"error\"` along with detailed error information:\n\n```json\n{\n  \"status\": \"error\",\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"message\": \"Invalid MAC address format\",\n    \"field\": \"mac_address\"\n  }\n}\n```\n\n**Error Fields:**\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| `code` | string | Machine-readable error code |\n| `message` | string | Human-readable error description |\n| `field` | string | The field that caused the error (if applicable) |\n\n\n# HTTP Status Codes\n\nThe API uses standard HTTP status codes to indicate the success or failure of requests:\n\n## Success Codes\n\n| Code | Description |\n| ---- | ----------- |\n| `200 OK` | Request succeeded |\n| `201 Created` | Resource successfully created |\n| `204 No Content` | Request succeeded with no response body |\n\n## Client Error Codes\n\n| Code | Description |\n| ---- | ----------- |\n| `400 Bad Request` | Invalid request syntax or parameters |\n| `401 Unauthorized` | Missing or invalid API key |\n| `403 Forbidden` | Valid API key but insufficient permissions |\n| `404 Not Found` | Requested resource does not exist |\n| `422 Unprocessable Entity` | Validation error in request body |\n| `429 Too Many Requests` | Rate limit exceeded |\n\n## Server Error Codes\n\n| Code | Description |\n| ---- | ----------- |\n| `500 Internal Server Error` | Unexpected server error (rare) |\n| `502 Bad Gateway` | Upstream service unavailable |\n| `503 Service Unavailable` | Service temporarily unavailable |\n\n\n# Error Handling Best Practices\n\n1. **Always check the `status` field** — Verify if the response indicates `\"success\"` or `\"error\"`\n\n2. **Handle specific error codes** — Use the `error.code` field to handle different error types programmatically\n\n3. **Implement retry logic** — For `429` and `5xx` errors, implement exponential backoff\n\n4. **Log error details** — Capture `error.code`, `error.message`, and `error.field` for debugging\n\n**Example error handling:**\n```python\nresponse = api.create_camera(data)\n\nif response[\"status\"] == \"error\":\n    error = response[\"error\"]\n    if error[\"code\"] == \"VALIDATION_ERROR\":\n        print(f\"Invalid {error['field']}: {error['message']}\")\n    elif error[\"code\"] == \"DUPLICATE_RESOURCE\":\n        print(\"Camera already exists\")\n    else:\n        print(f\"Error: {error['message']}\")\n```\n\n---\n\n# Getting Started\n\n1. **Generate an API key** from your Coram app settings\n2. **Explore the endpoints** in the navigation to begin integrating\n\nNeed help? Contact [support@coram.ai](mailto:support@coram.ai)\n\n# Webhooks\n\nWebhooks push events from Coram to your server as they happen, so you do not\nhave to poll. Register one endpoint per event type under\n**Settings → Webhooks** in the Coram app. Webhook registration is not part of\nthis API.\n\nEvery delivery is an HTTP `POST` with a JSON body. Respond with any `2xx`\nstatus as soon as you have accepted the event, and do the work afterwards.\n\n## Event types\n\n| Event type | Fires when | `data` payload |\n| ---------- | ---------- | -------------- |\n| `camera_alert` | An alert is triggered on a camera | `WebhookCameraAlertData` |\n| `camera_alert_details` | The AI description and clip for a camera alert (clip is only populated for firearm alerts).  Always fires after camera alert webhook. | `WebhookCameraAlertDetailsData` |\n| `device_status` | A camera or Coram Point goes online or offline | `WebhookDeviceStatusData` |\n| `emergency_activated` | An emergency is activated | `WebhookEmergencyEventData` |\n| `emergency_resolved` | An emergency is resolved | `WebhookEmergencyEventData` |\n| `door_unlocked` | A door is unlocked | `WebhookAccessControlEventData` |\n| `access_denied` | A credential is rejected at a door | `WebhookAccessControlEventData` |\n| `door_forced_open` | A door is opened without authorization | `WebhookAccessControlEventData` |\n| `door_held_open` | A door is held open past its threshold | `WebhookAccessControlEventData` |\n\n`camera_alert` and `camera_alert_details` are two events for the same alert.\nThe first fires immediately. The second fires once the description and clip processing has\nfinished, and carries the payload of\n`GET /v1/alerts/{alert_event_id}/details` plus `latency_ms` and\n`invocation_latency_ms`. Match them on `alert_event_id`.\n\n## Payload envelope\n\nEvery JSON webhook shares one envelope, with the per-event payload under\n`data`:\n\n```json\n{\n  \"event_type\": \"camera_alert\",\n  \"timestamp\": \"2024-01-01T12:00:00+00:00\",\n  \"test\": false,\n  \"tenant\": \"your-tenant-id\",\n  \"text\": \"Camera Alert: Perimeter on Front Door at Main Building\",\n  \"data\": {\n    \"alert_name\": \"Perimeter\",\n    \"camera_name\": \"Front Door\",\n    \"location\": \"Main Building\",\n    \"alert_type\": \"Trespasser\",\n    \"camera_mac_address\": \"AA:BB:CC:DD:EE:FF\",\n    \"organization_name\": \"Acme Corp\",\n    \"alert_event_id\": 12345\n  }\n}\n```\n\n`text` is a human-readable summary of the event, which makes the payload\ndirectly consumable by a Slack incoming webhook URL. `test` is `true` only\nfor deliveries triggered by the **Test** button in the app and never for real\nevents, so use it to keep test traffic out of production handling.\n\nA `camera_alert_details` delivery carries the alert details payload instead:\n\n```json\n{\n  \"event_type\": \"camera_alert_details\",\n  \"timestamp\": \"2024-01-01T12:00:30+00:00\",\n  \"test\": false,\n  \"tenant\": \"your-tenant-id\",\n  \"text\": \"Camera Alert Details: alert 12345 on camera AA:BB:CC:DD:EE:FF\",\n  \"data\": {\n    \"alert_event_id\": 12345,\n    \"alert_time\": \"2024-01-01T12:00:00+00:00\",\n    \"camera_mac_address\": \"AA:BB:CC:DD:EE:FF\",\n    \"ai\": {\n      \"status\": \"ready\",\n      \"short_description\": \"A person approaches the front door.\",\n      \"long_description\": \"A person in a dark jacket walks up to the front door and waits.\"\n    },\n    \"clip\": {\n      \"status\": \"ready\",\n      \"mp4_url\": \"https://developer.coram.ai/v1/assets/clips/...\",\n      \"expires_at\": \"2024-01-02T12:00:00+00:00\"\n    }\n  }\n}\n```\n\n`ai.status` is always terminal (`ready` or `failed`) on this event. The\nwebhook only fires once the description has settled, so you never receive\n`pending` here. `clip` is omitted entirely when no clip is produced for the\nalert, and `clip.mp4_url` expires 24 hours after it is issued.\n\n## Headers\n\nCoram sets these on every delivery, and they cannot be overridden:\n\n| Header | Value |\n| ------ | ----- |\n| `Content-Type` | `application/json` (`application/xml` for CAP XML emergency events) |\n| `User-Agent` | `CoramAI/1.0` |\n| `X-Webhook-Signature` | HMAC-SHA256 of the request body, hex-encoded |\n\nYou can add up to 20 static custom headers per webhook in the app, which is\nuseful for an API key or a tracing header your endpoint requires. Header\nnames must be valid RFC 7230 tokens, and the following are reserved:\n`content-type`, `content-length`, `user-agent`, `host`, `connection`,\n`transfer-encoding`, `x-webhook-signature`.\n\n## Verifying the signature\n\n`X-Webhook-Signature` is the hex-encoded HMAC-SHA256 of the **raw request\nbody bytes**, keyed with your webhook's secret. Compute it over the bytes as\nreceived. Parsing and re-serializing the JSON changes them and the signature\nwill not match. Always compare with a constant-time function, and reject a\nmissing or malformed header rather than letting the comparison raise. Both\nconstant-time primitives below throw on inputs an unauthenticated caller\ncontrols.\n\n```python\nimport hashlib\nimport hmac\n\ndef is_signature_valid(\n    raw_body: bytes, header_signature: str | None, secret: str\n) -> bool:\n    if not header_signature:\n        return False\n    expected = hmac.new(\n        key=secret.encode(\"utf-8\"),\n        msg=raw_body,\n        digestmod=hashlib.sha256,\n    ).hexdigest()\n    # Compare as bytes: compare_digest raises TypeError on a non-ASCII str.\n    return hmac.compare_digest(expected.encode(), header_signature.encode())\n```\n\n```javascript\nimport crypto from \"node:crypto\";\n\nfunction isSignatureValid(rawBody, headerSignature, secret) {\n  if (typeof headerSignature !== \"string\") return false;\n  const expected = Buffer.from(\n    crypto.createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\"),\n    \"utf8\",\n  );\n  const received = Buffer.from(headerSignature, \"utf8\");\n  // timingSafeEqual throws on a length mismatch, so check the lengths first.\n  if (received.length !== expected.length) return false;\n  return crypto.timingSafeEqual(expected, received);\n}\n```\n\nYour secret is shown when you create the webhook and can be regenerated in\nthe app. Regenerating takes effect immediately, and deliveries signed with\nthe previous secret stop at that point.\n\n## Delivery semantics\n\n| Property | Behaviour |\n| -------- | --------- |\n| Timeout | 5 seconds to connect, and 5 seconds to read your response |\n| Redirects | Not followed, so a `3xx` counts as a failed delivery |\n| Retries | None for most events. A failed delivery is logged and dropped |\n| Ordering | Not guaranteed, so use `timestamp` and `alert_event_id` to order and deduplicate |\n| Destination | Must be a publicly resolvable address. Private and loopback addresses are rejected |\n\n`camera_alert_details` is the exception to retries. It is retried on\ntransient failures, so your handler must be idempotent for a repeated\n`alert_event_id`.\n\nIf your endpoint is slow, deliveries will time out and be lost. Acknowledge\nfirst, process asynchronously.\n\n## Emergency events\n\n`emergency_activated` and `emergency_resolved` deliver **CAP 1.2 XML** by\ndefault, not the JSON envelope above, so an integration built for the JSON\nevents will not parse them as-is. Switch the webhook's response format to\nJSON in the app if you want the standard envelope with\n`WebhookEmergencyEventData` under `data`.\n","version":"1.0.0"},"servers":[{"url":"https://developer.coram.ai","description":"Production"}],"paths":{"/v1/alerts/{alert_event_id}/clip":{"get":{"tags":["alerts"],"summary":"Get alert video clip","description":"Returns a public download URL for the MP4 video clip associated with an alert event. The URL does not require authentication and expires after 24 hours. Use the alert_event_id from webhook payloads to retrieve the corresponding video clip.","operationId":"get_alert_clip","parameters":[{"required":true,"schema":{"type":"integer","title":"Alert Event Id"},"name":"alert_event_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertClipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-public-api":true,"security":[{"X-Auth-Token":[]}]}},"/v1/alerts/{alert_event_id}/feedback":{"get":{"tags":["alerts"],"summary":"Get firearm alert feedback","description":"Returns the current feedback (0.0-1.0 rating, reason, comment) for a firearm alert event. Feedback is organization-wide (last write wins); the feedback fields are null when no feedback has been submitted yet. Returns 404 if the alert event does not exist and 400 if it is not a firearm alert.","operationId":"get_firearm_alert_feedback","parameters":[{"required":true,"schema":{"type":"integer","title":"Alert Event Id"},"name":"alert_event_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirearmAlertFeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-public-api":true,"security":[{"X-Auth-Token":[]}]},"put":{"tags":["alerts"],"summary":"Submit firearm alert feedback","description":"Submits feedback for a firearm alert event. rating is a 0.0-1.0 score (today 0.0 = incorrect / unhelpful, 1.0 = correct). Feedback is organization-wide and idempotent (last write wins): submitting again replaces the previous feedback. feedback_reason and feedback_comment are optional. Returns 404 if the alert event does not exist and 400 if it is not a firearm alert.","operationId":"submit_firearm_alert_feedback","parameters":[{"required":true,"schema":{"type":"integer","title":"Alert Event Id"},"name":"alert_event_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirearmAlertFeedbackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirearmAlertFeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-public-api":true,"security":[{"X-Auth-Token":[]}]}},"/v1/alerts/{alert_event_id}/details":{"get":{"tags":["alerts"],"summary":"Get alert details (including AI description)","description":"Returns metadata about an alert event, including the rich natural-language description generated by Coram's vision language model. The description is produced asynchronously after the alert webhook fires, so consumers polling immediately may see `ai.status: pending` for a few seconds. Once `ai.status` is `ready`, the `ai.short_description` and `ai.long_description` fields are populated. If it is `failed`, no description will arrive.\n\nWhen a video clip is being produced for the alert, a `clip` object is included: `clip.status` is `ready` (with a download `mp4_url` valid for 24 hours and an `expires_at`), `pending` (still uploading — poll again shortly), or `failed`. Alerts with no clip pipeline omit the `clip` field entirely.","operationId":"get_alert_details","parameters":[{"required":true,"schema":{"type":"integer","title":"Alert Event Id"},"name":"alert_event_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDetailsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-public-api":true,"security":[{"X-Auth-Token":[]}]}},"/v1/alerts/firearm":{"post":{"tags":["alerts"],"summary":"Create firearm alert","description":"Creates a firearm-detection alert configuration. Recipients are specified by email (matched case-insensitively) and resolved to platform users in your organization; each recipient's notify_by_sms/notify_by_push only deliver when the user has a phone number / registered device. The schedule is interpreted in each camera's local time. The alert is attributed to the API key (creator_name = the key's identity). Note: a null name is omitted from the response rather than serialized as null.","operationId":"create_firearm_alert","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFirearmAlertRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirearmAlertResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Firearm alert not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Camera assignment conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/alerts/firearm/{firearm_alert_id}":{"delete":{"tags":["alerts"],"summary":"Delete firearm alert","description":"Deletes a firearm-detection alert configuration and any alerts it has produced. Returns 204 on success and 404 if the firearm alert does not exist in your organization.","operationId":"delete_firearm_alert","parameters":[{"required":true,"schema":{"type":"integer","title":"Firearm Alert Id"},"name":"firearm_alert_id","in":"path"}],"responses":{"204":{"description":"Successful Response"},"403":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Firearm alert not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true},"patch":{"tags":["alerts"],"summary":"Update firearm alert","description":"Partially updates a firearm-detection alert configuration; only the provided fields are changed. Providing recipients replaces the full recipient list. The schedule is interpreted in each camera's local time. Note: a null name is omitted from the response rather than serialized as null.","operationId":"update_firearm_alert","parameters":[{"required":true,"schema":{"type":"integer","title":"Firearm Alert Id"},"name":"firearm_alert_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFirearmAlertRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FirearmAlertResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Firearm alert not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Camera assignment conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true}},"/v1/cameras":{"get":{"tags":["cameras"],"summary":"List cameras","description":"List all cameras accessible to the user.\n\nRetrieve cameras accessible to the user with their current status and\nconfiguration details. Results can be filtered by NVR, location,\nMAC address, IP address, or camera group ID.","operationId":"list_cameras","parameters":[{"required":false,"schema":{"items":{"type":"string"},"type":"array","title":"nvr"},"name":"nvrs","in":"query"},{"required":false,"schema":{"items":{"type":"integer"},"type":"array","title":"location_id"},"name":"location_ids","in":"query"},{"required":false,"schema":{"type":"boolean","title":"exclude_disabled","default":false},"name":"exclude_disabled","in":"query"},{"required":false,"schema":{"items":{"type":"string"},"type":"array","title":"mac_address"},"name":"mac_addresses","in":"query"},{"required":false,"schema":{"items":{"type":"string"},"type":"array","title":"ip"},"name":"ips","in":"query"},{"required":false,"schema":{"items":{"type":"integer"},"type":"array","title":"group_id"},"name":"group_ids","in":"query"},{"required":false,"schema":{"type":"integer","minimum":1.0,"title":"page","default":1},"name":"page","in":"query"},{"required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"limit","default":100},"name":"limit","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraList"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}]},"post":{"tags":["cameras"],"summary":"Register cameras","description":"Register multiple cameras in single location.\n\nRegister one or more cameras at a location by assigning them to available NVRs.\nCameras are automatically load-balanced across NVRs based on available capacity.","operationId":"register_cameras","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterCameraRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterCameraResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]},"delete":{"tags":["cameras"],"summary":"Delete cameras","description":"Delete one or more cameras.\n\nRemove one or more cameras from the system.","operationId":"delete_cameras","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DeleteCameraPayload"},"type":"array","title":"DeleteCameraPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteCameraResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]},"patch":{"tags":["cameras"],"summary":"Update cameras","description":"Update one or more cameras.\n\nUpdate configuration, credentials, feature flags, and group assignments\nfor one or more cameras. Each camera is updated independently - if one\ncamera's update fails, others can still succeed. Within each camera's\nupdate, all changes are atomic.\n\n**Group Assignment:**\n- Provide `group_ids` to assign the camera to specific groups\n- An empty list `[]` resets the camera to the default group\n- Omit `group_ids` (null) to leave group assignments unchanged\n\n**IP Address:**\n- Provide `ip` to change the address the NVR connects to; it takes effect\n  within a few minutes without any action on the NVR\n- The change applies to every head of a multi-head camera\n- On NVRs with automatic network scan enabled, discovery overrides the\n  value if it does not match the IP the camera is actually reachable at","operationId":"update_cameras","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UpdateCameraPayload"},"type":"array","title":"UpdateCameraPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCameraResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/cameras/delete":{"post":{"tags":["cameras"],"summary":"Delete cameras (POST)","description":"Delete one or more cameras.\n\nEquivalent to `DELETE /v1/cameras` for clients that cannot send a request\nbody with DELETE.","operationId":"delete_cameras_via_post","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DeleteCameraPayload"},"type":"array","title":"DeleteCameraPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteCameraResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/cameras/move":{"post":{"tags":["cameras"],"summary":"Move cameras","description":"Move one or more cameras to different NVRs.\n\nTransfer one or more cameras from one NVR to another.","operationId":"move_cameras","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/MoveCameraPayload"},"type":"array","title":"MoveCameraPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveCameraResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/camera-groups":{"get":{"tags":["camera-groups"],"summary":"List camera groups","description":"List camera groups.\n\nRetrieve a paginated list of camera groups.\nGroups are used to organize cameras for easier management and access control.","operationId":"list_camera_groups","parameters":[{"required":false,"schema":{"type":"integer","minimum":1.0,"title":"Page","default":1},"name":"page","in":"query"},{"required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","default":100},"name":"limit","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraGroupList"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}]},"post":{"tags":["camera-groups"],"summary":"Create camera group","description":"Create a new camera group.\n\nCreate a new camera group with the specified name. Camera groups are used\nto organize cameras for easier management and access control.","operationId":"create_camera_group","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCameraGroupPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCameraGroupResponse"}}}},"409":{"description":"Camera group name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Camera group creation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/nvrs":{"get":{"tags":["nvrs"],"summary":"List NVRs","description":"List all NVRs accessible to the user.\n\nRetrieve NVRs accessible to the user with their current status and\nconfiguration details. Results can be filtered by NVR UUID or location.","operationId":"list_nvrs","parameters":[{"required":false,"schema":{"type":"string","title":"nvr"},"name":"nvr","in":"query"},{"required":false,"schema":{"type":"integer","title":"location_id"},"name":"location_id","in":"query"},{"required":false,"schema":{"type":"integer","minimum":1.0,"title":"page","default":1},"name":"page","in":"query"},{"required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"limit","default":100},"name":"limit","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NVRList"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}]},"post":{"tags":["nvrs"],"summary":"Register NVRs","description":"Register multiple NVRs in bulk.\n\nRegister one or more NVRs by assigning them to locations.","operationId":"register_nvrs","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RegisterNVRPayload"},"type":"array","title":"RegisterNVRPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterNVRResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/nvrs/ping":{"post":{"tags":["nvrs"],"summary":"Ping NVR","description":"Run ping test on NVR.\n\nSend a ping request to the specified NVR to test network connectivity.","operationId":"ping_nvr","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingRequestBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PingSuccessResponse"},{"$ref":"#/components/schemas/PingFailureResponse"}],"title":"Response Ping Nvr"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}],"parameters":[]}},"/v1/nvrs/speedtest":{"post":{"tags":["nvrs"],"summary":"Speed test NVR","description":"Run speed test on NVR.\n\nInitiate a speed test on the specified NVR to measure network performance.","operationId":"speedtest_nvr","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeedTestRequestPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SpeedTestSuccessResponse"},{"$ref":"#/components/schemas/SpeedTestFailureResponse"}],"title":"Response Speedtest Nvr"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}],"parameters":[]}},"/v1/locations":{"get":{"tags":["locations"],"summary":"List locations","description":"List all locations accessible to the user.\n\nRetrieve locations accessible to the user for the organization.\nReturns locations across all product lines the user has access to.","operationId":"list_locations","parameters":[{"required":false,"schema":{"type":"integer","minimum":1.0,"title":"page","default":1},"name":"page","in":"query"},{"required":false,"schema":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"limit","default":100},"name":"limit","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationList"}}}},"403":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}]},"post":{"tags":["locations"],"summary":"Create locations","description":"Create multiple locations in bulk.\n\nCreate one or more locations for the organization.","operationId":"create_locations","requestBody":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CreateLocationPayload"},"type":"array","title":"CreateLocationPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLocationResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"X-Auth-Token":[]}],"x-public-api-version":"v1","x-public-api":true,"parameters":[]}},"/v1/access-control/events/{event_id}/clip":{"get":{"tags":["events"],"summary":"Get event clip URL","description":"Returns a public download URL for the MP4 video clip captured by the event's door's primary camera around the event timestamp. The URL does not require authentication and expires after 24 hours. Use the event_id from the events list endpoint or a webhook payload to fetch the clip.","operationId":"GetEventClip","parameters":[{"required":true,"schema":{"type":"integer","title":"Event Id"},"name":"event_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventClipResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-public-api-version":"v1","x-public-api":true,"security":[{"X-Auth-Token":[]}]}},"/v1/access-control/doors":{"get":{"operationId":"list_access_control_doors","summary":"List access control doors","description":"Returns the doors the API key creator can see, with their online\nstatus. Admins see every door in the tenant; lower roles see only\ndoors they have permission to unlock. Use `page` and `limit` to\npaginate; `has_more` on the response signals more pages remain.\n","tags":["doors"],"security":[{"X-Auth-Token":[]}],"parameters":[{"in":"query","name":"page","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-indexed page number."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":100},"description":"Maximum doors per page (capped at 100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoorList"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope or role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}},"x-public-api":true,"x-public-api-version":"v1"}},"/v1/access-control/events":{"get":{"operationId":"list_access_control_events","summary":"List access control events","description":"Returns events emitted by access control devices: card scans\n(`card_reader_event`), grant/deny decisions (`access_control_event`),\nREX presses (`rex_event`), door-held-open alarms (`door_held_open_event`),\nbattery alerts, and so on. Use the filters to narrow down by door,\ncardholder, event type, or time window. Pagination uses the same\n`page` + `limit` + `has_more` shape as every other developer-API list\nendpoint.\n\nThe per-event `details` object carries event-type-specific data\n(card_id, access_decision_reason, etc.). Treat it defensively: new\nkeys may appear in future event types.\n","tags":["events"],"security":[{"X-Auth-Token":[]}],"parameters":[{"in":"query","name":"start_time","required":false,"schema":{"type":"string","format":"date-time"},"description":"RFC3339 inclusive lower bound on `timestamp`."},{"in":"query","name":"end_time","required":false,"schema":{"type":"string","format":"date-time"},"description":"RFC3339 inclusive upper bound on `timestamp`."},{"in":"query","name":"door_ids","required":false,"schema":{"type":"array","items":{"type":"integer","format":"int64"}},"style":"form","explode":true,"description":"Repeat to filter to events emitted at specific doors."},{"in":"query","name":"cardholder_ids","required":false,"schema":{"type":"array","items":{"type":"integer","format":"int64"}},"style":"form","explode":true,"description":"Repeat to filter to events for specific cardholders."},{"in":"query","name":"event_types","required":false,"schema":{"type":"array","items":{"type":"string","enum":["card_reader_event","access_control_event","remote_access_control_event","door_relay_event","door_forced_open_event","door_held_open_event","rex_event","door_position_event","fire_alarm_event","battery_status_event","device_lid_status_event","relay_testing_button_event","door_control_state_event"]}},"style":"form","explode":true,"description":"Repeat to filter to a subset of event types."},{"in":"query","name":"page","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-indexed page number."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":100},"description":"Maximum events per page (capped at 100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventList"}}}},"400":{"description":"Invalid filter values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}},"x-public-api":true,"x-public-api-version":"v1"}},"/v1/access-control/doors/{door_id}/unlock":{"post":{"operationId":"unlock_access_control_door","summary":"Unlock an access control door","description":"Sends a momentary-unlock command to the door's controller board.\nRequires `write:*` scope and an `admin` creator role. The same\npermission checks the in-app unlock flow runs are applied; an API key\nwhose creator no longer has access to the door is rejected.\n","tags":["doors"],"security":[{"X-Auth-Token":[]}],"parameters":[{"in":"path","name":"door_id","required":true,"schema":{"type":"integer","format":"int64"},"description":"ID of the door to unlock"}],"responses":{"200":{"description":"Unlock command issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnlockResponse"}}}},"400":{"description":"Invalid door_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope or role, or the creator cannot unlock this door","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}},"x-public-api":true,"x-public-api-version":"v1"}},"/v1/reunification/imports":{"post":{"operationId":"create_reunification_import","summary":"Import reunification roster data","description":"Imports a reunification roster as JSON. This is the API\nequivalent of the in-app CSV upload and writes to the same roster.\n\n**Every call replaces the whole roster.** Records you send are created\nor updated, and anything you leave out is deleted. Only records that\ncame from this API or the in-app CSV upload are replaced.\n\nDeleting someone revokes the Coram access an earlier import gave them.\nAccess granted by hand survives. Revocation is also skipped when a previous\nimport has not finished creating accounts yet, and when more than half\nof a provisioned roster of 10 or more people would lose access at once,\nwhich stops a truncated upload locking out a school.\n\nSend every key on every call. The one you may omit is `staff`, and\nomitting it deletes every staff member.\n\nThe import aborts and leaves the old roster intact if two schools map to\nthe same Coram location, if a location id does not exist, or if a\nstudent or teacher repeats a school id in `school_ids` after the first\none. Repeating a `student_id`, `teacher_id` or `staff_id` returns `400`\nbefore any work starts.\n\nThe order of the top level arrays does not matter.\n\nThe import runs in the background and is cancelled after one hour. The\n`202` carries the initial status. Poll\n`GET /v1/reunification/imports/status`. One import runs per organization\nat a time and the in-app CSV upload counts as one, so a second request\nreturns `409`.\n\nRequires `write:*` scope and an `admin` creator role, and the feature\nmust be enabled for your organization. The status endpoint needs\n`read:*`, so a write-only key can start an import but never see whether\nit worked. Requests are limited to an average of 5 per second with a\nshort burst allowance, and the body to 32 MiB.\n","tags":["reunification"],"security":[{"X-Auth-Token":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReunificationImportInput"},"example":{"schools":[{"school_id":"SCH-01","name":"Lincoln High School"}],"students":[{"student_id":"STU-1001","name":"Ada Reyes","email":"ada.reyes@example.edu","grade":"9","school_ids":["SCH-01"],"guardians":[{"name":"Marta Reyes","email":"marta.reyes@example.com","phone":"+14155550101","relationship":"Mother"}]}],"teachers":[{"teacher_id":"TCH-200","name":"Sam Okafor","email":"sam.okafor@example.edu","school_ids":["SCH-01"]}],"staff":[{"staff_id":"STF-300","name":"Dana Whitfield","email":"dana.whitfield@example.edu","title":"Front Office","school_ids":["SCH-01"]}],"sections":[{"section_id":"SEC-9A","teacher_ids":["TCH-200"],"student_ids":["STU-1001"]}],"school_location_mappings":{"SCH-01":42}}}}},"responses":{"202":{"description":"Import accepted and processing started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReunificationImportStatus"}}}},"400":{"description":"Body is not a JSON object, omits or nulls any of schools/students/teachers/sections, has an empty school_location_mappings, repeats a student/teacher/staff id, or exceeds 32 MiB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope/role, or the feature is not enabled for the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"409":{"description":"An import is already running for this organization, including one started by the in-app CSV upload. Poll the existing one instead. The lock clears after one hour at the latest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}},"x-public-api":true,"x-public-api-version":"v1","parameters":[]}},"/v1/reunification/imports/status":{"get":{"operationId":"get_reunification_import_status","summary":"Get the current reunification import status","description":"Returns the status of your organization's most recent reunification\nimport. Poll this after starting one. Requires `read:*` scope and a\n`regular` creator role.\n\nStatus is kept for one hour after its last update and then this returns\n`404`. Store anything you need to keep.\n\n`completed` means the roster was written and account provisioning was\nhanded to a background worker, not that every account exists.\n`succeeded_users` and `failed_users` keep rising afterwards, which can\ntake several minutes on a large roster, so poll until both stop moving.\n\nThe `synced_*` and `total_*` pairs are a progress bar rather than a\nsuccess ratio. They end up equal whenever the roster is written, so they\ncannot tell you a record was rejected.\n\nA `failed` status does not mean the roster is untouched. If the failure\ncame after the roster was committed the new roster is live, and account\nprovisioning may not have started or may already be under way.\n","tags":["reunification"],"security":[{"X-Auth-Token":[]}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReunificationImportStatus"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope/role, or the feature is not enabled for the organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"No import found for this organization, or the status expired. Status is kept for one hour after its last update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}},"x-public-api":true,"x-public-api-version":"v1","parameters":[]}},"/v1/alerts/{alert_event_id}/clip.mp4":{"get":{"operationId":"get_alert_clip_mp4","summary":"Download an alert's video clip","description":"Streams the alert's clip as MP4 bytes. Use this when you want the video\nitself; use `GET /v1/alerts/{alert_event_id}/clip` instead when you want\na temporary URL you can hand to a player or another system.\n\nSupports HTTP `Range` requests, so players can seek without downloading\nthe whole file. To learn a clip's size without fetching it, send\n`Range: bytes=0-0` and read the total from `Content-Range`.\n\nBatch-generated clips are retained for 7 days after the alert.\nStreaming-generated clips are available through this endpoint for 1\nhour because their readiness marker expires before the stored object.\nAnything else — the clip was never requested, the upload has not\nfinished, or the applicable availability window has passed — returns\n404.\n","tags":["alerts"],"security":[{"X-Auth-Token":[]}],"parameters":[{"in":"path","name":"alert_event_id","required":true,"schema":{"type":"integer","format":"int64"},"description":"The alert event to download the clip for."}],"responses":{"200":{"description":"The clip's MP4 bytes.","content":{"video/mp4":{"schema":{"type":"string","format":"binary"}}}},"206":{"description":"The requested byte range of the clip, when a `Range` header was sent.","content":{"video/mp4":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"The alert event ID is outside the supported integer range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope or role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"No clip available for this alert","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"405":{"description":"The endpoint only supports GET requests"}},"x-public-api":true,"x-public-api-version":"v1"}},"/v1/alerts/{alert_event_id}/thumbnail":{"get":{"operationId":"get_alert_thumbnail","summary":"Download an alert's image","description":"Returns the alert's image as JPEG bytes. For firearm and PPE alerts this\nis the full-resolution frame the detection ran on; for every other\ncamera alert it is the stored alert image, which is the detection crop.\n\nPass `size=medium` or `size=small` to receive a re-encoded image\nbounded to roughly 128 KB or 64 KB; resized responses ignore Range\nrequests.\n\nReturns 404 when the alert has no image — it is not a camera alert, or\nthe object has already been deleted from storage.\n","tags":["alerts"],"security":[{"X-Auth-Token":[]}],"parameters":[{"in":"path","name":"alert_event_id","required":true,"schema":{"type":"integer","format":"int64"},"description":"The alert event to download the image for."},{"in":"query","name":"size","required":false,"schema":{"type":"string","enum":["small","medium","full"]},"description":"Target size of the returned image. `full` returns the stored image\nunmodified. `medium` re-encodes to roughly 128 KB and `small` to\nroughly 64 KB; an image already within the target is returned\nunmodified. When omitted, a server-configured default applies\n(currently `full`).\n"}],"responses":{"200":{"description":"The alert image's JPEG bytes.","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"The alert event ID is outside the supported integer range, or the size value is not one of small, medium, full","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"API key lacks the required scope or role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"No image available for this alert","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"405":{"description":"The endpoint only supports GET requests"}},"x-public-api":true,"x-public-api-version":"v1"}}},"tags":[{"name":"alerts","description":""},{"name":"camera-groups","description":"Organize cameras into logical collections for easier management. Camera groups allow you to categorize cameras by location, purpose, or any custom criteria. Use these endpoints to create, update, list, and delete camera groups."},{"name":"cameras","description":"Manage individual cameras within your organization. Use these endpoints to register new cameras, update camera settings, retrieve camera information and status, move cameras between locations, and delete cameras from your system."},{"name":"doors","description":"List access control doors and trigger momentary remote unlocks. The same per-door permission checks the in-app unlock flow runs apply here — a key whose creator cannot unlock a door in the UI cannot unlock it via the API either."},{"name":"events","description":"Query access control device events (card scans, REX presses, forced-open / held-open alarms, battery alerts, …) and fetch the MP4 video clip recorded by the door's primary camera around an event's timestamp."},{"name":"locations","description":"Manage physical site configurations where cameras and NVRs are deployed. Locations represent your facilities, buildings, or areas. Use these endpoints to create, update, list, and delete locations."},{"name":"nvrs","description":"Control and monitor Network Video Recorders (NVRs) in your infrastructure. NVRs are hardware devices that record and store video from connected cameras. Use these endpoints to register NVRs, update their configuration, check connectivity status, run speed tests, and manage NVR settings."},{"name":"reunification","description":"Import your reunification roster of schools, students with their guardians, teachers, staff and class sections. This is the API equivalent of the in-app CSV upload. Every import replaces the roster rows that came from this API or the CSV upload, so anyone you leave out is removed. A roster synced from Clever is not touched. Imports run in the background, so poll the status endpoint for progress."}],"x-tagGroups":[{"name":"Surveillance","tags":["cameras","camera-groups","locations","nvrs","alerts"]},{"name":"Access Control","tags":["doors","events"]},{"name":"Emergency Management System","tags":["reunification"]}],"components":{"schemas":{"AlertClipResponse":{"properties":{"preview_url":{"type":"string","title":"Preview Url","description":"Web player page URL for viewing the clip in a browser. No authentication required. Valid until expires_at."},"mp4_url":{"type":"string","title":"Mp4 Url","description":"Direct download URL for the MP4 video clip. No authentication required. Valid until expires_at."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"When the URLs expire (UTC)"},"clip_start_time":{"type":"string","format":"date-time","title":"Clip Start Time","description":"Start time of the video clip (UTC)"},"clip_end_time":{"type":"string","format":"date-time","title":"Clip End Time","description":"End time of the video clip (UTC)"},"alert_event_id":{"type":"integer","title":"Alert Event Id","description":"ID of the alert event"},"camera_mac_address":{"type":"string","title":"Camera Mac Address","description":"MAC address of the camera that captured the alert"}},"type":"object","required":["preview_url","mp4_url","expires_at","clip_start_time","clip_end_time","alert_event_id","camera_mac_address"],"title":"AlertClipResponse","description":"Response for the alert clip download endpoint."},"FirearmAlertFeedbackRequest":{"properties":{"rating":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Rating","description":"Feedback score from 0.0 to 1.0. Today only 0.0 (incorrect / unhelpful) and 1.0 (correct) are produced by clients; intermediate values are reserved for future granular feedback."},"feedback_reason":{"allOf":[{"$ref":"#/components/schemas/AlertFeedbackReason"}],"description":"Optional structured reason for the feedback, typically provided with a sub-1.0 rating."},"feedback_comment":{"type":"string","maxLength":1000,"title":"Feedback Comment","description":"Optional free-text note (max 1000 characters), typically used with feedback_reason='other'."}},"type":"object","required":["rating"],"title":"FirearmAlertFeedbackRequest","description":"Request body for submitting feedback on a firearm alert."},"AlertFeedbackReason":{"type":"string","enum":["wrong_activity","not_useful","too_late","other"],"title":"AlertFeedbackReason","description":"Structured feedback reasons for thumbs down."},"SpeedTestRequestPayload":{"properties":{"nvr":{"type":"string","title":"nvr","description":"NVR name to run the speed test from"}},"type":"object","required":["nvr"],"title":"SpeedTestRequestPayload","description":"Request model for speed test from an NVR."},"CameraGroupList":{"properties":{"results":{"items":{"$ref":"#/components/schemas/CameraGroup"},"type":"array","title":"Results","description":"List of camera groups"},"has_more":{"type":"boolean","title":"Has More","description":"True if additional pages are available"}},"type":"object","required":["results","has_more"],"title":"CameraGroupList","description":"Paginated list of camera groups.","x-public-api-group":"camera-groups"},"CameraGroup":{"properties":{"id":{"type":"integer","title":"Id","description":"Unique camera group identifier"},"name":{"type":"string","title":"Name","description":"Camera group display name"}},"type":"object","required":["id","name"],"title":"CameraGroup","description":"A logical grouping of cameras for organization and access control.","x-public-api-group":"camera-groups"},"CreateFirearmAlertRequest":{"properties":{"name":{"type":"string","maxLength":255,"title":"Name"},"camera_mac_addresses":{"items":{"type":"string"},"type":"array","title":"Camera Mac Addresses"},"recipients":{"items":{"$ref":"#/components/schemas/FirearmAlertRecipient"},"type":"array","title":"Recipients"},"schedule":{"$ref":"#/components/schemas/FirearmAlertSchedule"},"sensitivity":{"allOf":[{"$ref":"#/components/schemas/AlertSensitivityLevel"}],"default":"Medium"},"handgun_positive_stances":{"items":{"$ref":"#/components/schemas/HandgunStanceType"},"type":"array","default":[]},"push_notification_level":{"allOf":[{"$ref":"#/components/schemas/PushNotificationLevel"}],"default":"active"}},"type":"object","required":["camera_mac_addresses","recipients"],"title":"CreateFirearmAlertRequest"},"FirearmAlertRecipient":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"notify_by_email":{"type":"boolean","title":"Notify By Email","default":true},"notify_by_sms":{"type":"boolean","title":"Notify By Sms","description":"Only delivered if the user has a phone number on file.","default":true},"notify_by_push":{"type":"boolean","title":"Notify By Push","description":"Only delivered if the user has a registered mobile device.","default":true}},"type":"object","required":["email"],"title":"FirearmAlertRecipient","description":"A platform user (resolved by email) to notify, with per-channel toggles."},"HandgunStanceType":{"enum":["INACTIVE_CARRY","OBJECT_HOLSTERED"],"title":"HandgunStanceType","description":"An enumeration."},"FirearmAlertSchedule":{"properties":{"start_time":{"type":"string","format":"time","title":"Start Time","default":"00:00:00"},"end_time":{"type":"string","format":"time","title":"End Time","default":"23:59:59"},"days_of_week":{"items":{"$ref":"#/components/schemas/DayOfWeek"},"type":"array","default":["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]}},"type":"object","title":"FirearmAlertSchedule","description":"When the alert is active, expressed in camera-local (naive) time."},"DayOfWeek":{"enum":["monday","tuesday","wednesday","thursday","friday","saturday","sunday"],"title":"DayOfWeek","description":"An enumeration."},"AlertSensitivityLevel":{"type":"string","enum":["Low","Medium","High"],"title":"AlertSensitivityLevel","description":"An enumeration."},"PushNotificationLevel":{"type":"string","enum":["active","critical","passive","time-sensitive"],"title":"PushNotificationLevel","description":"Defines the interruption level for mobile push notifications.\n\nThis level determines the notification priority and how it will interrupt the user:\n- ACTIVE: Standard priority notifications\n- CRITICAL: High priority notifications that can break through focus modes and DND.\n- PASSIVE: Low priority notifications that don't trigger sounds or banners\n- TIME_SENSITIVE: Important time-sensitive notifications that can break through\nsome focus modes"},"CameraList":{"properties":{"results":{"items":{"$ref":"#/components/schemas/Camera"},"type":"array","title":"Camera","description":"List of cameras"},"has_more":{"type":"boolean","title":"Has More","description":"True if additional pages are available"}},"type":"object","required":["results","has_more"],"title":"CameraList","description":"Paginated list of cameras.","x-public-api-group":"cameras"},"Camera":{"properties":{"id":{"type":"integer","title":"Id","description":"Unique camera identifier"},"mac_address":{"type":"string","title":"Mac Address","description":"Camera MAC address"},"name":{"type":"string","title":"Name","description":"Camera display name"},"ip":{"type":"string","title":"Ip","description":"Camera IP address on the local network"},"vendor":{"type":"string","title":"Vendor","description":"Camera manufacturer/brand"},"manufacturer":{"type":"string","title":"Manufacturer","description":"Camera hardware manufacturer"},"model":{"type":"string","title":"Model","description":"Camera hardware model"},"is_enabled":{"type":"boolean","title":"Is Enabled","description":"Whether the camera is enabled for recording"},"status":{"allOf":[{"$ref":"#/components/schemas/CameraStatus"}],"title":"Status","description":"Current connectivity status"},"nvr":{"allOf":[{"$ref":"#/components/schemas/CameraNVRInfo"}],"title":"Nvr","description":"NVR the camera is assigned to"},"rtsp_port":{"type":"integer","title":"Rtsp Port","description":"RTSP streaming port number"},"width":{"type":"integer","title":"Width","description":"Video resolution width in pixels"},"height":{"type":"integer","title":"Height","description":"Video resolution height in pixels"},"fps":{"type":"integer","title":"Fps","description":"Video frames per second"},"codec":{"type":"string","title":"Codec","description":"Video codec (e.g., 'H264', 'H265')"},"onvif_enabled":{"type":"boolean","title":"Onvif Enabled","description":"Whether ONVIF protocol is enabled"},"video_orientation_type":{"type":"string","title":"Video Orientation Type","description":"Video orientation (e.g., 'normal', 'rotated_180')"},"feature_flags":{"allOf":[{"$ref":"#/components/schemas/FeatureFlags"}],"title":"Feature Flags","description":"Current feature flag settings"},"camera_groups":{"items":{"$ref":"#/components/schemas/CameraGroup"},"type":"array","title":"CameraGroup","description":"Groups this camera belongs to"},"location":{"allOf":[{"$ref":"#/components/schemas/CameraLocation"}],"title":"Location","description":"Location where the camera is installed"}},"type":"object","required":["id","mac_address","name","ip","vendor","is_enabled","status","nvr","rtsp_port","onvif_enabled","video_orientation_type","feature_flags","camera_groups"],"title":"Camera","description":"Complete camera information in list response.","x-public-api-group":"cameras"},"CameraLocation":{"properties":{"id":{"type":"integer","title":"Id","description":"Unique location identifier"},"name":{"type":"string","title":"Name","description":"Location display name"},"timezone":{"type":"string","title":"Timezone","description":"IANA timezone (e.g., 'America/New_York')"}},"type":"object","required":["id","name","timezone"],"title":"CameraLocation","description":"Location associated with cameras and camera groups.","x-public-api-group":"cameras"},"CameraStatus":{"properties":{"is_online":{"type":"boolean","title":"Is Online","description":"Indicates whether the camera is currently connected and accessible"},"last_seen_time":{"type":"string","title":"Last Seen Time","description":"Last connection timestamp (ISO 8601 format)"}},"type":"object","required":["is_online"],"title":"CameraStatus","description":"Current connectivity status of a camera.","x-public-api-group":"cameras"},"FeatureFlags":{"properties":{"is_audio_enabled":{"type":"boolean","title":"Is Audio Enabled","description":"Whether audio recording is enabled"},"is_license_plate_detection_enabled":{"type":"boolean","title":"Is License Plate Detection Enabled","description":"Whether license plate detection is enabled"},"is_force_fps_enabled":{"type":"boolean","title":"Is Force Fps Enabled","description":"Whether FPS is forced to a specific value"},"auto_tracking":{"type":"boolean","title":"Auto Tracking","description":"Whether PTZ auto-tracking is enabled"}},"type":"object","required":["is_audio_enabled","is_license_plate_detection_enabled","is_force_fps_enabled","auto_tracking"],"title":"FeatureFlags","description":"Current feature flag settings for a camera.","x-public-api-group":"cameras"},"CameraNVRInfo":{"properties":{"name":{"type":"string","title":"Name","description":"NVR display name"},"timezone":{"type":"string","title":"Timezone","description":"NVR timezone (IANA format, e.g., 'America/New_York')"}},"type":"object","required":["name"],"title":"CameraNVRInfo","description":"NVR (Network Video Recorder) associated with the camera.","x-public-api-group":"cameras"},"PingRequestBody":{"properties":{"ip_address":{"type":"string","title":"ip_address","description":"IPv4 address to ping"},"nvr":{"type":"string","title":"nvr","description":"NVR name to run the ping from"}},"type":"object","required":["ip_address","nvr"],"title":"PingRequestBody","description":"Request model for ping test from an NVR."},"SpeedTestSuccessResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/SpeedTestSuccessData"}],"title":"SpeedTestSuccessData","description":"Speed test results"}},"type":"object","required":["data"],"title":"SpeedTestSuccessResponse","description":"Success response for speed test request."},"SpeedTestSuccessData":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"Timestamp of speed test result (ISO 8601 format)"},"domain":{"type":"string","title":"Domain","description":"Domain used for speed test"},"avg_ping_latency_ms":{"type":"number","title":"Avg Ping Latency Ms","description":"Average ping latency in milliseconds"},"packet_loss":{"type":"number","title":"Packet Loss","description":"Packet loss percentage (0-100)"},"download_speed_bps":{"type":"number","title":"Download Speed Bps","description":"Download speed in bits per second"},"upload_speed_bps":{"type":"number","title":"Upload Speed Bps","description":"Upload speed in bits per second"}},"type":"object","required":["timestamp","domain","download_speed_bps","upload_speed_bps"],"title":"SpeedTestSuccessData","description":"Speed test results."},"UpdateFirearmAlertRequest":{"properties":{"name":{"type":"string","maxLength":255,"title":"Name"},"camera_mac_addresses":{"items":{"type":"string"},"type":"array","title":"Camera Mac Addresses"},"recipients":{"items":{"$ref":"#/components/schemas/FirearmAlertRecipient"},"type":"array","title":"Recipients"},"schedule":{"$ref":"#/components/schemas/FirearmAlertSchedule"},"sensitivity":{"$ref":"#/components/schemas/AlertSensitivityLevel"},"handgun_positive_stances":{"items":{"$ref":"#/components/schemas/HandgunStanceType"},"type":"array"},"push_notification_level":{"$ref":"#/components/schemas/PushNotificationLevel"}},"type":"object","title":"UpdateFirearmAlertRequest"},"CreateLocationPayload":{"properties":{"name":{"type":"string","maxLength":30,"minLength":1,"title":"name","description":"Display name for the location (max 30 characters)"},"address":{"type":"string","minLength":5,"title":"address","description":"Physical street address (min 5 characters)"},"timezone":{"type":"string","title":"timezone","description":"Timezone (IANA format, e.g., 'America/New_York')"}},"type":"object","required":["name","address"],"title":"CreateLocationPayload","description":"Request model for creating a location.","x-public-api-group":"locations"},"CreateCameraGroupResponse":{"properties":{"data":{"allOf":[{"$ref":"#/components/schemas/CameraGroup"}],"title":"Data","description":"The newly created camera group data"}},"type":"object","required":["data"],"title":"CreateCameraGroupResponse","description":"Response after successfully creating a camera group.","x-public-api-group":"camera-groups"},"MoveCameraPayload":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"},"target_nvr":{"type":"string","title":"target_nvr","description":"Name of the NVR to move the camera to"}},"type":"object","required":["mac_address","target_nvr"],"title":"MoveCameraPayload","description":"Request model for moving a camera to a different NVR.","x-public-api-group":"cameras"},"LocationList":{"properties":{"results":{"items":{"$ref":"#/components/schemas/Location"},"type":"array","title":"Location","description":"List of locations"},"has_more":{"type":"boolean","title":"Has More","description":"True if additional pages are available"}},"type":"object","required":["results","has_more"],"title":"LocationList","description":"Paginated list of locations.","x-public-api-group":"locations"},"Location":{"properties":{"id":{"type":"integer","title":"Id","description":"Unique location identifier"},"name":{"type":"string","title":"Name","description":"Location display name"},"address":{"type":"string","title":"Address","description":"Physical street address"},"timezone":{"type":"string","title":"Timezone","description":"Timezone (IANA format, e.g., 'America/New_York')"}},"type":"object","required":["id","name","address"],"title":"Location","description":"A physical location where cameras and NVRs are installed.","x-public-api-group":"locations"},"CreateCameraGroupPayload":{"properties":{"name":{"type":"string","maxLength":30,"minLength":1,"title":"Name","description":"Display name for the new camera group (max 30 characters)"}},"type":"object","required":["name"],"title":"CreateCameraGroupPayload","description":"Request payload for creating a camera group.","x-public-api-group":"camera-groups"},"EventList":{"type":"object","required":["results","has_more"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"has_more":{"type":"boolean","description":"True if a further page beyond `page` exists."}},"x-public-api-group":"events"},"Event":{"type":"object","required":["id","timestamp","event_type","details"],"properties":{"id":{"type":"integer","format":"int64","description":"Stable identifier for the event."},"timestamp":{"type":"string","format":"date-time","description":"RFC3339 timestamp (UTC) at which the device emitted the event."},"event_type":{"type":"string","description":"Event type as emitted by the controller. See the `event_types`\nquery param on the list endpoint for the current enumeration.\n"},"door":{"$ref":"#/components/schemas/EventDoor"},"details":{"type":"object","additionalProperties":true,"description":"Event-type-specific payload (card_id, access_decision_reason,\ndpi_state, etc.). The set of keys depends on `event_type` and\nmay expand over time — clients should ignore unknown keys.\n`card_id` and `pin` are credential identifiers and are only\npresent when the API key's creator holds an admin role.\n"}},"x-public-api-group":"events"},"EventDoor":{"type":"object","required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"location_id":{"type":"integer","format":"int64","nullable":true}},"x-public-api-group":"events"},"FirearmAlertResponse":{"properties":{"firearm_alert_id":{"type":"integer","title":"Firearm Alert Id"},"name":{"type":"string","title":"Name"},"camera_mac_addresses":{"items":{"type":"string"},"type":"array","title":"Camera Mac Addresses"},"recipients":{"items":{"$ref":"#/components/schemas/FirearmAlertRecipient"},"type":"array","title":"Recipients"},"schedule":{"$ref":"#/components/schemas/FirearmAlertSchedule"},"sensitivity":{"$ref":"#/components/schemas/AlertSensitivityLevel"},"handgun_positive_stances":{"items":{"$ref":"#/components/schemas/HandgunStanceType"},"type":"array"},"push_notification_level":{"$ref":"#/components/schemas/PushNotificationLevel"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["firearm_alert_id","camera_mac_addresses","recipients","schedule","sensitivity","handgun_positive_stances","push_notification_level","created_at"],"title":"FirearmAlertResponse"},"NVRList":{"properties":{"results":{"items":{"$ref":"#/components/schemas/NVR"},"type":"array","title":"NVR","description":"List of NVRs"},"has_more":{"type":"boolean","title":"Has More","description":"True if additional pages are available"}},"type":"object","required":["results","has_more"],"title":"NVRList","description":"Paginated list of NVRs.","x-public-api-group":"nvrs"},"NVR":{"properties":{"name":{"type":"string","title":"Name","description":"NVR display name"},"location":{"allOf":[{"$ref":"#/components/schemas/Location"}],"title":"Location","description":"Location where the NVR is installed"},"camera_info":{"allOf":[{"$ref":"#/components/schemas/CameraInfo"}],"title":"Camera Info","description":"Camera capacity and status summary"},"retention_days":{"type":"integer","title":"Retention Days","description":"Video retention period in days"},"internet_status":{"allOf":[{"$ref":"#/components/schemas/InternetStatus"}],"title":"Internet Status","description":"Internet connectivity status"},"serial_number":{"type":"string","title":"Serial Number","description":"NVR serial number"},"network_info":{"allOf":[{"$ref":"#/components/schemas/NetworkInfo"}],"title":"Network Info","description":"Network configuration"},"status":{"allOf":[{"$ref":"#/components/schemas/Status"}],"title":"Status","description":"Current connectivity status"},"sku":{"type":"string","title":"Sku","description":"Stock Keeping Unit (SKU) identifier for the NVR hardware model"}},"type":"object","required":["name","camera_info","retention_days","status"],"title":"NVR","description":"Network Video Recorder details.","x-public-api-group":"nvrs"},"NetworkInfo":{"properties":{"ip_address":{"type":"string","title":"Ip Address","description":"NVR IP address on the local network"},"subnet_mask":{"type":"string","title":"Subnet Mask","description":"Network subnet mask"},"gateway":{"type":"string","title":"Gateway","description":"Default gateway IP address"}},"type":"object","title":"NetworkInfo","description":"Network configuration of the NVR."},"CameraInfo":{"properties":{"num_cameras_enabled":{"type":"integer","title":"Num Cameras Enabled","description":"Number of enabled cameras"},"num_cameras_disabled":{"type":"integer","title":"Num Cameras Disabled","description":"Number of disabled cameras"},"num_available_cameras_slots":{"type":"integer","title":"Num Available Cameras Slots","description":"Number of available camera slots"},"max_cameras_slots":{"type":"integer","title":"Max Cameras Slots","description":"Maximum camera slots supported"},"num_online_cameras":{"type":"integer","title":"Num Online Cameras","description":"Number of currently online cameras"}},"type":"object","required":["num_cameras_enabled","num_cameras_disabled","num_available_cameras_slots","max_cameras_slots","num_online_cameras"],"title":"CameraInfo","description":"Camera capacity and status summary for the NVR.","x-public-api-group":"cameras"},"Status":{"properties":{"last_seen_time":{"type":"string","title":"Last Seen Time","description":"Last connection timestamp (ISO 8601 format)"},"is_online":{"type":"boolean","title":"Is Online","description":"Whether the NVR is currently connected"}},"type":"object","required":["is_online"],"title":"Status","description":"Current connectivity status of the NVR."},"InternetStatus":{"properties":{"domain":{"type":"string","title":"Domain","description":"Domain/IP that was pinged"},"avg_ping_latency_ms":{"type":"number","title":"Avg Ping Latency Ms","description":"Average ping latency in milliseconds"},"packet_loss":{"type":"number","title":"Packet Loss","description":"Packet loss percentage (0-100)"},"internet_speed":{"allOf":[{"$ref":"#/components/schemas/InternetSpeed"}],"title":"Internet Speed","description":"Internet speed test results"}},"type":"object","required":["domain"],"title":"InternetStatus","description":"Internet connectivity status and speed information."},"InternetSpeed":{"properties":{"download_speed_bps":{"type":"number","title":"Download Speed Bps","description":"Download speed in bits per second"},"upload_speed_bps":{"type":"number","title":"Upload Speed Bps","description":"Upload speed in bits per second"},"timestamp":{"type":"string","title":"Timestamp","description":"Timestamp of speed test result (ISO 8601 format)"}},"type":"object","required":["download_speed_bps","upload_speed_bps","timestamp"],"title":"InternetSpeed","description":"Internet speed test results."},"FirearmAlertFeedbackResponse":{"properties":{"alert_event_id":{"type":"integer","title":"Alert Event Id","description":"ID of the firearm alert event"},"rating":{"type":"number","title":"Rating","description":"Current feedback score from 0.0 to 1.0, or null if no feedback has been submitted yet."},"feedback_reason":{"allOf":[{"$ref":"#/components/schemas/AlertFeedbackReason"}],"description":"Structured reason for the feedback."},"feedback_comment":{"type":"string","title":"Feedback Comment","description":"Free-text comment left with the feedback."},"user_id":{"type":"integer","title":"User Id","description":"ID of the user whose API key last submitted feedback."},"timestamp":{"type":"string","format":"date-time","title":"Timestamp","description":"When the feedback was last submitted (UTC)."}},"type":"object","required":["alert_event_id"],"title":"FirearmAlertFeedbackResponse","description":"Current feedback for a firearm alert (organization-wide, last write wins)."},"SpeedTestFailureResponse":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"SpeedTestFailureResponse","description":"Failure response for speed test request."},"ErrorDetails":{"properties":{"code":{"allOf":[{"$ref":"#/components/schemas/ErrorCode"}],"description":"Error code"},"message":{"type":"string","title":"Message","description":"Error message"},"field":{"type":"string","title":"Field","description":"Error field"},"docs":{"type":"string","title":"Docs","description":"Error docs"}},"type":"object","required":["code","message"],"title":"ErrorDetails","description":"Error message"},"ErrorCode":{"type":"string","enum":["VALIDATION_ERROR","BATCH_SIZE_EXCEEDED","NO_CAMERAS_PROVIDED","PARTIAL_OPERATIONS_NOT_SUPPORTED","CAMERA_ALREADY_EXISTS","CAMERA_NOT_FOUND","NO_LICENSES_AVAILABLE","INVALID_MAC_ADDRESS","INVALID_IP_ADDRESS","DUPLICATE_MAC_ADDRESSES","NVR_CAPACITY_EXCEEDED","NVR_NOT_FOUND","INVALID_NVR_NAME","LOCATION_NOT_FOUND","NVR_REGISTRATION_FAILED","NVR_ALREADY_REGISTERED","PING_REQUEST_TIMEOUT","PING_REQUEST_FAILED","SPEEDTEST_REQUEST_TIMEOUT","SPEEDTEST_REQUEST_FAILED","NO_NVRS_PROVIDED","DUPLICATE_NVR_NAMES","LOCATION_ACCESS_DENIED","LOCATION_NAME_ALREADY_EXISTS","LOCATION_CREATION_FAILED","NO_LOCATIONS_PROVIDED","DUPLICATE_LOCATION_NAMES","ALERT_EVENT_NOT_FOUND","ALERT_NO_VIDEO_AVAILABLE","ALERT_CAMERA_NOT_FOUND","ALERT_CLIP_GENERATION_FAILED","ALERT_NOT_FIREARM","FIREARM_ALERT_NOT_FOUND","INVALID_RECIPIENT_EMAILS","PREMIUM_ALERT_CAMERA_LIMIT_EXCEEDED","CAMERA_ALREADY_IN_USE","ACCESS_EVENT_NOT_FOUND","ACCESS_EVENT_NO_VIDEO_AVAILABLE","ACCESS_EVENT_CLIP_GENERATION_FAILED","CAMERA_GROUP_NOT_FOUND","CAMERA_GROUP_NAME_ALREADY_EXISTS","CAMERA_GROUP_CREATION_FAILED","NO_GROUPS_PROVIDED","DUPLICATE_GROUP_NAMES","INVALID_GROUP_ID","DUPLICATE_GROUP_IDS"],"title":"ErrorCode","description":"Centralized error codes for Developer API endpoints."},"DoorList":{"type":"object","required":["results","has_more"],"properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/Door"}},"has_more":{"type":"boolean","description":"Reserved for future pagination; always false today. The endpoint\ncurrently returns every door the caller can see.\n"}},"x-public-api-group":"doors"},"Door":{"type":"object","required":["id","name","location_id","is_online"],"properties":{"id":{"type":"integer","format":"int64","description":"Stable internal identifier of the door."},"name":{"type":"string","description":"Human-readable door name as set in the Coram app."},"location_id":{"type":"integer","format":"int64","description":"ID of the location this door belongs to. Zero when the door has no assigned location."},"is_online":{"type":"boolean","description":"True if the door's controller board has reported a heartbeat\nwithin the last 15 seconds.\n"}},"x-public-api-group":"doors"},"RegisterNVRResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the registration operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/RegisterNVRSuccessResult"},{"$ref":"#/components/schemas/RegisterNVRErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/RegisterNVRSuccessResult","error":"#/components/schemas/RegisterNVRErrorResult"}}},"type":"array","title":"RegisterNVRResult","description":"List of NVR registration results"}},"type":"object","required":["summary"],"title":"RegisterNVRResponse","description":"Response model for bulk NVR registration.","x-public-api-group":"nvrs"},"Summary":{"properties":{"total":{"type":"integer","title":"Total","description":"Total number of operations in request"},"success_count":{"type":"integer","minimum":0.0,"title":"Success Count","description":"Number of successful operations"},"failure_count":{"type":"integer","minimum":0.0,"title":"Failure Count","description":"Number of failed operations"}},"type":"object","required":["total","success_count","failure_count"],"title":"Summary","description":"Summary of operation."},"RegisterNVRErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"RegisterNVRErrorResult","description":"Error result for NVR registration.","x-public-api-group":"nvrs"},"RegisterNVRSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/RegisterNVRSuccessData"}],"title":"RegisterNVRSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"RegisterNVRSuccessResult","description":"Success result for NVR registration.","x-public-api-group":"nvrs"},"RegisterNVRSuccessData":{"properties":{"nvr":{"type":"string","title":"nvr","description":"NVR display name"}},"type":"object","required":["nvr"],"title":"RegisterNVRSuccessData","description":"Data returned for a successfully registered NVR.","x-public-api-group":"nvrs"},"AlertDetailsResponse":{"properties":{"alert_event_id":{"type":"integer","title":"Alert Event Id","description":"ID of the alert event"},"alert_time":{"type":"string","format":"date-time","title":"Alert Time","description":"When the alert was triggered (UTC)"},"camera_mac_address":{"type":"string","title":"Camera Mac Address","description":"MAC address of the camera that captured the alert"},"ai":{"allOf":[{"$ref":"#/components/schemas/AlertAIDescriptionInfo"}],"title":"Ai","description":"AI-generated description of the alert"},"clip":{"allOf":[{"$ref":"#/components/schemas/AlertClipInfo"}],"title":"Clip","description":"Video clip associated with the alert, if one exists. Omitted entirely when no clip is being produced for this alert. When present, `clip.status` indicates whether the download `mp4_url` is `ready`, `pending`, or `failed`."}},"type":"object","required":["alert_event_id","alert_time","camera_mac_address","ai"],"title":"AlertDetailsResponse","description":"Response for the alert details endpoint.\n\nReturns metadata about an alert event, including the rich natural-language\ndescription generated by Coram's vision language model. The description is\nproduced asynchronously, so `ai.status` indicates whether it is ready yet,\nstill being generated, or definitively missing. The `camera_alert_details`\nwebhook delivers a superset of this payload as its `data`: every field\nbelow, plus two server-measured latency fields that only a pushed event\ncan report."},"AlertAIDescriptionInfo":{"properties":{"status":{"allOf":[{"$ref":"#/components/schemas/AIDescriptionStatus"}],"description":"Whether the AI description is `ready` (at least one of `short_description` / `long_description` is present), `pending` (still being generated — poll again shortly; never sent by the webhook, which only fires once the description has settled), or `failed` (generation failed or was skipped; no description will arrive)."},"short_description":{"type":"string","title":"Short Description","description":"Short AI-generated summary of what the camera observed. Omitted when `status` is `pending` or `failed`. May also be omitted when `status` is `ready` if only the long description was generated — consumers should treat the field's absence as equivalent to null and check both descriptions before using either."},"long_description":{"type":"string","title":"Long Description","description":"Long-form AI-generated description of what the camera observed. Omitted when `status` is `pending` or `failed`. May also be omitted when `status` is `ready` if only the short description was generated — consumers should treat the field's absence as equivalent to null and check both descriptions before using either."}},"type":"object","required":["status"],"title":"AlertAIDescriptionInfo","description":"AI-generated description of an alert, grouped the way `clip` is.\n\nShared by the alert details endpoint and the `camera_alert_details`\nwebhook so both carry the identical payload shape."},"AIDescriptionStatus":{"type":"string","enum":["ready","pending","failed"],"title":"AIDescriptionStatus","description":"Status of the AI-generated alert description.\n\nThe description is produced asynchronously by a vision language model after\nthe alert is emitted, so consumers polling this endpoint right after a\nwebhook may see `pending` for a few seconds before it transitions to\n`ready` or `failed`."},"AlertClipInfo":{"properties":{"status":{"allOf":[{"$ref":"#/components/schemas/ClipStatus"}],"description":"Whether the clip is `ready` (`mp4_url` present and downloadable), `pending` (still being uploaded — poll again shortly), or `failed` (upload failed or was skipped)."},"mp4_url":{"type":"string","title":"Mp4 Url","description":"Direct download URL for the MP4 video clip. No authentication required. Present only when `status` is `ready`; valid until `expires_at`."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"When the `mp4_url` expires (UTC). Present only when `status` is `ready`."}},"type":"object","required":["status"],"title":"AlertClipInfo","description":"Video clip associated with an alert.\n\nPresent only when a clip exists for the alert (today: firearm alerts with\nauto clip download enabled). The `mp4_url`/`expires_at` fields are populated\nonly once `status` is `ready`."},"ClipStatus":{"type":"string","enum":["ready","pending","failed"],"title":"ClipStatus","description":"Status of the video clip associated with an alert.\n\nMirrors `AIDescriptionStatus`: the clip is produced asynchronously (the\nedge device uploads it after the alert fires), so consumers polling this\nendpoint right after a webhook may see `pending` before it becomes `ready`."},"ReunificationImportInput":{"type":"object","required":["schools","students","teachers","sections","school_location_mappings"],"description":"`schools`, `students`, `teachers` and `sections` must be present, and may\nbe empty arrays. Sending one as `null` is rejected the same way as leaving\nit out, because both would wipe everything that key covers. Only `staff`\nmay be left out entirely.\n\nPhone numbers are normalized to E.164 against the deployment's own\nregion, currently the United States. A number that cannot be parsed is\nstored as null rather than rejected, so include a country code.\n\nUnrecognized keys are dropped without error, so a misspelled field name\nfails silently.\n","properties":{"schools":{"type":"array","items":{"$ref":"#/components/schemas/ReunificationSchool"}},"students":{"type":"array","items":{"$ref":"#/components/schemas/ReunificationStudent"}},"teachers":{"type":"array","items":{"$ref":"#/components/schemas/ReunificationTeacher"}},"staff":{"type":"array","items":{"$ref":"#/components/schemas/ReunificationStaff"}},"sections":{"type":"array","description":"An empty array removes every student to teacher pairing on the roster.","items":{"$ref":"#/components/schemas/ReunificationSection"}},"school_location_mappings":{"type":"object","additionalProperties":{"type":"integer","format":"int32"},"description":"Maps your school ids to Coram location IDs. Must be present and non-empty. Give every school an entry, because a school without one is imported with no location and everyone attached only to it loses access. Two schools may not point at the same location, and a location that does not exist in your organization aborts the import after it has already returned 202. The location id in the example is illustrative, so look up your real ones with `GET /v1/locations` before your first call."}}},"ReunificationStaff":{"type":"object","required":["staff_id","name","school_ids"],"description":"Omitting the whole staff array deletes every staff member on the roster.","properties":{"staff_id":{"type":"string","description":"A blank value drops the staff member silently."},"name":{"type":"string"},"sis_id":{"type":"string"},"email":{"type":"string","description":"Without a valid one the staff member is still imported but gets no Coram account and loses any access an earlier import gave them."},"phone_number":{"type":"string"},"title":{"type":"string","description":"Stored and displayed only."},"department":{"type":"string","description":"Stored and displayed only."},"roles":{"type":"array","description":"Free text, not a fixed set. Displayed in Coram and used for nothing else.","items":{"type":"string"}},"school_ids":{"type":"array","description":"Unlike students and teachers, repeated ids here are removed rather than aborting the import. Staff have no primary school.","items":{"type":"string"}}}},"ReunificationSchool":{"type":"object","required":["school_id","name"],"properties":{"school_id":{"type":"string","description":"Your identifier for the school, referenced by students, teachers, staff and school_location_mappings. A blank value drops the school silently, and with it every reference to it. Do not send the same school_id twice."},"name":{"type":"string","description":"Not validated. A blank name is accepted and shows as a nameless school in Coram."},"sis_id":{"type":"string","description":"Stored and displayed only. Nothing matches or de-duplicates on it."},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}}},"ReunificationStudent":{"type":"object","required":["student_id","name","school_ids"],"properties":{"student_id":{"type":"string","description":"Your identifier for the student. A blank value drops the student silently. Stored as the student's card number and visible in Coram, so do not put anything sensitive here."},"name":{"type":"string","description":"Not validated. A blank name is accepted and stored empty."},"sis_id":{"type":"string","description":"Stored and displayed only. Nothing matches or de-duplicates on it."},"email":{"type":"string","description":"The identity Coram uses for this person. They are still imported and still counted without a valid one, but they get no Coram account, appear in neither user counter, and lose any access an earlier import gave them. Two people sharing an email collapse into one Coram account."},"phone_number":{"type":"string"},"grade":{"type":"string"},"school_ids":{"type":"array","description":"Schools the student belongs to, in order. The first one that resolves becomes the primary school, so a typo in the first position silently promotes the next. Repeating that first id is ignored, but repeating any other aborts the whole import. A student whose school_ids all fail to resolve is imported with no location access.","items":{"type":"string"}},"guardians":{"type":"array","items":{"$ref":"#/components/schemas/ReunificationGuardian"}}}},"ReunificationGuardian":{"type":"object","required":["name"],"description":"Guardians are contacts only and never receive a Coram account. Two guardians sharing an email address, or a phone number when there is no email, are merged into one guardian across the whole payload.","properties":{"name":{"type":"string","description":"A guardian with a blank name is discarded along with their contact details."},"email":{"type":"string"},"phone":{"type":"string"},"relationship":{"type":"string","description":"Free text. Stored as \"Other\" when omitted or blank."}}},"ReunificationTeacher":{"type":"object","required":["teacher_id","name","school_ids"],"properties":{"teacher_id":{"type":"string","description":"A blank value drops the teacher silently."},"name":{"type":"string"},"sis_id":{"type":"string"},"email":{"type":"string","description":"Without a valid one the teacher is still imported but gets no Coram account and loses any access an earlier import gave them."},"phone_number":{"type":"string"},"school_ids":{"type":"array","description":"The first one that resolves becomes the primary school. Repeating that first id is ignored, but repeating any other aborts the whole import.","items":{"type":"string"}}}},"ReunificationSection":{"type":"object","required":["section_id"],"description":"A class that pairs students with teachers. Sections build those pairings and are not stored as records you can look up later.","properties":{"section_id":{"type":"string","description":"Must not be blank or the section is dropped. Never stored, so nothing can look it up."},"school_id":{"type":"string","description":"Accepted and ignored. Sections take their schools from the people in them."},"teacher_ids":{"type":"array","description":"A teacher id that does not resolve drops that teacher's pairings in this section without error.","items":{"type":"string"}},"student_ids":{"type":"array","description":"A student id that does not resolve drops that student's pairings in this section without error.","items":{"type":"string"}}}},"RegisterNVRPayload":{"properties":{"nvr":{"type":"string","minLength":1,"title":"nvr","description":"Display name for the NVR"},"location_id":{"type":"integer","exclusiveMinimum":0.0,"title":"location_id","description":"Location ID to assign the NVR to"}},"type":"object","required":["nvr","location_id"],"title":"RegisterNVRPayload","description":"Request model for registering an NVR.","x-public-api-group":"nvrs"},"RegisterCameraResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the registration operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/RegisterCameraSuccessResult"},{"$ref":"#/components/schemas/RegisterCameraErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/RegisterCameraSuccessResult","error":"#/components/schemas/RegisterCameraErrorResult"}}},"type":"array","title":"RegisterCameraResult","description":"List of camera registration results"}},"type":"object","required":["summary"],"title":"RegisterCameraResponse","description":"Success response model for camera registration.","x-public-api-group":"cameras"},"RegisterCameraSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/RegisterCameraSuccessData"}],"title":"RegisterCameraSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"RegisterCameraSuccessResult","description":"Success result for camera registration.","x-public-api-group":"cameras"},"RegisterCameraSuccessData":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"},"nvr":{"allOf":[{"$ref":"#/components/schemas/CameraNVRInfo"}],"title":"Nvr","description":"NVR the camera was assigned to"}},"type":"object","required":["mac_address","nvr"],"title":"RegisterCameraSuccessData","description":"Data returned for a successfully registered camera.","x-public-api-group":"cameras"},"RegisterCameraErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"RegisterCameraErrorResult","description":"Error result for camera registration.","x-public-api-group":"cameras"},"UpdateCameraPayload":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"},"name":{"type":"string","title":"name","description":"New display name"},"ip":{"type":"string","title":"ip","description":"New camera IP address. Applied to every head of a multi-head camera. On NVRs with automatic network scan enabled, discovery overrides this value if it does not match the IP the camera is actually reachable at."},"credentials":{"allOf":[{"$ref":"#/components/schemas/Credentials"}],"title":"credentials","description":"Camera credentials"},"enforced_rtsp_url":{"type":"string","title":"Enforced Rtsp Url","description":"Custom RTSP URL to override auto-discovery. Provide the path component beginning with '/' (e.g. '/Streaming/Channels/101'); it is combined with the camera's IP, port and credentials at connection time. A full 'rtsp://host:port/path' URL is also accepted and reduced to its path."},"feature_flags":{"allOf":[{"$ref":"#/components/schemas/CameraFeatureFlag"}],"title":"CameraFeatureFlag","description":"Camera feature flags"},"group_ids":{"items":{"type":"integer"},"type":"array","title":"group_ids","description":"List of group IDs to assign (empty list resets to default group)"}},"type":"object","required":["mac_address"],"title":"UpdateCameraPayload","description":"Request model for updating a single camera in bulk operation.","x-public-api-group":"cameras"},"CameraFeatureFlag":{"properties":{"is_audio_enabled":{"type":"boolean","title":"Is Audio Enabled","description":"Whether audio recording is enabled"},"is_license_plate_detection_enabled":{"type":"boolean","title":"Is License Plate Detection Enabled","description":"Whether license plate detection is enabled"},"auto_tracking":{"type":"boolean","title":"Auto Tracking","description":"Whether PTZ auto-tracking is enabled"}},"type":"object","title":"CameraFeatureFlag","description":"Feature flags to update on a camera.","x-public-api-group":"cameras"},"Credentials":{"properties":{"username":{"type":"string","title":"Username","description":"Camera login username"},"password":{"type":"string","title":"Password","description":"Camera login password"}},"type":"object","required":["username","password"],"title":"Credentials","description":"Authentication credentials for camera access.","x-public-api-group":"cameras"},"ErrorResponse":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"Error","description":"Error details"}},"type":"object","required":["error"],"title":"ErrorResponse","description":"Error response model."},"PingSuccessResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/PingSuccessData"}],"title":"PingSuccessData","description":"Ping test results"}},"type":"object","required":["data"],"title":"PingSuccessResponse","description":"Success response for ping request."},"PingSuccessData":{"properties":{"timestamp":{"type":"string","title":"Timestamp","description":"Timestamp of ping result (ISO 8601 format)"},"domain":{"type":"string","title":"Domain","description":"Domain/IP that was pinged"},"avg_ping_latency_ms":{"type":"number","title":"Avg Ping Latency Ms","description":"Average ping latency in milliseconds"},"packet_loss":{"type":"number","title":"Packet Loss","description":"Packet loss percentage (0-100)"}},"type":"object","required":["timestamp","domain","avg_ping_latency_ms","packet_loss"],"title":"PingSuccessData","description":"Ping test results."},"DeleteCameraResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the delete operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/DeleteCameraSuccessResult"},{"$ref":"#/components/schemas/DeleteCameraErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/DeleteCameraSuccessResult","error":"#/components/schemas/DeleteCameraErrorResult"}}},"type":"array","title":"DeleteCameraResult","description":"List of camera delete results"}},"type":"object","required":["summary"],"title":"DeleteCameraResponse","description":"Success response model for camera delete.","x-public-api-group":"cameras"},"DeleteCameraErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"DeleteCameraErrorResult","description":"Error result for camera delete.","x-public-api-group":"cameras"},"DeleteCameraSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/DeleteCameraSuccessData"}],"title":"DeleteCameraSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"DeleteCameraSuccessResult","description":"Success result for camera delete.","x-public-api-group":"cameras"},"DeleteCameraSuccessData":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"}},"type":"object","required":["mac_address"],"title":"DeleteCameraSuccessData","description":"Data returned for a successfully deleted camera.","x-public-api-group":"cameras"},"MoveCameraResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the move operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/MoveCameraSuccessResult"},{"$ref":"#/components/schemas/MoveCameraErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/MoveCameraSuccessResult","error":"#/components/schemas/MoveCameraErrorResult"}}},"type":"array","title":"MoveCameraResult","description":"List of camera move results"}},"type":"object","required":["summary"],"title":"MoveCameraResponse","description":"Response model for bulk camera move.","x-public-api-group":"cameras"},"MoveCameraSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/MoveCameraSuccessData"}],"title":"MoveCameraSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"MoveCameraSuccessResult","description":"Success result for camera move.","x-public-api-group":"cameras"},"MoveCameraSuccessData":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"},"target_nvr":{"type":"string","title":"target_nvr","description":"Name of the NVR the camera was moved to"}},"type":"object","required":["mac_address","target_nvr"],"title":"MoveCameraSuccessData","description":"Data returned for a successfully moved camera.","x-public-api-group":"cameras"},"MoveCameraErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"MoveCameraErrorResult","description":"Error result for camera move.","x-public-api-group":"cameras"},"UpdateCameraResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the update operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/UpdateCameraSuccessResult"},{"$ref":"#/components/schemas/UpdateCameraErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/UpdateCameraSuccessResult","error":"#/components/schemas/UpdateCameraErrorResult"}}},"type":"array","title":"UpdateCameraResult","description":"List of camera update results"}},"type":"object","required":["summary"],"title":"UpdateCameraResponse","description":"Response model for bulk camera update.","x-public-api-group":"cameras"},"UpdateCameraErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"UpdateCameraErrorResult","description":"Error result for camera update.","x-public-api-group":"cameras"},"UpdateCameraSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/UpdateCameraSuccessData"}],"title":"UpdateCameraSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"UpdateCameraSuccessResult","description":"Success result for camera update.","x-public-api-group":"cameras"},"UpdateCameraSuccessData":{"properties":{"mac_address":{"type":"string","title":"Mac Address","description":"Camera MAC address"},"name":{"type":"string","title":"name","description":"Camera display name"},"ip":{"type":"string","title":"ip","description":"Updated camera IP address"},"credentials":{"allOf":[{"$ref":"#/components/schemas/Credentials"}],"title":"Credentials","description":"Camera credentials"},"enforced_rtsp_url":{"type":"string","title":"enforced_rtsp_url","description":"Custom RTSP URL"},"feature_flags":{"allOf":[{"$ref":"#/components/schemas/CameraFeatureFlag"}],"title":"CameraFeatureFlag","description":"Camera feature flags"},"group_ids":{"items":{"type":"integer"},"type":"array","title":"group_ids","description":"Updated group IDs"}},"type":"object","required":["mac_address"],"title":"UpdateCameraSuccessData","description":"Data returned for a successfully updated camera.","x-public-api-group":"cameras"},"RegisterCameraRequest":{"properties":{"cameras":{"items":{"$ref":"#/components/schemas/RegisterCameraPayload"},"type":"array","title":"RegisterCameraPayload","description":"List of cameras to register"},"location_id":{"type":"integer","title":"location_id","description":"Location ID to assign cameras to"}},"type":"object","required":["cameras","location_id"],"title":"RegisterCameraRequest","description":"Request model for bulk camera registration.","x-public-api-group":"cameras"},"RegisterCameraPayload":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address. If omitted, a synthetic unique identifier is generated server-side."},"name":{"type":"string","minLength":1,"title":"name","description":"Camera display name"},"ip":{"type":"string","title":"ip","description":"Camera IP address"},"vendor":{"type":"string","title":"vendor","description":"Camera manufacturer. Defaults to 'Unknown' when not provided."},"rtsp_port":{"type":"integer","maximum":65535.0,"minimum":1.0,"title":"rtsp_port","description":"RTSP port number","default":554},"rtsp_url_path":{"type":"string","title":"rtsp_url_path","description":"Path component of the RTSP URL (e.g. '/Streaming/Channels/101'). Combined with ip, rtsp_port and credentials at runtime to form the full URL. Defaults to '/unknown' to match the placeholder used elsewhere in the system; callers should set this when the actual path is known.","default":"/unknown"},"credentials":{"allOf":[{"$ref":"#/components/schemas/Credentials"}],"title":"Credentials","description":"Camera credentials"},"nvrs":{"items":{"type":"string","minLength":1},"type":"array","minLength":1,"title":"nvrs","description":"List of NVR display names from which the camera will be assigned to one. The system selects an available NVR from this list during registration."}},"type":"object","required":["name","ip","nvrs"],"title":"RegisterCameraPayload","description":"Request model for registering a single camera in bulk operation.","x-public-api-group":"cameras"},"PingFailureResponse":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"PingFailureResponse","description":"Failure response for ping request."},"EventClipResponse":{"properties":{"preview_url":{"type":"string","title":"Preview Url","description":"Web player page URL for viewing the clip in a browser. No authentication required. Valid until expires_at."},"mp4_url":{"type":"string","title":"Mp4 Url","description":"Direct download URL for the MP4 video clip. No authentication required. Valid until expires_at."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"When the URLs expire (UTC)"},"clip_start_time":{"type":"string","format":"date-time","title":"Clip Start Time","description":"Start time of the video clip (UTC)"},"clip_end_time":{"type":"string","format":"date-time","title":"Clip End Time","description":"End time of the video clip (UTC)"},"event_id":{"type":"integer","title":"Event Id","description":"ID of the access control event"},"camera_mac_address":{"type":"string","title":"Camera Mac Address","description":"MAC address of the camera that recorded the clip (door's primary camera)"}},"type":"object","required":["preview_url","mp4_url","expires_at","clip_start_time","clip_end_time","event_id","camera_mac_address"],"title":"EventClipResponse","description":"Response for the access control event clip download endpoint.","x-public-api-group":"events"},"UnlockResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["success"],"description":"Always `success` on a 200; non-200 responses use AuthErrorResponse."}},"x-public-api-group":"doors"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"CreateLocationResponse":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"summary":{"allOf":[{"$ref":"#/components/schemas/Summary"}],"title":"Summary","description":"Summary of the creation operation"},"results":{"items":{"oneOf":[{"$ref":"#/components/schemas/CreateLocationSuccessResult"},{"$ref":"#/components/schemas/CreateLocationErrorResult"}],"discriminator":{"propertyName":"status","mapping":{"success":"#/components/schemas/CreateLocationSuccessResult","error":"#/components/schemas/CreateLocationErrorResult"}}},"type":"array","title":"CreateLocationResult","description":"List of location creation results"}},"type":"object","required":["summary"],"title":"CreateLocationResponse","description":"Response model for bulk location creation.","x-public-api-group":"locations"},"CreateLocationErrorResult":{"properties":{"status":{"type":"string","enum":["error"],"title":"Status","default":"error"},"error":{"allOf":[{"$ref":"#/components/schemas/ErrorDetails"}],"title":"ErrorDetails","description":"Error details"}},"type":"object","required":["error"],"title":"CreateLocationErrorResult","description":"Error result for location creation.","x-public-api-group":"locations"},"CreateLocationSuccessResult":{"properties":{"status":{"type":"string","enum":["success"],"title":"Status","default":"success"},"data":{"allOf":[{"$ref":"#/components/schemas/CreateLocationSuccessData"}],"title":"CreateLocationSuccessData","description":"Success data"}},"type":"object","required":["data"],"title":"CreateLocationSuccessResult","description":"Success result for location creation.","x-public-api-group":"locations"},"CreateLocationSuccessData":{"properties":{"id":{"type":"integer","title":"id","description":"Unique identifier of the created location"}},"type":"object","required":["id"],"title":"CreateLocationSuccessData","description":"Data returned for a successfully created location.","x-public-api-group":"locations"},"AuthErrorResponse":{"type":"object","required":["detail"],"properties":{"detail":{"type":"string","description":"Human-readable error description. Matches the shape FastAPI uses\nfor `HTTPException` responses, so existing code that parses the\nPython developer-api's auth errors works unchanged.\n"}}},"DeleteCameraPayload":{"properties":{"mac_address":{"type":"string","title":"mac_address","description":"Camera MAC address"}},"type":"object","required":["mac_address"],"title":"DeleteCameraPayload","description":"Request model for deleting a single camera in bulk operation.","x-public-api-group":"cameras"},"ReunificationImportStatus":{"type":"object","required":["id","status"],"description":"Progress report for your most recent import. Counters that are zero are left out of the response, so treat an absent field as zero. Every counter resets when a new import starts.","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["in_progress","completed","failed"]},"synced_schools":{"type":"integer","format":"int32","default":0},"total_schools":{"type":"integer","format":"int32","default":0},"synced_students":{"type":"integer","format":"int32","default":0},"total_students":{"type":"integer","format":"int32","default":0},"synced_teachers":{"type":"integer","format":"int32","default":0},"total_teachers":{"type":"integer","format":"int32","default":0},"synced_staff":{"type":"integer","format":"int32","default":0},"total_staff":{"type":"integer","format":"int32","default":0},"synced_guardians":{"type":"integer","format":"int32","default":0},"total_guardians":{"type":"integer","format":"int32","default":0,"description":"Counted after the merge described on the guardian object, so it is usually lower than the number of guardian entries you sent."},"succeeded_users":{"type":"integer","format":"int32","default":0,"description":"Coram accounts created or updated. Rises in the background after status becomes completed. Guardians are never included, because they get no account."},"failed_users":{"type":"integer","format":"int32","default":0,"description":"Accounts that could not be created, usually a seat limit or an Auth0 rejection. Zero does not mean everyone succeeded, since anyone without a valid email is absent from both figures."},"error_message":{"type":"string"},"started_at":{"type":"string","format":"date-time","description":"Carries a real time only while the import is in progress. Once it reaches completed or failed the field is still returned but reset to 0001-01-01T00:00:00Z, so record the start time yourself if you need it."},"completed_at":{"type":"string","format":"date-time","description":"Returned as 0001-01-01T00:00:00Z until the import finishes."}}},"WebhookEvent":{"type":"object","description":"Envelope every JSON webhook delivery shares. The per-event payload is\ncarried under `data`. See the Webhooks section of the introduction for\nwhich payload each event type carries.\n","required":["event_type","timestamp","tenant","data"],"properties":{"event_type":{"$ref":"#/components/schemas/WebhookEventType"},"timestamp":{"type":"string","format":"date-time","description":"When the event was emitted (UTC)."},"test":{"type":"boolean","default":false,"description":"True only for deliveries triggered by the Test button in the Coram app. Never true for real events."},"tenant":{"type":"string","description":"Identifier of the Coram organization the event belongs to."},"text":{"type":"string","nullable":true,"description":"Human-readable summary of the event. Makes the payload directly consumable by a Slack incoming webhook URL."},"data":{"description":"Event payload. Its shape is determined by `event_type`.","oneOf":[{"$ref":"#/components/schemas/WebhookCameraAlertData"},{"$ref":"#/components/schemas/WebhookCameraAlertDetailsData"},{"$ref":"#/components/schemas/WebhookDeviceStatusData"},{"$ref":"#/components/schemas/WebhookEmergencyEventData"},{"$ref":"#/components/schemas/WebhookAccessControlEventData"}]}}},"WebhookEventType":{"type":"string","description":"Event types a webhook can be registered for.","enum":["camera_alert","camera_alert_details","device_status","emergency_activated","emergency_resolved","door_unlocked","access_denied","door_forced_open","door_held_open"]},"WebhookCameraAlertData":{"type":"object","description":"Payload of the `camera_alert` event.","required":["alert_name","camera_name","location"],"properties":{"alert_name":{"type":"string","description":"Name of the alert configuration."},"camera_name":{"type":"string","description":"Name of the camera."},"location":{"type":"string","description":"Human-readable location name."},"alert_type":{"type":"string","nullable":true,"description":"Human-readable alert type, e.g. 'Trespasser', 'Firearm'."},"camera_mac_address":{"type":"string","nullable":true,"description":"MAC address of the camera."},"organization_name":{"type":"string","nullable":true,"description":"Name of the organization."},"alert_event_id":{"type":"integer","nullable":true,"description":"ID of the alert event. Use it to correlate with the `camera_alert_details` event and to retrieve the alert via `GET /v1/alerts/{alert_event_id}/details`."},"latency_ms":{"type":"integer","nullable":true,"description":"Server-measured milliseconds from alert trigger to right before the webhook HTTP request is issued."},"invocation_latency_ms":{"type":"integer","nullable":true,"description":"Server-measured milliseconds spent in Coram's webhook pipeline for this alert. A subset of `latency_ms` covering only the webhook path."}}},"WebhookCameraAlertDetailsData":{"description":"Payload of the `camera_alert_details` event. The alert details endpoint's response plus two server-measured latency fields that only a pushed event can report.","allOf":[{"$ref":"#/components/schemas/AlertDetailsResponse"},{"type":"object","properties":{"latency_ms":{"type":"integer","nullable":true,"description":"Server-measured milliseconds from the alert trigger (`alert_time`) to right before the webhook HTTP request is issued. This event waits for the AI description and, for firearm alerts, the video clip to settle, so the value is dominated by that wait."},"invocation_latency_ms":{"type":"integer","nullable":true,"description":"Server-measured milliseconds Coram spent on the work needed to fire this webhook, up to right before the HTTP request is issued. A subset of `latency_ms` excluding the wait for the description and clip to settle."}}}]},"WebhookDeviceStatusData":{"type":"object","description":"Payload of the `device_status` event.","required":["device_status","device_type","devices"],"properties":{"device_status":{"type":"string","enum":["offline","online"],"description":"The status the devices changed to."},"device_type":{"type":"string","enum":["camera","coram_point"],"description":"Type of device."},"devices":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/WebhookDevice"},"description":"Devices affected by this status change."}}},"WebhookDevice":{"type":"object","description":"A single device in a `device_status` event.","required":["name","location"],"properties":{"name":{"type":"string","description":"Name of the device."},"location":{"type":"string","description":"Human-readable location name."}}},"WebhookEmergencyEventData":{"type":"object","description":"Payload of the `emergency_activated` and `emergency_resolved` events. Only delivered when the webhook's response format is set to JSON. The default is CAP 1.2 XML.","required":["configuration_name","location_name","event_type"],"properties":{"configuration_name":{"type":"string","description":"Name of the emergency configuration."},"location_name":{"type":"string","description":"Location where the emergency occurred."},"event_type":{"$ref":"#/components/schemas/WebhookEventType"}}},"WebhookAccessControlEventData":{"type":"object","description":"Payload shared by the `door_unlocked`, `access_denied`, `door_forced_open` and `door_held_open` events. Per-event extras are optional fields. `actor_*` appears on `door_unlocked` and `access_denied`, and `reason` on `access_denied`.","required":["event_id","door_id","door_name"],"properties":{"event_id":{"type":"integer","description":"ID of the underlying access-control event. Use it to fetch the event's video clip via `GET /v1/access-control/events/{event_id}/clip`."},"door_id":{"type":"integer","description":"ID of the door the event was emitted on."},"door_name":{"type":"string","description":"Display name of the door."},"location_id":{"type":"integer","nullable":true,"description":"ID of the location the door belongs to, if any."},"actor_user_id":{"type":"integer","nullable":true,"description":"Coram user_id that initiated the action, if known."},"actor_card_id":{"type":"integer","nullable":true,"description":"Card credential id used, if the actor was a card holder."},"actor_source":{"type":"string","nullable":true,"enum":["remote","card_reader"],"description":"How the action was initiated."},"reason":{"type":"string","nullable":true,"description":"Controller's denial reason for `access_denied` events (e.g. 'card_not_recognized', 'outside_schedule', 'card_disabled'). Null on other event types."}}}},"securitySchemes":{"X-Auth-Token":{"type":"apiKey","in":"header","name":"X-Auth-Token"}}},"security":[{"X-Auth-Token":[]}]}