API Reference
Assets

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

ParamTypeDescription
limitintegerResults per page (default 100, max 500)
offsetintegerRow 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"
}
FieldRequiredDescription
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 }
FieldTypeDescription
foundbooleanRequired, 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.