Assets
The assets API manages your organization's equipment inventory - laptops, monitors, phones, badges - and tracks recovery when someone leaves.
Base path: /api/assets
List assets
GET /api/assets
Needs the assets_view permission - every built-in role has it by default; a custom role that withholds it gets 403.
Query parameters
| Param | Type | Description |
|---|---|---|
limit | integer | Results per page (default 100, max 500) |
offset | integer | Row offset for pagination (default 0) |
There's no status or assignedTo filter on this endpoint - filtering happens client-side, or you can page through everything and filter yourself.
Response 200 OK
{
"rows": [
{
"id": "8f14e45f-...",
"org_id": "org_xyz",
"org_employee_id": "9bb58f26-...",
"item_name": "MacBook Pro 14\"",
"serial_number": "C02XY1234",
"model": "MacBook Pro M3 14-inch",
"category": "laptop",
"condition": "good",
"tracking_number": "",
"return_deadline": "2024-06-15",
"status": "deployed",
"created_at": "2023-09-01T00:00:00.000Z",
"updated_at": "2023-09-01T00:00:00.000Z",
"employee": {
"id": "9bb58f26-...",
"full_name": "Jordan Lee",
"email": "jordan@acme.com",
"employee_id": "EMP-1042",
"department": "Engineering"
}
}
],
"total": 38,
"limit": 100,
"offset": 0
}Fields come straight off the assets table - snake_case throughout, not camelCase. employee is joined in from org_employees and will be null-ish (all fields null) if the asset isn't assigned to anyone.
Asset summary
GET /api/assets/summary
Org-wide rollup used by the unreturned-assets report: totals by status, a breakdown by category, and a recovery timeline of deployed assets by return deadline. Needs the assets_view permission, like the list.
Response 200 OK
{
"totals": { "total": 38, "deployed": 22, "returned": 15, "lost": 1 },
"byCategory": [
{ "category": "laptop", "total": 20, "deployed": 12, "returned": 8 }
],
"recoveryTimeline": [
{ "bucket": "overdue-30d+", "state": "overdue", "count": 0 },
{ "bucket": "overdue-8-30d", "state": "overdue", "count": 1 },
{ "bucket": "overdue-1-7d", "state": "overdue", "count": 3 },
{ "bucket": "due-0-7d", "state": "due", "count": 12 },
{ "bucket": "due-8d+", "state": "due", "count": 6 }
],
"noDeadlineDeployedCount": 4
}recoveryTimeline always has these five buckets in this order - a chronology from the worst overdue through to the furthest out. It only counts assets with status: "deployed" and a return_deadline; days are counted on the org's calendar, and a deadline is still due through its own day. Deployed assets with no deadline are in noDeadlineDeployedCount instead.
Create an asset
POST /api/assets
Admin only.
Request body
{
"itemName": "MacBook Pro 14\"",
"serialNumber": "C02XY1234",
"model": "MacBook Pro M3 14-inch",
"category": "laptop",
"condition": "good",
"orgEmployeeId": "9bb58f26-...",
"trackingNumber": "",
"returnDeadline": "2024-06-15"
}| Field | Required | Description |
|---|---|---|
itemName | ✅ | Device / equipment name |
serialNumber | ✅ | Unique serial or asset tag |
model | - | Model name |
category | - | laptop · monitor · phone · badge · peripheral · other (anything else is coerced to other) |
condition | - | Free text, defaults to good |
orgEmployeeId | - | Assign to an org employee record |
trackingNumber | - | Shipping tracking number |
returnDeadline | - | ISO date string |
New assets are always created with status: "deployed". Fails with 409 if the serial number already exists, and 403 if your plan's asset limit is reached.
Response 201 Created - the created asset, including the joined employee object.
Bulk import assets
POST /api/assets/bulk
Admin only. Imports as many assets as your plan has room for; the rest are reported as skipped rather than rejecting the whole batch.
Request body
{
"assets": [
{ "itemName": "MacBook Pro 14\"", "serialNumber": "C02XY1234", "category": "laptop" },
{ "itemName": "Dell Monitor", "serialNumber": "D5567", "category": "monitor" }
]
}Response 201 Created
{
"inserted": 1,
"skipped": 1,
"errors": [
{ "serialNumber": "D5567", "reason": "Duplicate serial number - already exists" }
]
}Update an asset
PATCH /api/assets/:id
Admin only. This is a full edit, not a partial one - itemName and serialNumber are required on every call, even if unchanged. Accepts the same fields as create, plus status.
Response 200 OK - the updated asset, with the joined employee object.
Mark asset returned
PATCH /api/assets/:id/return
Needs the offboardings_edit permission: Super Admin, Admin or Manager, or a custom role that grants it. Sets status to returned and touches updated_at, writes an audit entry (asset_returned, or asset_found for a lost asset), re-syncs any checklist task linked to the asset, and fires the asset.returned webhook event.
Request body (optional)
{ "found": true }| Field | Type | Description |
|---|---|---|
found | boolean | Required, as true, to return an asset whose status is lost. Without it a lost asset is refused with 409 { "error": "...", "code": "ASSET_LOST" } |
found is the only field read. Any condition or notes you send are ignored; there's no field on the assets table to store return condition or notes today. If you need to record condition at return time, follow up with PATCH /api/assets/:id to set condition separately.
Response 200 OK - the full updated asset row (snake_case, same shape as the list endpoint). An asset that is already returned is left alone: you get the row back with "unchanged": true added, and nothing is audited or announced again.
Returns 404 if the asset isn't in your organisation.
Bulk mark returned
PATCH /api/assets/bulk-return
Same permission as the single return (offboardings_edit). Marks multiple assets returned in one call.
Request body
{ "ids": ["8f14e45f-...", "3c9c9a1e-..."] }Response 200 OK
{ "updated": 2, "assets": [ /* updated asset rows */ ] }Only assets whose status is deployed are updated. Ids that are already returned, recorded as lost, or not in your organisation are skipped silently - compare updated with the number of ids you sent. Bringing a lost asset back is an individual decision: use PATCH /api/assets/:id/return with { "found": true }. One asset_bulk_returned audit entry covers the batch, and each updated asset is announced on the asset.returned webhook event.
Delete an asset
DELETE /api/assets/:id
Admin only.
Response 200 OK
{ "success": true }Returns 409 with "code": "legal_hold" when the asset is assigned to someone whose offboarding is under legal hold (or was released less than 7 days ago). Nothing is deleted; lift the hold first.