Summary
The KC Safety Public REST API is a read-only HTTP API that lets an external system pull incident and reference data from your KC Safety account. This article is the reference: base URL, authentication, response envelope, rate limiting, errors, and every endpoint available today. For a step-by-step walkthrough of creating a key and making your first call, see How to Create API Keys and Read Data with the KC Safety Public REST API.
Getting started
About
The KC Safety Public REST API lets an external system — your own middleware, a data warehouse, a reporting tool, or another KnowledgeCity product — pull incident and reference data from your KC Safety account.
Requests are authenticated with an API key, scoped automatically to your account, and rate-limited according to your plan.
Read-only. The API exposes GET endpoints only. There is no create, update, or delete. To send data into KC Safety, use inbound email reporting (for incidents) or SCIM provisioning (for user accounts). To receive KC Safety events as they happen, use outbound event webhooks.Before calling the API, create an API key: Admin Panel → Settings → Integrations → API Keys. This requires the Super Admin role and a plan that includes API access.
The reference below is organised into resource groups, each listing the endpoints it exposes.
| Group | Endpoints |
|---|---|
| Departments | 1 |
| Incident types | 1 |
| Incidents | 2 |
| Locations | 1 |
| Users | 1 |
Base URL and versioning
| Environment | Base URL |
|---|---|
| Production | https://safety-api.knowledgecity.com/api/v1 |
All public endpoints live under /api/v1/public/.
Versioning
- Major version is set in the URL path, e.g.
/api/v1/public/.... - There is no minor-version request header. The API does not accept an
x-api-versionheader, and there is nothing to pin — unlike the KC Integration API,v1is the whole version contract.
Use the API host (safety-api.…), not your KC Safety web address — the application origin does not proxy /api/* through to the backend. If you need a non-production base URL for testing, ask your account team.
Authentication
Every request must carry an API key as a bearer token:
Authorization: Bearer kcs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- Keys are prefixed
kcs_followed by 40 hexadecimal characters. The header must begin exactly withBearer kcs_— there is no alternative header such asX-API-Key. - The key both authenticates the caller and resolves the account. Every response is automatically scoped to the key's account, so there is no account or tenant parameter to pass, and a key can never reach another account's data.
- The raw key is displayed once, at creation. Only a hash is stored, so the value cannot be retrieved again. If a key is lost, revoke it and create a replacement.
Send requests over HTTPS only. Never embed a key in client-side code, a browser request, or a public repository.
Example request
curl -H "Authorization: Bearer kcs_xxxx" \
"https://safety-api.knowledgecity.com/api/v1/public/incidents?page=1&limit=50"{
"error": false,
"data": {
"data": [
{
"id": "…",
"incident_number": "INC-2026-0001",
"description": "…",
"status": "open",
"severity_score": 12,
"severity_likelihood": 3,
"severity_impact": 4,
"location": "…",
"incident_date": "2026-08-01",
"created_at": "2026-08-01T10:00:00.000Z",
"updated_at": "2026-08-02T09:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 137, "pages": 3 }
}
}Pagination
Only Return incidents is paginated. It accepts page and limit query parameters and returns a pagination object alongside the rows.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | optional | 1-based page number. Defaults to 1. |
limit | integer | optional | Page size. Defaults to 50. Maximum 200 — a higher value is reduced to 200 rather than rejected. |
The pagination object reports page, limit, total (rows matching the request) and pages (total pages at the current limit).
Every other endpoint returns a complete, unpaginated array and accepts no query parameters.
Response envelope
Every successful response shares a top-level wrapper:
{ "error": false, "data": <payload> }The shape of data varies by endpoint — it is not uniform, so handle each endpoint's shape explicitly:
| Endpoint | data contains |
|---|---|
| Return incidents | An object: { "data": [ … ], "pagination": { … } } — note the nested data |
| Return incident by id | A single incident object |
| Return incident types, users, locations, departments | A bare array: [ … ], with no pagination |
The Responses table on each endpoint below describes the full body, envelope included.
Rate limiting
Limits are enforced over a fixed one-hour window per account — the window opens with your first request and resets one hour later; it does not slide continuously. The allowance depends on your plan. Read the response headers rather than hard-coding a number or a reset time — your allowance can change with your plan, and X-RateLimit-Reset always carries the exact reset moment.
Present on every response:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your hourly allowance. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | When the window resets, as an absolute Unix epoch time in seconds — not a countdown. |
When the limit is exceeded, the API returns 429 with a Retry-After header (in seconds) and this body:
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "API rate limit exceeded for this account (<limit> requests/hour). Retry after <n>s."
}On a 429 response, X-RateLimit-Reset is not set — use Retry-After. On successful responses, Retry-After is not set.
Design your integration to read incrementally and cache what it can, rather than re-reading the full data set on a tight loop. If you need to react to changes promptly, use outbound event webhooks instead of polling — they deliver within seconds and consume no API quota.
Errors
Authentication and rate-limit errors return a string error code in error. Errors raised by an individual endpoint return error: true with a message. Both forms occur, so branch on the HTTP status rather than the body shape.
| Status | error value | When |
|---|---|---|
| 401 | API_KEY_REQUIRED | The Authorization: Bearer kcs_… header is missing or malformed. |
| 401 | API_KEY_INVALID | The key is unknown, revoked, or expired. |
| 403 | INSUFFICIENT_SCOPE | The key's scope does not permit the HTTP method used. |
| 403 | FEATURE_NOT_AVAILABLE | The account's plan does not include API access. |
| 404 | true | The resource was not found, e.g. { "error": true, "message": "Incident not found" }. |
| 429 | RATE_LIMIT_EXCEEDED | The hourly limit was reached. |
| 500 | AUTH_ERROR or true | Unexpected server error. |
If your account is moved to a plan without API access, existing keys stop working and requests return 403 FEATURE_NOT_AVAILABLE, even though the keys still exist.
Scopes
Each key carries a scope: read, read-write, or admin. The scope governs which HTTP methods the key may use — read permits GET; read-write and admin additionally permit write verbs.
Because the Public API is read-only today, scope has no practical effect on the current surface: a read key can reach every endpoint. Scopes exist for forward compatibility with future write endpoints. Choose read unless you have a specific reason not to.
Managing API keys
Keys are managed by a tenant administrator at Admin Panel → Settings → Integrations → API Keys, which requires the Super Admin role and a plan that includes API access.
- Create — name the key and optionally set a scope (default
read) and an expiry date. The rawkcs_value is shown once; copy it immediately. - List — shows each key's name, prefix, scope, active state, last use, and expiry. The raw value is never shown again.
- Rename, enable or disable, change scope — all supported.
- Revoke — immediate and permanent. Requests using a revoked key return 401
API_KEY_INVALID.
Issue a separate key per integration, so revoking one does not interrupt the others. Revoke any key that is unused, has been exposed, or belonged to a decommissioned system.
A quick reference is also available in the app at Admin Panel → Docs → API Keys.
API reference
Departments
GET /api/v1/public/departments
Return departments
Returns the active departments configured for your account, ordered by name.
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | array | required | Array of department objects. |
↳ id | string (uuid) | required | Department identifier. |
↳ name | string | required | Department name. |
↳ created_at | string (date-time) | required | When the department was created. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error. |
Incident types
GET /api/v1/public/incident-types
Return incident types
Returns the active incident types configured for your account, ordered by name.
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | array | required | Array of incident-type objects. |
↳ id | string (uuid) | required | Incident-type identifier. |
↳ name | string | required | Incident-type name. |
↳ description | string | optional | Incident-type description. |
↳ created_at | string (date-time) | required | When the incident type was created. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error. |
Incidents
GET /api/v1/public/incidents
Return incidents
Returns incidents for your account, newest first (ordered by creation date, descending). This is the only paginated endpoint. There are no filter or sort parameters.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | integer | optional | 1-based page number. Defaults to 1. |
limit | integer | optional | Page size. Defaults to 50. Maximum 200 — a higher value is reduced to 200. |
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | object | required | Payload wrapper. |
↳ data | array | required | Array of incident objects. |
↳↳ id | string (uuid) | required | Incident identifier. |
↳↳ incident_number | string | required | Human-readable reference, e.g. INC-2026-0001. |
↳↳ description | string | optional | Incident description. |
↳↳ status | string | required | Workflow status, e.g. open. |
↳↳ severity_score | integer | optional | Calculated severity, severity_likelihood × severity_impact. |
↳↳ severity_likelihood | integer | optional | Likelihood rating. |
↳↳ severity_impact | integer | optional | Impact rating. |
↳↳ location | string | optional | Location name. |
↳↳ incident_date | string (date) | optional | Date the incident occurred. |
↳↳ created_at | string (date-time) | required | When the incident was created. |
↳↳ updated_at | string (date-time) | required | When the incident was last updated. |
↳ pagination | object | required | Page counters. |
↳↳ page | integer | required | Current page number. |
↳↳ limit | integer | required | Page size in effect. |
↳↳ total | integer | required | Total incidents in the account. |
↳↳ pages | integer | required | Total pages at the current limit. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error — { "error": true, "message": "Failed to fetch incidents" }. |
GET /api/v1/public/incidents/{id}
Return incident by id
Returns a single incident by its identifier, scoped to your account. Returns the same fields as Return incidents, as a single object with no pagination wrapper.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | required | Incident identifier. |
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | object | required | The incident object. |
↳ id | string (uuid) | required | Incident identifier. |
↳ incident_number | string | required | Human-readable reference, e.g. INC-2026-0001. |
↳ description | string | optional | Incident description. |
↳ status | string | required | Workflow status, e.g. open. |
↳ severity_score | integer | optional | Calculated severity, severity_likelihood × severity_impact. |
↳ severity_likelihood | integer | optional | Likelihood rating. |
↳ severity_impact | integer | optional | Impact rating. |
↳ location | string | optional | Location name. |
↳ incident_date | string (date) | optional | Date the incident occurred. |
↳ created_at | string (date-time) | required | When the incident was created. |
↳ updated_at | string (date-time) | required | When the incident was last updated. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
404 | Not found — { "error": true, "message": "Incident not found" }. Also returned when the incident belongs to another account. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error — { "error": true, "message": "Failed to fetch incident" }. |
Locations
GET /api/v1/public/locations
Return locations
Returns the active locations configured for your account, ordered by name.
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | array | required | Array of location objects. |
↳ id | string (uuid) | required | Location identifier. |
↳ name | string | required | Location name. |
↳ created_at | string (date-time) | required | When the location was created. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error. |
Users
GET /api/v1/public/users
Return users
Returns active users in your account, ordered by name. KnowledgeCity support accounts are always excluded from this list.
Responses
200 — application/json body
| Field | Type | Required | Description |
|---|---|---|---|
error | boolean | required | Always false on a successful response. |
data | array | required | Array of user objects. |
↳ id | string (uuid) | required | User identifier. |
↳ name | string | required | Full name. |
↳ email | string (email) | required | Email address. |
↳ role | string | required | Role in KC Safety. |
↳ department | string | optional | Department name. |
↳ job_title | string | optional | Job title. |
↳ created_at | string (date-time) | required | When the user was created. |
| Status | Description |
|---|---|
401 | API_KEY_REQUIRED — the Authorization header is missing or malformed. API_KEY_INVALID — the key is unknown, revoked, or expired. |
403 | INSUFFICIENT_SCOPE or FEATURE_NOT_AVAILABLE. |
429 | RATE_LIMIT_EXCEEDED — hourly limit reached. See Retry-After. |
500 | Server error. |
Not yet available
- Write endpoints — none. No create, update, or delete through the API.
- Filtering and sorting — the incidents list accepts only
pageandlimit. There are no status, type, severity, or date-range filters, and no sort parameter. - Other resources — only the six endpoints above are exposed. Investigations, action items, and documents are not on the Public API.