Authentication
To authorize, use this code:
# Every request requires an Authorization header with your API token
curl "api_endpoint_here"
-H "Authorization: Bearer your_api_token"
Make sure to replace
your_api_tokenwith your API key.
SafetyAmp uses API keys to allow access to the API. To request an API key, please e-mail our support team.
In most cases, your API key is tied to a user profile in the SafetyAmp account and you will likely create a new user to represent the api user.
SafetyAmp expects for the API key to be included in all API requests to the server in a header that looks like the following:
Authorization: Bearer api_key
Making Requests
Apart from including the Authorization header, there are additional headers that are required for every API request.
Required HTTP Headers
| Name | Example Value | Description |
|---|---|---|
Authorization |
Bearer api_key |
This is used to authenticate your client |
Fqdn |
your_subdomain.safetyamp.com |
This is the subdomain of the account your token is for |
User-Agent |
YourOrg/1.0 |
Give your client a descriptive user agent that is unique to your organization |
Accept |
application/json |
Dates and Time
This API will respond with dates and times in the format YYYY-mm-dd H:i:s in the UTC timezone. Most requests expect a similarly formatted string also in UTC. Be sure to send datetime strings in UTC in this format.
Pagination
On list endpoints that support pagination, the following query parameters are available:
| Parameter | Default | Description |
|---|---|---|
limit |
25 | The maximum number of results to return in a single response. |
page |
0 | The zero-indexed page number. page=0 returns the first page, page=1 the second, and so on. |
Paginated responses wrap the results array in a data key and include a meta object with the total row count and current window:
{
"data": [ ... ],
"meta": {
"total": 39,
"current_page": 0,
"per_page": 25,
"last_page": 1
}
}
meta.last_page is the index of the final page (also zero-indexed), so a total of 39 rows at a page size of 25 yields last_page: 1. Non-list endpoints and read-only lookup endpoints (e.g. incident_involved_person_types) return data without a meta block.
Rate Limit
To avoid receiving 429 - Too Many Requests error responses, ensure your client sends no more than 60 api calls per minute.
Filtering, Sorting, and Includes
List endpoints accept a standard request envelope for narrowing, ordering, and expanding the returned data. Each endpoint advertises which keys it supports for filtering, sorting, and including related resources.
# Filter utility bills to a single billing month, sort by period date, and
# include the related utility account and line items.
curl -G "https://api.safetyamp.com/api/utility_bills" \
-H "Authorization: Bearer ..." \
--data-urlencode 'filter_groups[0][filters][0][key]=billing_year' \
--data-urlencode 'filter_groups[0][filters][0][operator]=eq' \
--data-urlencode 'filter_groups[0][filters][0][value]=2025' \
--data-urlencode 'filter_groups[0][filters][1][key]=billing_month' \
--data-urlencode 'filter_groups[0][filters][1][operator]=eq' \
--data-urlencode 'filter_groups[0][filters][1][value]=3' \
--data-urlencode 'sort[0][key]=period_date' \
--data-urlencode 'sort[0][direction]=desc' \
--data-urlencode 'includes[]=utility_account' \
--data-urlencode 'includes[]=line_items'
Filters
Filters are grouped under filter_groups[N][filters][M]. Each filter is an object with key, operator, and value. Filters within a group are combined with AND by default; pass filter_groups[N][or]=true to combine with OR instead.
| Operator | Description |
|---|---|
eq |
Equal to |
neq |
Not equal to |
in |
Value matches any item in a comma-separated or array value |
gt, gte |
Greater than (or equal) |
lt, lte |
Less than (or equal) |
Only the keys advertised by an endpoint may be used; unknown keys return 422 Unprocessable Entity.
Sorting
Sort entries are objects with key and optional direction (asc or desc, default asc):
sort[0][key]=period_date&sort[0][direction]=desc
Only the keys advertised by an endpoint are accepted.
Includes
includes[] expands related resources inline in the response. Endpoints document the include keys they support (e.g. utility_account, line_items.meter). Nested relations are addressed with dot notation.
Sites
Sites are physical locations of plants and facilities.
Get All Sites
curl "https://api.safetyamp.com/api/sites"
-H "Authorization: Bearer ..."
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"name": "Hybrid Plant - Austin, TX",
"street": "111 Paradise Lane",
"street2": null,
"city": "Austin",
"state": "TX",
"zip_code": "11111",
"country": "USA",
"latitude": "",
"longitude": "",
"naics_code": null,
"industry_description": "",
},
...
]
}
This endpoint retrieves all sites.
HTTP Request
GET https://api.safetyamp.com/api/sites
Get a Specific Site
curl "GET https://api.safetyamp.com/api/sites/1"
-H "Authorization: Bearer ..."
Refer to list endpoint for response
This endpoint retrieves a specific site.
HTTP Request
GET https://api.safetyamp.com/api/sites/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The id of the site to retrieve |
Roles
SafetyAmp implements role based access control. Users are assigned to multiple roles and receive aggregate permissions from their combined roles.
Get All Roles
curl "https://api.safetyamp.com/api/roles"
-H "Authorization: Bearer ..."
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"name": "Plant Manager"
},
...
]
}
This endpoint retrieves all roles.
HTTP Request
GET https://api.safetyamp.com/api/roles
Titles
SafetyAmp maintains a set of titles for your company. Users can be assigned one of these.
Get All Titles
curl "https://api.safetyamp.com/api/user_titles"
-H "Authorization: Bearer ..."
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"name": "Plant Assitant Manager"
},
...
]
}
This endpoint retrieves all titles.
HTTP Request
GET https://api.safetyamp.com/api/user_titles
Users
Creating a User
This endpoint allows the creation of new users in your account.
curl -X POST "https://api.safetyamp.com/api/users" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"first_name": "Jane",
"middle_name": null,
"last_name": "Doe",
"nickname": null,
"gender": "1",
"email": "jane.doe@example.com",
"date_of_birth": "1999-01-31",
"street": "11 Paradise Ln",
"street2": null,
"city": "San Francisco",
"state": "CA",
"zip_code": "94131",
"country":"US",
"mobile_phone": "+11231231234",
"home_phone": "+12342342345",
"work_phone": "+13453453456",
"pending":1,
"system_access":1,
"bypass_sso":0,
"text_opt_out": 0,
"text_opt_out":0,
"timezone":"America/New_York",
"roles":[{"id": 1}],
"home_site_id": 1,
"sites":[{"id": 1}],
"current_title_id":1,
"current_department_id":1,
"current_supervisor_id":1
}'
HTTP Request
POST https://api.safetyamp.com/api/users
Request Body
| Parameter | Description |
|---|---|
| first_name | (Required) The first name of the user |
| last_name | (Required) The last name of the user |
| (Required) The email address of the user, must be unique, currently used as username | |
| mobile_phone | Integers only, used for SMS messaging if provided |
| system_access | 1 if user can login to SafetyAmp, 0 if user cannot |
| pending | Boolean, a user input with pending as true will not send Activation email on create |
| activated | Set to 1 to activate user bypassing email validation |
| bypass_sso | If SSO enabled for account, set to 1 to allow user to login without SSO |
| text_opt_out | 1 if user does not wish to receive SMS on mobile number |
| timezone | chosen from list of timezones in Object Reference |
| roles[] | An array of objects containing id properties of the roles to assign to the user |
| home_site_id | uses the id property of the site the user is primarily at. This will automatically be added to sites for site access |
| sites[] | An array of objects containing the id property of the sites that user is allowed access to |
Getting All Users
curl "https://api.safetyamp.com/api/users"
-H "Authorization: Bearer ..."
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"first_name": "Jane",
"middle_name": null,
"last_name": "Doe",
"nickname": null,
"date_of_birth": 1999-01-31,
"owner": true,
"email": "jane.doe@example.com",
"work_phone": null,
"home_phone": null,
"mobile_phone": null,
"avatar_location": null,
"activated": 1,
"gender": null,
"street": "11 Paradise Ln",
"city": "San Francisco",
"state": "CA",
"zip_code": "94131",
"country": "USA",
"street2": null,
"text_opt_out": 0,
"system_access": 1,
"timezone": "America/Los_Angeles",
"home_site_id": 1,
"last_logged_in": "2019-10-25 21:40:22",
"current_title": null,
"deleted_at": null
},
...
]
}
HTTP Request
GET https://api.safetyamp.com/api/users
This endpoint supports pagination.
Getting a Specific User
curl "GET https://api.safetyamp.com/api/users/1"
-H "Authorization: Bearer ..."
Refer to list endpoint for response
HTTP Request
GET https://api.safetyamp.com/api/users/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The id of the user to retrieve |
Updating a User
curl -X PUT "https://api.safetyamp.com/api/users/<id>" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
}'
HTTP Request
PUT https://api.safetyamp.com/api/users/<id>
URL Parameters
first_name, last_name, and email are required fields on PUT calls. Refer to creating a user for list of additional parameters that can be updated. Only provided values will be updated.
Remove a User
Removing a user inactivates their user account and does not allow the user to access the SafetyAmp platform. The user data will be retained.
curl -X DELETE "https://api.safetyamp.com/api/users/<id>" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
HTTP Request
DELETE https://api.safetyamp.com/api/users/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The id of the user to retrieve |
SiteSense
SiteSense is SafetyAmp's energy management module. It tracks utility accounts (the relationship between a site and a utility provider), the meters attached to those accounts, and the bills (invoices) recorded against them. PDF copies of invoices are stored as attachments on the bill.
Permissions
SiteSense endpoints require one of two permissions:
| Permission | What it grants |
|---|---|
InvoiceViewer |
Read-only access to utility accounts and bills. Account access is scoped to the user's accessible sites. |
EnergyAdministrator |
Full read/write access to all SiteSense resources. |
Endpoints that require EnergyAdministrator are noted on each section below.
Utility Types
Utility types are the categories of utility tracked by SiteSense (e.g. electricity, natural gas, water). The set of types is managed by SafetyAmp and cannot be created, updated, or deleted via the API.
List Utility Types
curl "https://api.safetyamp.com/api/utility_types" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"name": "Electricity",
"slug": "electricity",
"unit_consumption": "kWh",
"unit_demand": "kW"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_types
This endpoint supports pagination.
Get a Utility Type
curl "https://api.safetyamp.com/api/utility_types/1" \
-H "Authorization: Bearer ..."
Refer to list endpoint for response
HTTP Request
GET https://api.safetyamp.com/api/utility_types/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The id of the utility type to retrieve |
Utility Accounts
A utility account represents a single account a site holds with a utility provider. Bills are recorded against accounts.
Requires InvoiceViewer or EnergyAdministrator. Users without EnergyAdministrator only see accounts at sites they have access to.
List Utility Accounts
curl "https://api.safetyamp.com/api/utility_accounts" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 12,
"site_id": 1,
"provider_ref": "ACCT-44123",
"provider_name": "Austin Energy",
"type": "delivery",
"linked_to": null,
"notes": null,
"start_date": "2024-01-01",
"is_excluded": false,
"is_closed": false,
"integration_type": null,
"integration_ext_id": null,
"integration_status": null,
"last_missing_invoice_notification": null
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_accounts
Filter Keys
| Key | Type |
|---|---|
id |
integer |
provider_id |
integer |
provider_name |
string |
provider_ref |
string |
site_id |
integer |
type_id |
integer |
is_closed |
boolean |
is_excluded |
boolean |
integration_type |
string |
integration_status |
string |
Sort Keys
| Key |
|---|
site |
provider_name |
provider_ref |
Include Keys
| Key | What it adds |
|---|---|
site |
Site object. |
meters |
Meters attached to the account. |
linked |
The account this one is linked to (delivery ↔ supply pairing). |
Get a Utility Account
curl "https://api.safetyamp.com/api/utility_accounts/12" \
-H "Authorization: Bearer ..."
Refer to list endpoint for response
HTTP Request
GET https://api.safetyamp.com/api/utility_accounts/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The id of the utility account to retrieve |
Include Keys
| Key | What it adds |
|---|---|
site |
Site object. |
meters |
Meters attached to the account. |
linked |
The account this one is linked to. |
default_line_items |
Default line items configured for the account. |
Create a Utility Account
Requires EnergyAdministrator. Returns 409 Conflict if an account with the same site_id, provider_ref, and provider_name already exists.
curl -X POST "https://api.safetyamp.com/api/utility_accounts" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"site_id": 1,
"provider_name": "Austin Energy",
"provider_ref": "ACCT-44123",
"type": "delivery",
"start_date": "2024-01-01",
"notes": null,
"is_excluded": false,
"is_closed": false
}'
HTTP Request
POST https://api.safetyamp.com/api/utility_accounts
Request Body
| Parameter | Description |
|---|---|
| site_id | (Required) The id of the site the account belongs to |
| provider_name | (Required) Name of the utility provider, max 255 chars |
| start_date | (Required) Date the account starts tracking bills, YYYY-MM-DD |
| provider_id | Optional reference to a known utility provider record |
| provider_ref | The provider's external account number, max 255 chars |
| type | One of delivery, supply, general |
| linked_to | The id of another active utility account this one is paired with |
| is_excluded | Boolean, if true the account is excluded from energy model aggregations |
| is_closed | Boolean, if true no new bills are expected on this account |
| integration_type | arc to enable Arc integration. When set, the account is validated against Arc and tied to an Arc account id |
| meters | An array of meter objects to create on the account. See "Sync Utility Account Meters" for the shape |
Update a Utility Account
Requires EnergyAdministrator.
site_id cannot be changed after creation. If the account is configured with an integration, provider_ref and provider_name cannot be changed.
curl -X PATCH "https://api.safetyamp.com/api/utility_accounts/<id>" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"notes": "Switched to time-of-use rate in March",
"is_closed": false
}'
HTTP Request
PATCH https://api.safetyamp.com/api/utility_accounts/<id>
Request Body
All fields are optional. Same parameters as create, except site_id which is immutable. Also accepts default_line_items: an array of {utility_meter_id, type, desc} objects that fully replace the account's default line items used when creating new bills.
Delete a Utility Account
Requires EnergyAdministrator. Deleting an account also deletes any bills attached to it.
curl -X DELETE "https://api.safetyamp.com/api/utility_accounts/<id>" \
-H "Authorization: Bearer ..."
HTTP Request
DELETE https://api.safetyamp.com/api/utility_accounts/<id>
Export Utility Account Bills
Requires EnergyAdministrator. Queues a background job that produces a CSV export of all bills at a site for a given year. The response is a JobStatus resource that can be polled to check progress and retrieve the resulting file.
curl -X POST "https://api.safetyamp.com/api/utility_accounts:export" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"site": 1,
"year": 2025
}'
HTTP Request
POST https://api.safetyamp.com/api/utility_accounts:export
Request Body
| Parameter | Description |
|---|---|
| site | (Required) The id of the site to export bills for. Must have energy management enabled. |
| year | (Required) The year to export, >= 2000 |
Utility Account Meters
Meters represent the individual measurement points on a utility account. A single account can have multiple meters (e.g. main panel and a sub-panel), each tied to a utility_type.
Requires EnergyAdministrator.
List Utility Account Meters
curl "https://api.safetyamp.com/api/utility_accounts/12/meters" \
-H "Authorization: Bearer ..."
The above command returns JSON structured like this:
{
"data": [
{
"id": 88,
"name": "Main Electric Meter",
"type_id": 1,
"portfolio_mgr_id": null
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_accounts/<id>/meters
Sync Utility Account Meters
Requires EnergyAdministrator. This is a full sync: the request body must contain every meter that should exist on the account. Meters present on the account but absent from the payload are deleted. Meters with an id are updated; meters without one are created.
curl -X PUT "https://api.safetyamp.com/api/utility_accounts/12/meters" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d `[
{ "id": 88, "name": "Main Electric Meter", "type_id": 1 },
{ "name": "Sub Panel A", "type_id": 1, "portfolio_mgr_id": "PM-1234" }
]`
HTTP Request
PUT https://api.safetyamp.com/api/utility_accounts/<id>/meters
Request Body
Each item in the array supports:
| Parameter | Description |
|---|---|
| id | The id of an existing meter on this account. Omit to create a new meter. |
| name | (Required when creating) The display name, max 255 chars |
| type_id | (Required when creating) The id of a utility_type |
| portfolio_mgr_id | Optional ENERGY STAR Portfolio Manager id, max 255 chars |
Utility Bills
Utility bills (also called invoices) represent a single billing period on a utility account. Each bill carries one or more line items (usage, demand, or charges) and optionally one or more PDF attachments.
Requires InvoiceViewer or EnergyAdministrator. Users without EnergyAdministrator only see bills at sites they have access to.
List Utility Bills
curl "https://api.safetyamp.com/api/utility_bills" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": 4801,
"import_ref": null,
"import_src": null,
"utility_account_id": 12,
"billing_year": 2025,
"billing_month": 3,
"period_date": "2025-02-15",
"period_days": 30,
"currency": "USD",
"total_charges": 1842.55,
"flags": [],
"flags_silenced": false,
"unavailable": false,
"created_at": "2025-04-01 12:00:00",
"updated_at": "2025-04-01 12:00:00"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_bills
Filter Keys
| Key | Type |
|---|---|
id |
integer |
utility_account_id |
integer |
billing_year |
integer |
billing_month |
integer (1–12) |
Sort Keys
| Key |
|---|
period_date |
Include Keys
| Key | What it adds |
|---|---|
site |
Site object for the bill's utility account. |
attachments |
Attached files (PDF invoices, etc.). |
utility_account |
Parent utility account. |
line_items |
Line items on the bill. |
line_items.meter |
Nested: the meter on each line item. |
Response Fields
| Field | Description |
|---|---|
| id | The bill identifier |
| utility_account_id | The id of the account this bill belongs to |
| billing_year | Calendar year the bill covers |
| billing_month | Calendar month the bill covers, 1-12 |
| period_date | First day of the billing period, YYYY-MM-DD |
| period_days | Length of the billing period in days |
| currency | One of USD, BSD, CAD, CRC, EUR, GBP, MXN, PAB, SEK |
| total_charges | The sum of Charge line items in the bill's currency |
| flags | An array of automated validation flags raised against this bill |
| flags_silenced | Boolean, whether the flags were silenced by an admin |
| unavailable | Boolean, true if the bill is recorded as known-missing for the period (placeholder; cannot have line items or attachments) |
| import_ref | External id from the source system when the bill was imported. Cleared automatically if any non-flags_silenced field is updated. |
| import_src | Name of the source system the bill was imported from |
Get a Utility Bill
curl "https://api.safetyamp.com/api/utility_bills/4801" \
-H "Authorization: Bearer ..."
Refer to list endpoint for response
HTTP Request
GET https://api.safetyamp.com/api/utility_bills/<id>
Include Keys
| Key | What it adds |
|---|---|
site |
Site object for the bill's utility account. |
attachments |
Attached files. |
utility_account |
Parent utility account. |
utility_account.meters |
Nested: meters attached to the parent account. |
line_items |
Line items on the bill. |
line_items.meter |
Nested: the meter on each line item. |
creator |
User who created the bill. |
updater |
User who last updated the bill. |
Create a Utility Bill
Requires EnergyAdministrator.
curl -X POST "https://api.safetyamp.com/api/utility_bills" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"utility_account_id": 12,
"billing_year": 2025,
"billing_month": 3,
"period_date": "2025-02-15",
"period_days": 30,
"currency": "USD",
"items": [
{ "type": 1, "meter_id": 88, "desc": "Usage", "value": 12450 },
{ "type": 3, "meter_id": null, "desc": "Amount Due", "value": 1842.55 }
]
}'
HTTP Request
POST https://api.safetyamp.com/api/utility_bills
Request Body
| Parameter | Description |
|---|---|
| utility_account_id | (Required) The id of the account this bill belongs to |
| billing_year | (Required) Calendar year, integer |
| billing_month | (Required) Calendar month, integer 1-12 |
| period_date | (Required) First day of the billing period, YYYY-MM-DD |
| period_days | (Required) Length of the billing period in days, integer ≥ 1 |
| currency | (Required) Three-letter currency code (see list endpoint) |
| items | An array of line items. See "Sync Utility Bill Line Items" for the shape. Prohibited if unavailable is true. |
| flags_silenced | Boolean, silences automated validation flags on creation |
| unavailable | Boolean, mark the bill as known-missing. The bill cannot have line items or attachments. |
A 422 is returned if the new bill overlaps an existing unavailable bill on the same account/period.
Update a Utility Bill
Requires EnergyAdministrator. The utility_account_id is immutable. Bills with unavailable=true cannot be updated.
curl -X PATCH "https://api.safetyamp.com/api/utility_bills/<id>" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"period_days": 31,
"items": [
{ "id": 1, "type": 1, "meter_id": 88, "desc": "Usage", "value": 12500 }
]
}'
HTTP Request
PATCH https://api.safetyamp.com/api/utility_bills/<id>
Request Body
All fields optional. Same parameters as create, except utility_account_id. If any field other than flags_silenced is updated, the bill's import_ref and import_src are cleared.
Delete a Utility Bill
Requires EnergyAdministrator. Also removes any PDF attachments on the bill.
curl -X DELETE "https://api.safetyamp.com/api/utility_bills/<id>" \
-H "Authorization: Bearer ..."
HTTP Request
DELETE https://api.safetyamp.com/api/utility_bills/<id>
Utility Bill Line Items
A bill's line items break down its usage, demand, and charges, optionally tied to a specific meter.
List Utility Bill Line Items
curl "https://api.safetyamp.com/api/utility_bills/4801/line_items" \
-H "Authorization: Bearer ..."
The above command returns JSON structured like this:
{
"data": [
{
"id": 1,
"meter_id": 88,
"desc": "Usage",
"type": 1,
"value": 12450
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_bills/<id>/line_items
Line Item Types
| Value | Type | Notes |
|---|---|---|
| 1 | Usage | Quantity of the metered utility consumed (e.g. kWh) |
| 2 | Demand | Peak demand for the period (e.g. kW) |
| 3 | Charge | Monetary amount in the bill's currency |
Line items without a meter_id (ad-hoc lines) must have type = 3 (Charge).
Sync Utility Bill Line Items
Requires EnergyAdministrator. A full sync: the array supplied replaces the bill's line items entirely.
curl -X PUT "https://api.safetyamp.com/api/utility_bills/4801/line_items" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d `[
{ "id": 1, "type": 1, "meter_id": 88, "desc": "Usage", "value": 12500 },
{ "type": 3, "meter_id": null, "desc": "Late Fee", "value": 25.00 }
]`
HTTP Request
PUT https://api.safetyamp.com/api/utility_bills/<id>/line_items
Request Body
Each item in the array supports:
| Parameter | Description |
|---|---|
| id | The id of an existing line item. Omit to create a new one. |
| type | (Required) One of 1 (Usage), 2 (Demand), 3 (Charge). Ad-hoc lines (meter_id=null) must use 3. |
| desc | (Required) Description, max 255 chars |
| meter_id | The id of a meter on this bill's account, or null for ad-hoc charges |
| value | Numeric value, up to 4 decimal places |
Utility Bill Attachments
PDFs (and other files) attached to a utility bill are exposed via short-lived signed URLs. To download an attachment:
- Call
GET /api/utility_bills/<id>/attachmentsto list attachments on the bill, orGET /api/utility_bills/<id>/attachments/<attachment_id>for a single one. - The response contains a
temporary_urlfield. Issue aGETagainst that URL to download the file. - The signed URL expires; re-request the attachment to obtain a fresh URL if needed.
List Utility Bill Attachments
curl "https://api.safetyamp.com/api/utility_bills/4801/attachments" \
-H "Authorization: Bearer ..."
The above command returns JSON structured like this:
{
"data": [
{
"id": 901,
"file_name": "invoice-2025-03.pdf",
"display_name": "March 2025 Invoice",
"file_size": 184320,
"mime_type": "application/pdf",
"type": "default",
"description": null,
"private": false,
"external_url": null,
"temporary_url": "https://storage.safetyamp.com/...signed...",
"created_at": "2025-04-01 12:00:00",
"updated_at": "2025-04-01 12:00:00"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/utility_bills/<id>/attachments
Get a Utility Bill Attachment
curl "https://api.safetyamp.com/api/utility_bills/4801/attachments/901" \
-H "Authorization: Bearer ..."
Refer to list endpoint for response
HTTP Request
GET https://api.safetyamp.com/api/utility_bills/<id>/attachments/<attachment_id>
A 404 is returned if the attachment is not attached to the specified bill.
Upload a Utility Bill Attachment
Requires EnergyAdministrator. Bills marked unavailable=true cannot have attachments. Uploads are sent as multipart/form-data with a single attachment file field.
curl -X POST "https://api.safetyamp.com/api/utility_bills/4801/attachments" \
-H "Authorization: Bearer ..." \
-F "attachment=@invoice-2025-03.pdf"
Refer to list endpoint for response
HTTP Request
POST https://api.safetyamp.com/api/utility_bills/<id>/attachments
Request Body (multipart)
| Parameter | Description |
|---|---|
| attachment | (Required) The file to upload |
Delete a Utility Bill Attachment
Requires EnergyAdministrator.
curl -X DELETE "https://api.safetyamp.com/api/utility_bills/4801/attachments/901" \
-H "Authorization: Bearer ..."
HTTP Request
DELETE https://api.safetyamp.com/api/utility_bills/<id>/attachments/<attachment_id>
A 404 is returned if the attachment is not attached to the specified bill.
Incidents
Incidents are the core record of "something happened that safety cares about" — an injury, near-miss, spill, equipment issue, and so on. An incident carries the event context (site, date/time, description), links to the people involved, tracks the investigation, and (optionally) rolls up into a claim filed with an insurance carrier.
Incidents are only available on accounts with has_incidents enabled — every endpoint returns 404 Not Found if it's off, before any permission check runs.
Permissions
Every incident endpoint requires at least one of:
| Permission slug | In-app name | What it grants |
|---|---|---|
can_administer_incidents |
Can Administer Incidents | Create, view, edit, and delete every incident in the account. |
can_investigate_incidents |
Can Investigate Incidents | Add root-cause analyses, involved persons, attachments, comments; edit the investigator payload. |
can_create_incidents |
Can Create Incidents | File new incidents. |
can_view_all_incidents |
Can View All Incidents | Read every incident in the account. |
can_view_subordinates_incidents |
Can View Subordinates Incidents | Read incidents filed by users the current user supervises. |
can_view_own_incidents |
Can View Own Incidents | Read only own incidents — created by, responsible for, or listed as primary involved person. |
can_edit_all_incidents |
Can Edit All Incidents | Edit every incident in the account. |
can_edit_subordinates_incidents |
Can Edit Subordinates Incidents | Edit incidents filed by users the current user supervises. |
can_edit_own_incidents |
Can Edit Own Incidents | Edit only own incidents. |
can_delete_all_incidents |
Can Delete All Incidents | Delete every incident in the account. |
can_delete_subordinates_incidents |
Can Delete Subordinates Incidents | Delete incidents filed by supervisees. |
can_delete_own_incidents |
Can Delete Own Incidents | Delete only own incidents. |
can_manage_case_and_medical_data |
Can Manage Case And Medical Data | Edit case-and-medical fields on involved persons and set osha_recordable. |
can_set_osha_recordable |
Can Set Osha Recordable | Toggle an incident's OSHA recordable flag. |
can_export_osha_documents |
Can Export Osha Documents | Export OSHA regulatory documents. |
Company Administrators (or any custom role with company_administration) bypass every check in this table.
Callers with can_administer_incidents or can_investigate_incidents receive the investigator view — the full field set plus investigation-side blocks (root causes, RCAs, costings, comments, attachments, accident classifications, all involved persons). Other callers get the reporter view — same top-level scalars, but investigation blocks are omitted and action_items is filtered to items they created, own, or are assigned to (unless they hold can_view_all_action_items). Field tables below mark investigator-only fields.
List Incidents
Requires any view or edit permission from the permissions table.
curl "https://api.safetyamp.com/api/incidents" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json"
The above command returns JSON structured like this:
{
"data": [
{
"id": "x6Ad2",
"status": 0,
"severity": "High",
"event_date": "2025-09-19",
"event_time": "10:00:00",
"time_indeterminate": 0,
"investigation_date": "2025-09-19 15:41:16",
"report_date": "2025-09-19",
"report_time": "10:00:00",
"site_id": 1,
"claim_id": null,
"case_number_prefix": "INC",
"case_number": 1,
"location_id": 4,
"location": "Loading Dock",
"asset_id": 4,
"stage_id": null,
"department_id": 4,
"description": "Employee slipped on wet floor at loading dock.",
"title": "Slip and fall — loading dock",
"osha_recordable": 0,
"occurred_off_premise": 0,
"// … off_premise_* fields (null unless occurred_off_premise=1)": null,
"others_injured": 0,
"shift_id": 4,
"responsible_person": "5V4A2",
"completed_by": null,
"private": false,
"open_claims_count": 0,
"claims_count": null,
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"created_by": "5V4A2",
"updated_by": "5V4A2",
"action_items": [],
"types": [ /* Attached types — see Incident Types */ ],
"creator": { "id": "5V4A2", "first_name": "Jane", "last_name": "Doe" },
"responsible_person_user": { "id": "5V4A2", "first_name": "Jane", "last_name": "Doe" },
"site": { "id": 1, "name": "Charlottesville" },
"location_object": { "id": 4, "site_id": 1, "name": "Loading Dock" },
"shift": { "id": 4, "site_id": 1, "name": "Day Shift" },
"stage": null
}
],
"meta": {
"total": 1,
"current_page": 0,
"per_page": 25,
"last_page": 1
}
}
HTTP Request
GET https://api.safetyamp.com/api/incidents
This endpoint supports pagination.
Filter Keys
| Key | Type | Notes |
|---|---|---|
title |
string | Partial match. |
title_or_id |
string | Match against the incident's title or its case number. |
event_date |
date YYYY-MM-DD |
Supports gte / lte / eq operators. |
has_injury |
boolean | 0 or 1. |
osha_recordable |
boolean | 0 or 1. |
severity |
enum | Low, Medium, High. |
status |
integer | 0 (In Progress) or 1 (Complete). Derived from stage. |
site_id |
integer | Site DB id. |
cluster_id |
integer | Site cluster DB id. |
stage_id |
integer | Workflow stage DB id. |
case_number |
integer | Account-local case number. |
department_id |
integer | Department DB id. |
location_id |
integer | Location DB id. |
shift_id |
integer | Shift DB id. |
account_types.id |
integer | Account-authored incident type DB id. |
default_types.id |
integer | Default incident type DB id. |
involved_persons.injury |
boolean | 0 or 1. |
involved_persons.illness |
boolean | 0 or 1. |
involved_persons.user_id |
integer | User DB id of the involved person (raw integer, not encoded). |
involved_persons.type_id |
integer | Involved-person-type DB id. |
Sort Keys
| Key |
|---|
id |
event_date |
created_at |
title |
severity |
case_number |
Included Relationships
Always eager-loaded on the list response — the client cannot opt out:
| Key | What it adds |
|---|---|
site |
Basic site object (id, name). |
types |
Attached incident types with the nested type object. |
location_object |
Location record (when location_id is set). |
stage |
Workflow stage record. |
creator |
Basic creator user object. |
responsible_person_user |
Basic assignee user object. |
shift |
Shift record. |
action_items |
Attached action items. |
On accounts with claims enabled, each row also carries an open_claims_count integer.
The list respects the caller's view/edit scope from the permissions table above; can_create_incidents is treated as a legacy alias for can_view_own_incidents. Site restrictions apply on top.
Get an Incident
Requires any view or edit permission covering this incident.
curl "https://api.safetyamp.com/api/incidents/x6Ad2" \
-H "Authorization: Bearer ..."
Returns the same fields as the list endpoint, plus these additional relationship blocks:
{
"data": {
"id": "x6Ad2",
"// … all scalar fields shown above": null,
"attachments": [ /* Attachment objects — see Incident Attachments */ ],
"comments": [ /* Comment objects — see Incident Comments */ ],
"costings": [ /* Costing objects — see Incident Costings */ ],
"involved_persons": [ /* Involved Person objects — see Involved Persons */ ],
"root_cause_analyses": [ /* RCA objects — see Root Cause Analyses */ ],
"root_causes": [],
"selected_root_causes": [],
"accident_classifications": [],
"forms": [],
"audit_trails": [],
"custom_fields": [ /* CustomField objects with nested answers[] — see Custom Fields */ ],
"asset": { "id": 3, "name": "Forklift #7" },
"department": { "id": 3, "name": "Warehouse Operations" }
}
}
HTTP Request
GET https://api.safetyamp.com/api/incidents/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The ID of the incident to retrieve (e.g. x6Ad2) |
Returns 403 if the caller lacks a view permission covering this incident. Returns 404 if the incident does not exist or is soft-deleted.
Create an Incident
Requires any incident permission. Investigator-only fields (marked below) are silently dropped for callers without can_administer_incidents or can_investigate_incidents.
curl -X POST "https://api.safetyamp.com/api/incidents" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"title": "Slip and fall — loading dock",
"event_date": "2025-09-19",
"event_time": "11:42:00",
"report_date": "2025-09-19",
"report_time": "11:42:00",
"site_id": 1,
"description": "Employee slipped on wet floor.",
"severity": "Low",
"responsible_person": "5V4A2",
"types": [
{ "incident_type_id": 1, "incident_type_model": "default" }
],
"involved_persons": [
{
"user_id": "5V4A2",
"type_id": 2,
"injury": 1,
"injuries": [
{
"injury_type_model": "default",
"injury_type_id": 4,
"location_model": "default",
"location_id": 6,
"side_of_body": "Right"
}
]
}
]
}'
HTTP Request
POST https://api.safetyamp.com/api/incidents
Request Body
| Field | Description |
|---|---|
| title | (Required) Short summary of the incident, max 255 chars. |
| event_date | (Required) Date the event happened, YYYY-MM-DD. |
| event_time | Time the event happened, HH:MM:SS. Send null and set time_indeterminate to 1 when the exact time is unknown. |
| time_indeterminate | Boolean. 1 marks event_time as unknown. |
| report_date | (Required) Date the incident was reported, YYYY-MM-DD. |
| report_time | Time the incident was reported, HH:MM:SS. Nullable. |
| site_id | Integer ID of the site the incident occurred at. |
| location_id | Integer ID of a specific location record. Nullable. |
| location | Free-text location description used when a location record isn't appropriate. |
| asset_id | Integer ID of an involved asset. |
| department_id | Integer ID of a department. |
| shift_id | Integer ID of a shift. |
| stage_id | Integer ID of the workflow stage. Defaults to the account's first Incident stage. Setting it to the account's closed stage marks the incident complete — see notes below. |
| severity | One of Low, Medium, High. Defaults to Low. |
| description | Free-text description, up to 65534 chars. |
| responsible_person | User ID of the assigned investigator, encoded (e.g. "5V4A2"). |
| completed_by | User ID of the user marking the incident complete, encoded. Automatically populated by the server. |
| others_injured | Boolean. true if additional non-listed people were injured. |
| occurred_off_premise | Boolean. When true, the off_premise_* fields become required. |
| off_premise_street, off_premise_street_2, off_premise_city, off_premise_state, off_premise_zip_code, off_premise_country, off_premise_phone_number, off_premise_note | Off-premise address block. off_premise_state and off_premise_country must be exactly two characters (state code and ISO alpha-2 country). off_premise_zip_code maxes at 6 characters. |
| types | Array of { incident_type_id, incident_type_model } objects. incident_type_model must be default or account. See "Incident Types" below. On update the full array replaces the previous value. |
| accident_classifications | Array of { id, name } objects. Passing only a name (no id) creates a new classification. |
| root_causes | Array of `{ root_cause_type: "default"\ |
| involved_persons | Array of involved-person objects. See "Involved Persons" below for the shape. |
| costings | Array of costing objects. See "Incident Costings" below. |
| custom_field_answers | Array of { custom_field_id, answer } entries. See below for the answer sub-shape. |
| attachments | Array of { file_name, description, type, external_url } objects. Each item must include an external_url pointing at a file you host. |
| osha_recordable | (Investigator-only) Boolean flagging the incident as OSHA recordable. Requires can_administer_incidents or can_manage_case_and_medical_data on the request, plus can_administer_incidents or can_investigate_incidents at the service layer. |
| private | (Investigator-only) Boolean. true marks the incident as a privacy case — see the callout below. |
status, case_number, case_number_prefix, claim_id, open_claims_count, claims_count are server-controlled and cannot be set through this endpoint.
Request Body — Custom Field Answers
Each entry in custom_field_answers targets one incident-scoped custom field. Fetch definitions from GET /api/custom_fields to look up custom_field_id values and their type.
| Field | Description |
|---|---|
custom_field_id |
(Required) Integer ID of the custom field being answered. |
answer |
(Required) Type-specific object; shape depends on the parent field's type. |
answer shape by parent field type:
Parent type |
answer |
|---|---|
ShortText, LongText |
{ "value": "..." } |
Date |
{ "value": "YYYY-MM-DD" } |
Time |
{ "value": "HH:MM:SS" } |
DateTime |
{ "value": "YYYY-MM-DD HH:MM:SS" } |
MultipleChoice |
{ "custom_field_choice_id": <choice_id> } |
Upload |
{ "uuid": "...", "type": "image/png", "file_name": "photo.png" } |
For Upload, the uuid must reference a file already staged in temp storage — upload the file to the attachments upload endpoint first, then attach it here by uuid.
Update an Incident
Requires an edit permission covering this incident.
Same body shape as create, with these differences:
event_dateandtitleremain required. Every other field is optional.severityacceptsnullto clear it.typessent in the payload fully replaces the existing set.custom_field_answers[].answer.idmay be supplied to update an existing answer row in place. Any answer previously attached to this incident that is not present in the payload is deleted.case_numberis immutable; the server drops it silently if you send it.- Transitioning
stage_idto the account's closed stage triggers a completion check — the request returns422if any required templates for the incident's types are still missing (Company Administrators bypass this check).
Callers without can_administer_incidents or can_investigate_incidents may only update: severity, event_date, event_time, investigation_date, report_date, report_time, site_id, location_id, location, shift_id, asset_id, stage_id, department_id, description, title, types, responsible_person, completed_by, osha_recordable. Any other field is dropped silently.
curl -X PATCH "https://api.safetyamp.com/api/incidents/x6Ad2" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"title": "Slip and fall — loading dock (updated)",
"severity": "Medium"
}'
HTTP Request
PATCH https://api.safetyamp.com/api/incidents/<id>
URL Parameters
| Parameter | Description |
|---|---|
| id | The ID of the incident to update |
Delete an Incident
Requires a delete permission covering this incident.
curl -X DELETE "https://api.safetyamp.com/api/incidents/x6Ad2" \
-H "Authorization: Bearer ..."
Soft-deletes the incident and writes an audit-trail entry. Related sub-resources (attachments, comments, involved persons, RCAs, costings) remain in the database but stop surfacing through any other endpoint.
HTTP Request
DELETE https://api.safetyamp.com/api/incidents/<id>
Responds 204 No Content.
Incident Attachments
Attachments on incidents are links (URLs) to files you host. Host the file yourself, then send its public URL to SafetyAmp using the endpoints below.
All attachment endpoints require can_administer_incidents or can_investigate_incidents.
List Attachments
curl "https://api.safetyamp.com/api/incidents/x6Ad2/attachments" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 42,
"attachable_id": 1,
"attachable_type": "incident",
"file_name": "loading-dock-photo.jpg",
"display_name": null,
"type": "image",
"external_url": "https://example.com/incident-photos/dock.jpg",
"description": null,
"private": false,
"created_at": "2025-09-19 15:45:00",
"created_by": "5V4A2",
"updated_at": "2025-09-19 15:45:00",
"updated_by": "5V4A2"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incidents/<incident_id>/attachments
Add an Attachment
Two options: link to a file you host, or upload the file directly.
Attach by URL (JSON):
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/attachments" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"attachments": [
{
"file_name": "loading-dock-photo.jpg",
"description": "Photo of the wet floor",
"type": "image",
"external_url": "https://example.com/incident-photos/dock.jpg"
}
]
}'
Upload files (multipart/form-data):
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/attachments" \
-H "Authorization: Bearer ..." \
-F "attachments[0][file_name]=loading-dock-photo.jpg" \
-F "attachments[0][type]=image" \
-F "attachments[0][attachment]=@/path/to/dock.jpg"
HTTP Request
POST https://api.safetyamp.com/api/incidents/<incident_id>/attachments
Request Body
The endpoint accepts a batch. Send attachments as an array even for a single file. Each entry must include exactly one of attachment, external_url, or uuid.
| Field | Description |
|---|---|
| attachments | (Required) Array of attachment items. |
| attachments[].file_name | (Required) A display filename for the attachment, max 255 chars. |
| attachments[].type | (Required) Short category slug, max 80 chars. Examples: image, document. |
| attachments[].attachment | (Required if neither external_url nor uuid is set) The file itself, sent via multipart/form-data. |
| attachments[].external_url | (Required if neither attachment nor uuid is set) The URL of a file you host, up to 2048 chars. Must be reachable by SafetyAmp. |
| attachments[].description | Optional description, max 255 chars. |
Responds 201 with the created attachment objects. Uploaded files are processed asynchronously — image compression and PDF conversion run after the response.
Get an Attachment
curl "https://api.safetyamp.com/api/incidents/x6Ad2/attachments/42" \
-H "Authorization: Bearer ..."
GET https://api.safetyamp.com/api/incidents/<incident_id>/attachments/<attachment_id>
Returns 422 if the attachment is not attached to the given incident.
Update an Attachment
description, file_name, external_url, and type may all be updated.
curl -X PATCH "https://api.safetyamp.com/api/incidents/x6Ad2/attachments/42" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"description": "Photo taken from north side",
"external_url": "https://example.com/incident-photos/dock-v2.jpg",
"file_name": "dock-v2.jpg",
"type": "image"
}'
HTTP Request
PATCH https://api.safetyamp.com/api/incidents/<incident_id>/attachments/<attachment_id>
Delete an Attachment
curl -X DELETE "https://api.safetyamp.com/api/incidents/x6Ad2/attachments/42" \
-H "Authorization: Bearer ..."
DELETE https://api.safetyamp.com/api/incidents/<incident_id>/attachments/<attachment_id>
Responds 204. Soft-delete — the row persists with deleted_at set. Returns 422 if the attachment is not attached to the given incident.
Incident Comments
Comments are threaded to an incident with author + timestamp + rich body. Both plain strings and structured rich-text JSON are accepted; the server returns the resolved rich-text structure on read.
All comment endpoints require can_administer_incidents or can_investigate_incidents. Additionally, only the original author can update a comment. Any Company Administrator (or the author) can delete a comment.
List Comments
curl "https://api.safetyamp.com/api/incidents/x6Ad2/comments" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 12,
"commentable_id": "x6Ad2",
"commentable_type": "incident",
"template_revision_element_id": null,
"form_section_id": null,
"body": {
"version": 1,
"type": "comment",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Adjuster called back — awaiting quote."
}
]
}
]
},
"uuid": null,
"created_by": "5V4A2",
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"updated_by": "5V4A2",
"deleted_at": null,
"deleted_by": null,
"creator": {
"id": "5V4A2",
"first_name": "Jane",
"last_name": "Doe",
"// … remaining user fields": "see Object Reference"
}
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incidents/<incident_id>/comments
Create a Comment
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/comments" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "body": "Reviewed footage." }'
HTTP Request
POST https://api.safetyamp.com/api/incidents/<incident_id>/comments
Request Body
| Field | Description |
|---|---|
| body | (Required) Either a plain-text string, or a JSON string containing a structured comment (TipTap/ProseMirror shape). Max 65535 chars. Plain strings are wrapped in a single paragraph on read. |
| uuid | Optional client-supplied UUID for idempotency. |
A plain-text body ({ "body": "Reviewed footage." }) is wrapped into a single paragraph on read.
For rich formatting — including user mentions that fire notification emails — body may instead be a JSON string encoding a TipTap/ProseMirror document. When decoded, the payload structure is:
{
"version": 1,
"type": "comment",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Reviewed "
}
]
}
]
}
Serialize the above with JSON.stringify (or your language's equivalent) and send the resulting string as body. A mention node with an encoded user id sends that user a notification email when the comment is created.
Returns 422 with body: "json is malformed" if body starts to look like JSON but doesn't parse.
Update a Comment
Only the original author can update.
curl -X PATCH "https://api.safetyamp.com/api/incidents/x6Ad2/comments/12" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "body": "Reviewed footage — filed follow-up." }'
HTTP Request
PATCH https://api.safetyamp.com/api/incidents/<incident_id>/comments/<comment_id>
Delete a Comment
curl -X DELETE "https://api.safetyamp.com/api/incidents/x6Ad2/comments/12" \
-H "Authorization: Bearer ..."
DELETE https://api.safetyamp.com/api/incidents/<incident_id>/comments/<comment_id>
Responds 204. Returns 403 if the caller is neither the author nor a Company Administrator.
Involved Persons
An involved person is anyone connected to an incident — the primary injured worker, a witness, a manager, and so on. Every involved person is tagged with a type_id from the involved-person-type lookup.
An involved person can either link to an existing user (user_id) or be freeform (name). Exactly one involved person per incident may have type_id = 2 (Primary Involved Person).
Read endpoints require can_administer_incidents or can_investigate_incidents. Create/update/delete are policed by the parent incident's edit permission. Fields marked "case-and-medical" below only appear in responses and are only writable when the caller holds can_administer_incidents or can_manage_case_and_medical_data.
List Involved Persons
curl "https://api.safetyamp.com/api/incidents/x6Ad2/involved_persons" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 4,
"incident_id": "x6Ad2",
"user_id": "5V4A2",
"type_id": 2,
"name": "Jane Doe",
"user_type": "Employee",
"injury": 1,
"illness": null,
"// … narrative fields": "before_incident_description, injury_description, injury_occurrence_description, harming_object_description, notes",
"// … case-and-medical fields": "drug_test/alcohol_test + _at, ppe_used, treated_onsite, medical_facility_id, physician_name, treatment_facility_*, emergency_room, ambulance, seen_at_clinic, in_patient, treatment_medical, treatment_description, illness_classification, estimated_restricted/lost_days, death, death_date, classification_dates",
"user": { "id": "5V4A2", "first_name": "Jane", "last_name": "Doe" },
"type": { "id": 2, "name": "Primary Involved Person" },
"attachments": [],
"injuries": [ /* Injury objects — see the Create body for shape */ ],
"ppe": [],
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"created_by": "5V4A2",
"updated_by": "5V4A2"
}
]
}
Non-investigator callers see only the row with type_id = 2 (the primary involved person).
HTTP Request
GET https://api.safetyamp.com/api/incidents/<incident_id>/involved_persons
Get an Involved Person
GET https://api.safetyamp.com/api/incidents/<incident_id>/involved_persons/<involved_person_id>
Additional fields loaded on show: full injuries[].type, injuries[].location, and classification_dates (grouped into away_from_work and restricted when the caller can see case-and-medical fields).
Response:
{
"data": {
"id": 4,
"incident_id": "x6Ad2",
"user_id": "5V4A2",
"type_id": 2,
"name": "Jane Doe",
"user_type": "Employee",
"injury": 1,
"illness": null,
"// … narrative fields": "before_incident_description, injury_description, injury_occurrence_description, harming_object_description, notes",
"// … case-and-medical fields": "drug_test/alcohol_test + _at, ppe_used, treated_onsite, medical_facility_id, physician_name, treatment_facility_*, emergency_room, ambulance, seen_at_clinic, in_patient, treatment_medical, treatment_description, illness_classification, estimated_restricted/lost_days, death, death_date, classification_dates",
"user": { "id": "5V4A2", "first_name": "Jane", "last_name": "Doe" },
"type": { "id": 2, "name": "Primary Involved Person" },
"attachments": [],
"injuries": [ /* Injury objects — see the Create body for shape */ ],
"ppe": [],
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"created_by": "5V4A2",
"updated_by": "5V4A2"
}
}
Create an Involved Person
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/involved_persons" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"user_id": "5V4A2",
"type_id": 2,
"user_type": "Employee",
"injury": 1,
"injuries": [
{
"injury_type_model": "default",
"injury_type_id": 4,
"location_model": "default",
"location_id": 6,
"side_of_body": "Right"
}
]
}'
HTTP Request
POST https://api.safetyamp.com/api/incidents/<incident_id>/involved_persons
Request Body
| Field | Description |
|---|---|
| user_id | User ID (encoded) linking this involved person to a user record. Required if name is not sent. When set, name/address/phone/hire-date are copied from the user record. |
| name | Freeform name. Required if user_id is not sent. |
| type_id | (Required) Integer ID from the involved-person-type lookup. 2 = Primary Involved Person, 3 = Witness, 1 = Reporter, 4 = Secondary Involved Person, 5 = Manager. |
| user_type | One of Other, Private, Employee, Contractor. Defaults to Employee. |
| job_title | String. |
| hired_date | Date, YYYY-MM-DD. |
| company_name | String, max 255. |
| contact_info | String, max 255. Freeform contact block for freeform involved persons. |
| signature | Base64-encoded signature or image data, up to 5000 chars. |
| injury | Boolean. When 1, injuries[], emergency_room, and in_patient become required. |
| illness | Boolean. When 1, illness_classification becomes required. |
| injuries | Array of injury objects (see below). |
| injuries[].injury_type_model | default or account. |
| injuries[].injury_type_id | Integer ID from incident_injury_types (account) or the seeded defaults. |
| injuries[].location_model | default or account. |
| injuries[].location_id | Integer ID from incident_injury_locations (account) or the seeded defaults. |
| injuries[].side_of_body | One of Upper, Lower, Bilateral, Left, Center, Right, N/A. |
| drug_test / alcohol_test | 1 (Yes), 2 (No), 3 (Unsure), or null. |
| drug_test_at / alcohol_test_at | Date, YYYY-MM-DD. |
| ppe_used | 1 / 2 / 3 / null. |
| ppe | Array of { id } or { name } entries selecting or creating PPE items. |
| before_incident_description, injury_description, injury_occurrence_description, harming_object_description | Free-text narrative fields. |
| notes | Free-text notes. |
| time_work_began | Time, HH:MM:SS. |
Case-and-medical fields (require can_administer_incidents or can_manage_case_and_medical_data):
| Field | Description |
|---|---|
| medical_facility_id | Encoded ID of a medical facility record. |
| treated_onsite, treatment_medical, ambulance, seen_at_clinic, emergency_room, in_patient, loss_of_consciousness | Booleans. |
| treatment_description | Free text. |
| physician_name | String, max 254. |
| treatment_facility, treatment_facility_street, treatment_facility_street2, treatment_facility_city, treatment_facility_state, treatment_facility_zip_code, treatment_facility_country | Freeform treating-facility address (used when medical_facility_id is not applicable). |
| illness_classification | One of SkinDisorder, RespiratoryCondition, Poisoning, HearingLoss, AllOtherIllnesses. Required when illness=1. |
| death, death_date | Boolean and date. |
| estimated_restricted_days, estimated_lost_days | Integers. |
| classification_dates.restricted[] / classification_dates.away_from_work[] | Arrays of { case_classification, start_date, end_date, notes } marking OSHA case-classification date ranges. case_classification must be one of Death, DaysAwayFromWork, JobTransferOrRestriction, OtherRecordableCases. |
Returns 422 with An involved person has already been assigned to this incident if you try to create a second row with type_id = 2.
Update an Involved Person
Same body shape as create. id on nested injuries, ppe, and classification_dates entries lets you update rows in place; entries missing an existing ID are treated as new; existing rows not present in the payload are removed.
Non-investigator callers have their update payload whitelisted to user_type, type_id, contact_info, company_name, and name. Other fields are dropped without error.
PATCH https://api.safetyamp.com/api/incidents/<incident_id>/involved_persons/<involved_person_id>
Delete an Involved Person
DELETE https://api.safetyamp.com/api/incidents/<incident_id>/involved_persons/<involved_person_id>
Responds 204. Soft-delete.
Root Cause Analyses
A Root Cause Analysis (RCA) is a structured investigation session attached to an incident. Three analysis types are supported:
0— Contributing Factor Analysis. Tree-shaped, driven by a template.1— Five Whys. Linear chain.2— Plain Text. Free-form summary.
The specific structure of the analysis is stored in a config array — the API accepts any array shape here, so clients are responsible for producing the correct nested structure for the chosen type.
All RCA endpoints require can_administer_incidents or can_investigate_incidents, plus edit-scope on the parent incident.
List RCAs
curl "https://api.safetyamp.com/api/incidents/x6Ad2/incident_rcas" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 3,
"type": 2,
"title": "Loading dock slip",
"config": {
"body": "Employee slipped on wet floor at loading dock."
},
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"created_by": "5V4A2",
"updated_by": "5V4A2",
"incident_id": "x6Ad2",
"action_items": null
}
],
"includes": [],
"meta": {
"total": 1,
"current_page": 0,
"per_page": 25,
"last_page": 0
}
}
HTTP Request
GET https://api.safetyamp.com/api/incidents/<incident_id>/incident_rcas
Get an RCA
GET https://api.safetyamp.com/api/incidents/<incident_id>/incident_rcas/<rca_id>
Create an RCA
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/incident_rcas" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"type": 0,
"title": "Loading dock slip",
"config": [ { "id": 0, "order": 1, "text": "Wet floor", "children": [] } ],
"users": [ { "id": "5V4A2" } ]
}'
HTTP Request
POST https://api.safetyamp.com/api/incidents/<incident_id>/incident_rcas
Request Body
| Field | Description |
|---|---|
| type | (Required) Integer, 0 (Contributing Factor Analysis), 1 (Five Whys), or 2 (Plain Text). |
| title | (Required) String, max 255. |
| config | (Required) Array. Structure depends on type — the server does not validate the inner shape. |
| users | Array of { id } entries — user IDs (encoded) of the RCA participants. |
| action_items | Object { add: [...], remove: [...] } linking action items into the analysis. add[] items are { id, node_id }; remove[] items are just id. Applied as a delta against the existing set. |
Update an RCA
Same shape as create. type cannot be changed — attempting to changes it returns 422 Cannot change analysis type.
PATCH https://api.safetyamp.com/api/incidents/<incident_id>/incident_rcas/<rca_id>
Delete an RCA
DELETE https://api.safetyamp.com/api/incidents/<incident_id>/incident_rcas/<rca_id>
Responds 204.
Incident Costings
Costings are line items tracking the dollar impact of an incident (medical costs, equipment damage, lost production, etc). Each row has an amount, a costing_type (either a system-provided default or an account-defined category), and a potential flag for estimated vs actual.
All costing endpoints require can_administer_incidents or can_investigate_incidents.
List Costings
curl "https://api.safetyamp.com/api/incidents/x6Ad2/incident_costings" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 8,
"incident_id": 1,
"costing_type_model": "account",
"costing_type_id": 4,
"amount": "1250.00",
"potential": 0,
"description": null,
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"deleted_at": null,
"created_by": "5V4A2",
"updated_by": "5V4A2",
"deleted_by": null,
"costing_type": {
"id": 4,
"title": "Medical Costs",
"category": 1,
"created_at": "2025-09-19 15:41:16",
"updated_at": "2025-09-19 15:41:16",
"deleted_at": null,
"created_by": "5V4A2",
"updated_by": "5V4A2",
"deleted_by": null
}
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incidents/<incident_id>/incident_costings
Create a Costing
curl -X POST "https://api.safetyamp.com/api/incidents/x6Ad2/incident_costings" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"amount": 1250.00,
"costing_type_model": "default",
"costing_type_id": 3,
"potential": false,
"description": "Medical bills"
}'
HTTP Request
POST https://api.safetyamp.com/api/incidents/<incident_id>/incident_costings
Request Body
| Field | Description |
|---|---|
| amount | (Required) Numeric dollar amount. Currency is implicit (account-level). |
| costing_type_model | (Required) default or account. |
| costing_type_id | (Required) Integer ID from costing_default_types or the account's costing_types. |
| potential | Boolean. true marks the row as an estimated potential cost rather than an actual one. |
| description | Optional free-text description. |
Update / Delete
PATCH /api/incidents/<incident_id>/incident_costings/<costing_id> — same body shape as create.
DELETE /api/incidents/<incident_id>/incident_costings/<costing_id> — responds 204.
Lookups
Lookup tables used when creating or filtering incidents. All Company Administrators bypass permission checks on these — customers integrating with a Company Administrator user can freely list any of them.
Incident Types
Incident types are the classification a customer picks when filing an incident — Injury, Vehicle Accident, Spill, and so on. The set is a merge of twelve SafetyAmp-provided defaults and any account-authored additions (which can shadow a default via default_type_id).
Type definitions are managed through the SafetyAmp UI, not the public API. The list endpoint below returns them so you can look up incident_type_id values before setting types[] on an incident.
List Incident Types
curl "https://api.safetyamp.com/api/incident_types" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 3,
"default_type_id": null,
"name": "Behavior",
"model": "default",
"disabled": 0,
"templates": []
}
]
}
Each row is either the account's customization (model: "account") or an unmodified default (model: "default"). templates[] lists the form templates that will be attached when an incident of that type is filed.
HTTP Request
GET https://api.safetyamp.com/api/incident_types
Filter Keys
| Key | Type | Notes |
|---|---|---|
disabled |
boolean | 0 or 1. |
Sort Keys
| Key |
|---|
name |
Default sort: name ascending.
Injury Types
The set of injury types available when marking an involved person as injured. Twelve seeded defaults (Cut, Laceration, Puncture, Abrasion, Strain, Sprain, Dislocation, etc.) plus any account-defined additions.
Reads are available to any authenticated user. Writes require company_administration.
List Injury Types
curl "https://api.safetyamp.com/api/incident_injury_types" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 6,
"default_type_id": null,
"name": "Sprain",
"model": "default",
"disabled": 0
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incident_injury_types
Filter Keys
| Key | Type | Notes |
|---|---|---|
disabled |
boolean | 0 or 1. |
Sort Keys
| Key |
|---|
name |
Create / Update / Delete
POST /api/incident_injury_types
PATCH /api/incident_injury_types/<id>
DELETE /api/incident_injury_types/<id>
| Field | Description |
|---|---|
| name | (Required) String. |
| default_type_id | Optional. Marks this row as a customization of a default. |
| disabled | Boolean. |
Injury Locations
Body-location taxonomy — Head, Neck, Shoulder, Torso, Back, Arm, Hand, Leg, Foot, and so on. Each location is optionally tagged with a coarse body_part grouping.
Reads are available to any authenticated user. Writes require company_administration.
List Injury Locations
curl "https://api.safetyamp.com/api/incident_injury_locations" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 1,
"default_location_id": null,
"name": "Head",
"model": "default",
"disabled": 0,
"body_part": 0
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incident_injury_locations
Filter Keys
| Key | Type | Notes |
|---|---|---|
disabled |
boolean | 0 or 1. |
Sort Keys
| Key |
|---|
name |
Create / Update / Delete
POST /api/incident_injury_locations
PATCH /api/incident_injury_locations/<id>
DELETE /api/incident_injury_locations/<id>
| Field | Description |
|---|---|
| name | (Required) String. |
| default_location_id | Optional. Marks this row as a customization of a default. |
| disabled | Boolean. |
| body_part | Integer 0..8 — 0 Head, 1 Neck, 2 Shoulder, 3 Torso, 4 Back, 5 Arm, 6 Hand, 7 Leg, 8 Foot. If default_location_id is set, this value is overwritten from the default's own body_part. |
Accident Classifications
Account-defined labels describing how an accident occurred — e.g. "Slip and Fall", "Struck By", "Chemical Exposure". Entirely account-authored; there are no seeded defaults.
Reads are available to any authenticated user. Writes require can_administer_incidents.
List Accident Classifications
curl "https://api.safetyamp.com/api/accident_classifications" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 1,
"name": "Chemical Exposure",
"disabled": false,
"created_at": "2025-09-19 15:41:16",
"created_by": "5V4A2",
"updated_at": "2025-09-19 15:41:16",
"updated_by": "5V4A2",
"deleted_at": null,
"deleted_by": null
}
],
"meta": {
"total": 1,
"current_page": 1,
"per_page": 25,
"last_page": 1
}
}
HTTP Request
GET https://api.safetyamp.com/api/accident_classifications
Filter Keys
| Key | Type | Notes |
|---|---|---|
disabled |
boolean | 0 or 1. |
Sort Keys
| Key |
|---|
name |
Create / Update / Delete
POST /api/accident_classifications
PATCH /api/accident_classifications/<id>
DELETE /api/accident_classifications/<id>
| Field | Description |
|---|---|
| name | (Required) String, max 254 chars. |
| disabled | Boolean. |
Involved Person Types Lookup
Read-only lookup — the seeded set of involved-person types.
List
curl "https://api.safetyamp.com/api/incident_involved_person_types" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 1,
"name": "Reporter"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incident_involved_person_types
Read-only — no create/update/delete.
Involved Person Employment Statuses Lookup
Read-only lookup — the seeded set of employment statuses used on involved persons.
List
curl "https://api.safetyamp.com/api/incident_involved_person_employment_statuses" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 1,
"name": "Employee"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/incident_involved_person_employment_statuses
Read-only — no create/update/delete.
Custom Fields
Custom fields are account-defined attributes that attach optional data to incidents. Each field has a source_type that controls which incidents it appears on:
incident— the field applies to every incident in the account.incident_type— the field only applies to incidents of certain classifications.
Seven field types are supported: ShortText, LongText, Date, Time, DateTime, MultipleChoice, and Upload.
Field definitions are managed through the SafetyAmp UI, not the public API. The list endpoint below returns your account's definitions so you can populate custom_field_answers when creating or updating incidents.
List Custom Fields
curl "https://api.safetyamp.com/api/custom_fields" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": 5,
"type": "MultipleChoice",
"title": "Root cause detail",
"hint": null,
"order": 0,
"config": {
"required": false
},
"choices": [
{
"id": 12,
"custom_field_id": 5,
"name": "Slip/Trip",
"// ... 10 fields total; see corresponding section": null
}
],
"source_type": "incident",
"connections": [
{
"id": 5,
"custom_field_id": 5,
"connection_id": null,
"connection_type": "incident"
}
]
}
],
"includes": [],
"meta": {
"total": 1,
"current_page": 0,
"per_page": 25,
"last_page": 0
}
}
HTTP Request
GET https://api.safetyamp.com/api/custom_fields
Filter Keys
| Key | Type | Notes |
|---|---|---|
source_type |
enum | incident or incident_type. |
Sort Keys
| Key |
|---|
order |
Include Keys
| Key | What it adds |
|---|---|
choices |
Choice options for MultipleChoice fields. |
connections |
Rows mapping this field to specific incident types (only meaningful when source_type=incident_type). |
connections.resource |
Nested — the Type / DefaultType object each connection points at. |
Claims
Claims are insurance claims (workers' comp, auto, general liability, property damage) filed against an incident. A claim records the person involved, event context, medical/employment info, and can be pushed to an integrated claims broker.
Claims layer on top of incidents: they require the incidents feature (has_incidents) and additionally the claims feature (has_claims). Both settings are on the customer's account; if either is off, every endpoint below returns 404.
Permissions
Every claim endpoint (including carriers, medical facilities, contacts, and claim attachments/comments) requires the can_administer_claims permission. There are no granular scopes — either you can administer claims for the account, or you can't touch them.
| Permission slug | In-app name | What it grants |
|---|---|---|
can_administer_claims |
Can Administer Claims | Create, view, edit, and delete every claim in the account. Also required for carriers, medical facilities, contacts, and claim attachments/comments. |
Users assigned the Company Administrator role bypass this check.
Site restrictions still apply — a user restricted from a site cannot see or write claims tied to incidents at that site.
List Claims
curl "https://api.safetyamp.com/api/claims" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json"
Response (fields trimmed for brevity — see "Get a Claim" for the full shape):
{
"data": [
{
"id": "5V4A2",
"type": "workers_comp",
"status": "open",
"case_number": "2026-0001",
"external_claim_number": null,
"incident_id": "R2wvV",
"initiated_on": "2026-03-30",
"integration_id": null,
"carrier_contact_id": null,
"sync_status": null,
"last_synced": null,
"pip_user_id": "5V4A2",
"pip_name": "Hampus Backman",
"site_id": 1,
"event_date": "2026-02-11",
"created_at": "2026-03-30 16:08:50",
"created_by": 1,
"updated_at": "2026-03-30 16:11:37",
"updated_by": 1
}
],
"meta": {
"total": 1,
"current_page": 0,
"per_page": 25,
"last_page": 0
}
}
HTTP Request
GET https://api.safetyamp.com/api/claims
Filter Keys
| Key | Type | Notes |
|---|---|---|
status |
enum | open or closed. |
sync_status |
enum | queued or sent. |
site_id |
integer | Site DB id. |
incident_id |
string | Encoded incident ID. |
pip_user_id |
string | Encoded user ID of the primary involved person. |
integration_id |
string | Encoded integration ID. |
carrier_contact_id |
string | Encoded contact ID. |
Sort Keys
| Key |
|---|
site |
created_at |
updated_at |
case_number |
employee_last_name |
created_by_last_name |
incident_case_number |
Include Keys
| Key | What it adds |
|---|---|
creator |
Basic user object for the claim's creator. |
incident |
The full incident payload (see Incidents section). |
carrier_contact |
Contact object attached to the claim's carrier. |
integration |
Integration record (name, type, settings). |
integration.carrier |
Nested — the carrier belonging to the integration. |
Each key accepts a :count suffix (e.g. ?includes[]=incident:count) to return just the relation count instead of the full payload.
List Response Scope
Callers without can_administer_incidents see only claims whose site_id is one of their accessible sites. Callers with can_administer_incidents see every claim in the account.
Get a Claim
curl "https://api.safetyamp.com/api/claims/5V4A2" \
-H "Authorization: Bearer ..."
Response (full shape):
{
"data": {
"id": "5V4A2",
"type": "workers_comp",
"status": "open",
"case_number": "2025-0001",
"external_claim_number": null,
"incident_id": "x6Ad2",
"initiated_on": "2025-09-19",
"integration_id": null,
"carrier_contact_id": null,
"contact_user_id": "5V4A2",
"sync_status": null,
"last_synced": null,
"event_date": "2025-09-19",
"event_time": "10:00:00",
"report_date": "2025-09-19",
"report_time": "10:00:00",
"site_id": 1,
"site_name": "Charlottesville",
"site_state": "VA",
"site_city": "Charlottesville",
"location_id": null,
"location_name": null,
"location_description": null,
"description": "Employee slipped on wet floor at loading dock.",
"claim_notes": null,
"// … primary-involved-person fields": "pip_user_id, pip_name, pip_user_type, pip_job_title, pip_emp_id, pip_department, pip_hire_date, pip_date_of_birth, pip_gender, pip_street/_2, pip_city, pip_state, pip_zip_code, pip_country, pip_home_phone, pip_work_phone, pip_nature_of_claim, pip_insurance_company, pip_insurance_company_phone_number, pip_policy_number, pip_policy_holder_name",
"// … narrative fields": "before_incident_description, injury_description, injury_occurrence_description, harming_object_description",
"// … case classification fields": "injuries, accident_classifications, illness_classification, employment_status, state_of_hire, payroll_state, started_current_position, performing_regular_job, hours_worked_per_day, time_work_began, full_wages_on_date_of_injury, total_days_away_from_work, restricted_work, death, death_date",
"// … medical fields": "in_patient, ambulance, emergency_room, seen_at_clinic, treated_onsite, loss_of_consciousness, date_of_treatment, physician_name, treatment_description, facility_type, facility_name, facility_street/_2, facility_city, facility_state, facility_zip_code, facility_country, facility_phone_number",
"// … off-premise fields (null unless occurred_off_premise=1)": null,
"occurred_off_premise": 0,
"created_at": "2025-09-19 15:41:16",
"created_by": 1,
"updated_at": "2025-09-19 15:41:16",
"updated_by": 1,
"vehicles": [],
"witnesses": [],
"property_damages": [],
"medical_visits": [],
"attachments": [],
"comments": [],
"action_items": [],
"integration": null,
"carrier_contact": null,
"creator": { "id": "5V4A2", "first_name": "Jane", "last_name": "Doe" }
}
}
HTTP Request
GET https://api.safetyamp.com/api/claims/<id>
The show endpoint eager-loads every sub-collection (vehicles, witnesses, property damages, medical visits, attachments, comments, action items) plus the integration and its carrier. Sub-collections are inlined into the response rather than returned as a separate includes block.
Draft a Claim from an Incident
Generate an unsaved draft claim pre-populated from an incident. Useful for building a "new claim" form — take the response, let the user tweak it, then POST it back to /api/claims to persist.
curl -X POST "https://api.safetyamp.com/api/claims/make" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "incident_id": "R2wvV", "type": "workers_comp" }'
HTTP Request
POST https://api.safetyamp.com/api/claims/make
Request Body
| Field | Description |
|---|---|
| incident_id | (Required) Encoded ID of the source incident. |
| type | (Required) One of auto, general, property_damage, workers_comp. |
| integration_id | Optional encoded ID of a claims integration to associate this claim with once created. |
Returns a hydrated (but not persisted) Claim payload matching the shape of "Get a Claim". Fields are populated from the incident: site, event date/time, off-premise, description, primary involved person's user + address + phone + hire date + DOB, injuries, accident classifications, days-away calculations, restricted-work state. Medical visits are derived from the involved person's medical fields when available.
Create a Claim
curl -X POST "https://api.safetyamp.com/api/claims" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"incident_id": "R2wvV",
"type": "workers_comp",
"pip_user_id": "5V4A2",
"pip_name": "Hampus Backman",
"contact_user_id": "5V4A2",
"occurred_off_premise": false,
"event_date": "2026-02-11",
"site_id": 1,
"description": "Slipped and fell in the parking lot."
}'
HTTP Request
POST https://api.safetyamp.com/api/claims
Request Body — Required Fields
| Field | Description |
|---|---|
| incident_id | (Required) Encoded incident ID. The parent incident must exist and the caller must have permission to update it. |
| type | (Required) One of auto, general, property_damage, workers_comp. Immutable once set. |
| pip_user_id | (Required) Encoded user ID of the primary involved person. |
| pip_name | (Required) Display name of the primary involved person, max 255. |
| contact_user_id | (Required) Encoded user ID of the internal contact for this claim. |
| occurred_off_premise | (Required) Boolean. |
Request Body — Optional Scalars
| Field | Description |
|---|---|
| claim_notes, description, before_incident_description, injury_occurrence_description, injury_description, harming_object_description, off_premise_note, time_work_began | Free-text narrative fields. |
| external_claim_number | External reference from the broker, string max 255. |
| pip_job_title, pip_emp_id, pip_department, pip_nature_of_claim, pip_insurance_company, pip_insurance_company_phone_number, pip_policy_number, pip_policy_holder_name | Primary involved person identifying info, strings. |
| pip_street, pip_street_2, pip_city, pip_state, pip_zip_code, pip_country, pip_home_phone, pip_work_phone | Primary involved person address / phone. |
| pip_date_of_birth, pip_hire_date | Dates, YYYY-MM-DD. |
| pip_gender | Integer, 0 or 1. |
| site_id, location_id | Integer IDs referencing site and location records. |
| site_name, site_city, site_state, location_name, location_description | Denormalized site/location strings (max 255). |
| event_date, report_date, initiated_on, started_current_position, death_date | Dates, YYYY-MM-DD. |
| event_time, report_time | Times, HH:MM:SS. |
| occurred_off_premise, off_premise_street, off_premise_street_2, off_premise_city, off_premise_state, off_premise_zip_code, off_premise_country, off_premise_phone_number | Off-premise address block, strings. |
| injuries, accident_classifications, illness_classification, state_of_hire, payroll_state | Free-text summary fields, strings. |
| death, full_wages_on_date_of_injury, restricted_work, performing_regular_job | Booleans. |
| total_days_away_from_work | Integer, ≤180. |
| hours_worked_per_day | Integer, ≤24. |
| employment_status | One of full_time, part_time, seasonal, temporary, unemployed, unknown. |
| integration_id | Encoded ID of a claims integration. When set, sync_status is force-initialized to queued. |
| carrier_contact_id | Encoded ID of a contact (adjuster) attached to a carrier. |
Request Body — Nested Sub-Collections
witnesses[] — witness objects.
| Field | Description |
|---|---|
name |
(Required) String, max 255. |
user_id |
Encoded user ID. |
street, street_2, city, state, zip_code, country, phone_number |
Address and contact. |
notes |
Free-text. |
property_damages[] — property damage objects. Only synced when the claim's type is auto, general, or property_damage.
| Field | Description |
|---|---|
title |
(Required) String, max 255. |
repair_cost, replacement_cost, amount_claimed |
Numeric. |
vehicles[] — vehicle objects. Only synced when the claim's type is auto.
| Field | Description |
|---|---|
type |
(Required) primary or secondary. |
make, model |
String. |
year |
4-digit integer. |
license_plate, vin |
String. |
number_of_passengers |
Integer, ≤100. |
names_of_passengers |
String. |
driver_user_id |
Integer DB id (not encoded). |
driver_street, driver_street_2, driver_city, driver_state, driver_zip_code, driver_country, driver_phone_number |
Driver address and contact. |
driver_license_no, driver_license_state |
String. |
medical_visits[] — medical visit objects.
| Field | Description |
|---|---|
treatment_type |
One of initial, follow_up, specialist. |
treatment_date |
Date. |
treatment_tz |
Timezone string. |
doctor_name |
String. |
facility_type |
physician or hospital_clinic. |
medical_facility_id |
Encoded ID. |
phone_number, notes |
String. |
treated_on_site, loss_of_consciousness, ambulance, emergency_room, seen_at_clinic, in_patient |
Booleans. |
answers[] — Broadspire integration answers. Only synced when the linked integration is Broadspire; silently dropped otherwise.
| Field | Description |
|---|---|
key |
(Required) String matching a Broadspire answerable key. |
value |
String. |
Server-Controlled Fields
case_number, sync_status, last_synced, status (open/closed), id, created_at, created_by, updated_at, updated_by cannot be set by the client. case_number is generated as YYYY-#### (creation year + incrementing counter). sync_status transitions to queued when an integration is attached, and to sent when a sync is triggered.
Update a Claim
Same body shape as create, with these differences:
incident_idandtypeare immutable — omit them; sending them has no effect after creation.statusis accepted here and can be flipped betweenopenandclosed.employment_statusaccepts any string (the enum restriction from create is not enforced on update).- Sub-collections (
witnesses,property_damages,vehicles,medical_visits,answers) support in-place updates when each row carries itsid. Rows in the DB but not present in the payload are removed (formedical_visits) or detached (forwitnesses/property_damages/vehicles). medical_visits.*.treatment_typeand.facility_typebecome required.- The
answers[]block is only accepted when the claim is linked to a Broadspire integration;answers[].keymust match one of the integration's answerable keys or the request returns422. For non-Broadspire claims,answersis ignored.
curl -X PATCH "https://api.safetyamp.com/api/claims/5V4A2" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "claim_notes": "Adjuster contacted", "status": "open" }'
HTTP Request
PATCH https://api.safetyamp.com/api/claims/<id>
Delete a Claim
curl -X DELETE "https://api.safetyamp.com/api/claims/5V4A2" \
-H "Authorization: Bearer ..."
DELETE https://api.safetyamp.com/api/claims/<id>
Responds 204. Soft-delete — the row persists with deleted_at set. Sub-collections (vehicles, witnesses, property_damages, medical_visits, attachments, comments) are not cascade-deleted; they remain in the database.
List Claim Integrations
Lists the account's claims-broker integrations. Each row is the raw integration record plus its linked carrier.
curl "https://api.safetyamp.com/api/claims/integrations" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": "8bZ4y",
"name": "Broadspire",
"settings": { /* provider-specific config */ },
"integration_type": "BroadSpireSftpSync",
"carrier": {
"id": "aC2xD",
"name": "Broadspire Insurance",
"claim_type": "workers_comp",
"integration_id": "8bZ4y",
"contacts": []
},
"answerable_fields": [
{
"name": "employee_ssn",
"title": "Employee SSN",
"description": "…",
"key": "employee_ssn",
"required": true,
"default": null,
"options": null,
"answer": null
}
]
}
]
}
Currently only Broadspire integrations (integration_type = BroadSpireSftpSync) are returned. answerable_fields describes the extra fields the broker requires — these values are supplied via the answers[] array on POST / PATCH /api/claims and returned back here as answer.value when set on a specific claim.
HTTP Request
GET https://api.safetyamp.com/api/claims/integrations
Sync a Claim to Its Integration
Queue an asynchronous push of the claim to its linked integration (currently: Broadspire via SFTP). The endpoint takes no request body — the claim's linkage and payload live on the claim itself.
curl -X POST "https://api.safetyamp.com/api/claims/5V4A2/sync-integration" \
-H "Authorization: Bearer ..."
HTTP Request
POST https://api.safetyamp.com/api/claims/<claim_id>/sync-integration
Returns the updated claim resource, with sync_status flipped to sent. The actual delivery to the broker happens out-of-band; a 200 response means "queued for delivery", not "acknowledged by the broker."
Returns 409 Conflict with the message Claim already submitted to Broadspire if the claim's current sync_status is not queued. To re-sync a claim after edits, the linkage must go through the sync_status = queued state again — this is currently only achieved by attaching a fresh integration_id.
Claim Attachments
Two options: link to a file you host, or upload the file directly.
Attachments are read as part of the parent claim (GET /api/claims/<id> returns them in the attachments[] array). There is no GET /api/claims/<id>/attachments/<attachment_id> endpoint.
Add an Attachment
Attach by URL (JSON):
curl -X POST "https://api.safetyamp.com/api/claims/5V4A2/attachments" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"file_name": "police-report.pdf",
"type": "document",
"external_url": "https://example.com/claims/5V4A2/police-report.pdf"
}'
Upload a file (multipart/form-data):
curl -X POST "https://api.safetyamp.com/api/claims/5V4A2/attachments" \
-H "Authorization: Bearer ..." \
-F "file_name=police-report.pdf" \
-F "type=document" \
-F "attachment=@/path/to/police-report.pdf"
HTTP Request
POST https://api.safetyamp.com/api/claims/<claim_id>/attachments
Request Body
Send exactly one of attachment, external_url, or uuid.
| Field | Description |
|---|---|
| file_name | (Required) A display filename for the attachment, max 255. |
| type | (Required) Short category slug, max 80. Examples: image, document. |
| attachment | (Required if neither external_url nor uuid is set) The file itself, sent via multipart/form-data. |
| external_url | (Required if neither attachment nor uuid is set) The URL of a file you host, up to 2048 chars. Must be reachable by SafetyAmp. |
Delete an Attachment
curl -X DELETE "https://api.safetyamp.com/api/claims/5V4A2/attachments/42" \
-H "Authorization: Bearer ..."
DELETE https://api.safetyamp.com/api/claims/<claim_id>/attachments/<attachment_id>
Responds 204. Returns 422 if the attachment is not attached to the given claim.
Claim Comments
Comments live directly on claims. Author + timestamp + rich body, same TipTap/ProseMirror shape as incident comments (see Incident Comments).
Comments are read as part of the parent claim (GET /api/claims/<id> returns them in the comments[] array). There is no standalone list or show endpoint.
Create a Comment
curl -X POST "https://api.safetyamp.com/api/claims/5V4A2/comments" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "body": "Adjuster returned call." }'
HTTP Request
POST https://api.safetyamp.com/api/claims/<claim_id>/comments
Request Body
| Field | Description |
|---|---|
| body | (Required) Plain string or structured comment JSON, max 65535 chars. |
| uuid | Optional client-supplied UUID for idempotency. |
Update a Comment
Only the original author can update a comment.
curl -X PATCH "https://api.safetyamp.com/api/claims/5V4A2/comments/12" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{ "body": "Adjuster returned call — filed follow-up." }'
PATCH https://api.safetyamp.com/api/claims/<claim_id>/comments/<comment_id>
Delete a Comment
DELETE https://api.safetyamp.com/api/claims/<claim_id>/comments/<comment_id>
Responds 204. Returns 403 if the caller is neither the author nor a Company Administrator.
Medical Facilities
A directory of medical facilities where injured workers get treated. Referenced by medical_facility_id on medical visits and involved persons.
Writes require any of can_administer_claims, can_administer_incidents, or can_manage_case_and_medical_data. Read is available to any authenticated user.
List Medical Facilities
curl "https://api.safetyamp.com/api/medical_facilities" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": "aC2xD",
"name": "UVA Medical Center",
"// … remaining site fields": "see Object Reference"
}
],
"count": 1
}
HTTP Request
GET https://api.safetyamp.com/api/medical_facilities
Get / Create / Update / Delete
GET /api/medical_facilities/<id>
POST /api/medical_facilities
PATCH /api/medical_facilities/<id>
DELETE /api/medical_facilities/<id>
Request body:
| Field | Description |
|---|---|
| name | (Required) Name of the facility, max 255. |
| street | Address, max 255. |
| street_2 | Address line 2, max 255. |
| city | Max 255. |
| state | Free-form state string, max 255 (not an enum). |
| zip_code | Max 255. |
| country | Max 255. |
| phone_number | Max 255 (no format validation). |
| notes | Free text. |
Carriers
Insurance / claims carriers linked to a specific claims integration. Claims reference a carrier's contacts via carrier_contact_id.
Every carrier endpoint requires can_administer_claims.
List Carriers
curl "https://api.safetyamp.com/api/carriers" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": "aC2xD",
"name": "Broadspire Insurance",
"claim_type": "workers_comp",
"integration_id": "8bZ4y"
}
]
}
GET /api/carriers?limit=contacts also includes the carrier's contacts inline.
HTTP Request
GET https://api.safetyamp.com/api/carriers
Include Keys
| Key | What it adds |
|---|---|
contacts |
Array of the carrier's contacts, inlined. Use contacts:count to return the count only. |
Get / Create / Update / Delete
GET /api/carriers/<id> — always includes integration and contacts inline.
POST /api/carriers
PATCH /api/carriers/<id>
DELETE /api/carriers/<id> — soft-deletes the carrier and all its contacts in a single transaction.
Request body:
| Field | Description |
|---|---|
| name | (Required) String, max 255. |
| integration_id | (Required) Encoded ID of a claims integration. There can be only one carrier per integration — attempting to create a second returns 422. |
| claim_type | Optional. One of auto, general, property_damage, workers_comp. |
Add a Contact to a Carrier
Convenience endpoint that creates a contact and attaches it to the given carrier in one step. Under the hood the contact is stored with contactable_id = <carrier id> and contactable_type = "carrier".
curl -X POST "https://api.safetyamp.com/api/carriers/aC2xD/contacts" \
-H "Authorization: Bearer ..." \
-H "Content-Type: application/json" \
-d '{
"first_name": "Jamie",
"last_name": "Adjuster",
"phone_number": "555-123-4567",
"email": "jamie@broadspire.example"
}'
POST /api/carriers/<carrier_id>/contacts
Same body shape as POST /api/contacts — see Contacts below.
Contacts
People — adjusters, brokers, and other claim-adjacent contacts. Today contacts can only be attached to carriers (the polymorphic morph map registers only carrier); every other contactable_type value is unused.
Every contact endpoint requires can_administer_claims, even though the plain /contacts routes sit under the feature.incidents flag rather than feature.claims.
List Contacts
curl "https://api.safetyamp.com/api/contacts?filter_groups[0][filters][0][key]=contactable_id&filter_groups[0][filters][0][value]=<carrier_db_id>&filter_groups[0][filters][0][operator]=eq" \
-H "Authorization: Bearer ..."
Response:
{
"data": [
{
"id": "gH5i6",
"first_name": "Jamie",
"last_name": "Adjuster",
"phone_number": "555-123-4567",
"email": "jamie@broadspire.example",
"contactable_id": "aC2xD",
"contactable_type": "carrier"
}
]
}
HTTP Request
GET https://api.safetyamp.com/api/contacts
Filter Keys
| Key | Type | Notes |
|---|---|---|
contactable_id |
integer | Raw integer DB id of the parent row (e.g. a carrier). See the callout below. |
contactable_type |
string | Currently only carrier. |
Get / Create / Update / Delete
GET /api/contacts/<id>
POST /api/contacts — creates a standalone contact with no parent.
PATCH /api/contacts/<id>
DELETE /api/contacts/<id>
Request body:
| Field | Description |
|---|---|
| first_name | (Required) String, max 255. |
| last_name | (Required) String, max 255. |
| phone_number | Optional string, max 255 (no format validation). |
| Optional string, max 255 (not validated as an email address). |
contactable_id and contactable_type are not accepted through this endpoint. To attach a contact to a carrier, use POST /api/carriers/<carrier_id>/contacts instead.
Object Reference
User
{
"data": {
"id": "6Q4N8",
"owner": false,
"current_supervisor_id": null,
"current_department_id": null,
"email": "chris+conf-homer@safetyamp.com",
"work_phone": "(345)345-3456",
"home_phone": "(234)234-2345",
"mobile_phone": "(123)123-1234",
"avatar_location": null,
"created_at": "2019-10-26 15:37:13",
"created_by": "nRAzE",
"updated_at": "2019-10-26 15:37:13",
"updated_by": "nRAzE",
"deleted_at": null,
"deleted_by": null,
"last_name": "Simpson",
"date_of_birth": "1994-04-15",
"gender": 1,
"street": "street",
"city": "city",
"state": "state",
"zip_code": "24477",
"country": "US",
"timezone": "America/New_York",
"mailing_address_same_as_physical": 0,
"mailing_street": "street2",
"mailing_street2": "apt 22",
"mailing_city": "city2",
"mailing_state": "state2",
"mailing_zip_code": "24472",
"mailing_country": "US2",
"current_title_id": null,
"current_hire_date": null,
"current_employee_status_id": null,
"street2": "apt 20",
"first_name": "Homer",
"middle_name": null,
"nickname": null,
"activated": 0,
"system_access": 1,
"bypass_sso": 0,
"text_opt_out": 0,
"home_site_id": null,
"last_logged_in": null,
"remember_me": 0,
"rate_limit": null,
"constructed_permissions": [],
"current_title": null,
"preferences": null,
"roles": [],
"active_policies": [],
"eligibility_events": [],
"module_permissions": [],
"current_department": null,
"current_supervisor": null,
"sites": [],
"current_status": null,
"permissions": []
}
}
User Format
| Key | Type | Description |
|---|---|---|
| string | The email address of the user (also, the username) | |
| first_name | string | The first name of the user |
| middle_name | string | The middle name of the user |
| last_name | string | The last name of the user |
| owner | boolean | True if the user is the designated account owner |
| home_site_id | int | Id of users home site, controls record access |
| sites | array[] | Ids of sites user should have access to |
| roles | array[] | Ids of roles user should be given, controls system access |
| current_supervisor_id | int | The id of the users supervisor |
| current_department_id | int | The id of the users department |
| current_title_id | int | The id of the users title |
| mobile_phone | string | The users mobile phone number - SafetyAmp can send SMS |
| work_phone | string | The users work phone number |
| home_phone | string | The users home phone number |
| gender | int | 0 for male, 1 for female |
| street | string | User Address: Street address |
| street2 | string | User Address: Street address 2 |
| city | string | User Address: City |
| state | string | User Address: State or Province |
| zip_code | string | User Address: Postal Code |
| country | string | User Address: Country |
| activated | int | 1 for activated, 0 for not activated |
| system_access | int | 1 if the user is allowed the ability to login |
| bypass_sso | int | If SSO enabled for account, set to 1 to allow user to login without SSO |
| text_opt_out | int | 1 if user does not wish to receive SMS on mobile number |
| last_logged_in | string | The time the user last used credentials to login, does not represent last activity in the system |
Timezones
You can find a complete list of supported timezone strings (.e.g, America/New_York) by inspecting any timezone drop-down menu in the SafetyAmp web application.
Errors
Every error response returns a JSON body with a top-level message describing the failure.
{
"message": "Resource not found"
}
Validation errors (422 Unprocessable Entity) additionally include an errors object keyed by request field, each mapping to an array of human-readable messages. The top-level message summarizes the first error and how many others there are.
{
"message": "The event date field is required. (and 4 more errors)",
"errors": {
"event_date": [
"The event date field is required."
],
"event_time": [
"The event time field must be present."
],
"title": [
"The title field is required."
]
}
}
The SafetyAmp API uses the following status codes:
| Status | Meaning |
|---|---|
400 |
Bad Request — Your request is invalid. |
401 |
Unauthorized — Your API key is missing or invalid. |
403 |
Forbidden — You do not have access to the requested resource. |
404 |
Not Found — The specified resource could not be found. Also returned when a feature (e.g. Incidents, Claims) is not enabled on your account. |
405 |
Method Not Allowed — You tried to access a resource with an unsupported HTTP method. |
409 |
Conflict — Your request cannot be completed because it would create a duplicate or conflicts with existing state. |
422 |
Unprocessable Entity — Your request is syntactically correct but one or more fields failed validation. See the errors object for field-level details. |
429 |
Too Many Requests — You've submitted more requests than the rate limit allows. |
500 |
Internal Server Error — We had a problem with our server. Try again later. |
503 |
Service Unavailable — We're temporarily offline for maintenance. Please try again later. |