Creating, editing, and retiring Assets with the API
The API can do more than look devices up and check them out — it can create an Asset record, edit its details, and retire it into the trashcan.
The API can do more than look devices up and check them out — it can create a device record, edit its details, and retire it into the trashcan. This article covers that full lifecycle, plus three safety features built for automation: advisory warnings, an Idempotency-Key so a retry never duplicates a device, and a dry-run mode so you can test a call before it changes anything.
Examples use curl; set KEY to your API key and PANEL to your panel domain first:
export KEY="k12_live_xxxxxxxxxxxx_yyyy..."
export PANEL="https://<your-panel-domain>"
The permission you need
Creating, editing, and trashing a device all need a key with the assets:write permission — the same permission that already covers editing a device's site, notes, and state. Trashing is deliberately grouped here rather than behind a separate delete permission, because the API never truly deletes a device (see Retiring a device below). An administrator sets a key's permissions under Settings → API Keys.
Creating a device
POST to the assets collection. Only two things are required — a name and a site — and you can identify the site by name or by id, the same flexible lookup used everywhere else in the API:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name": "CB-4021", "site": {"name": "Middle School"}}' \
"$PANEL/api/public/v1/assets"
A successful create returns 201 Created with the new device, including the id the panel assigned it:
{
"id": 4021,
"name": "CB-4021",
"site": { "id": 16, "name": "Middle School" },
"state": "inactive",
"warnings": []
}
You can send more fields in the same call — everything a technician could type on the device's page:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{
"name": "CB-4021",
"site": {"name": "Middle School"},
"serial": "5CD1234XYZ",
"asset_class": {"name": "Chromebook"},
"asset_tag": "INV-00417",
"manufacturer": "HP",
"model": "Chromebook 11 G9",
"notes": "New cart, fall rollout",
"manufacture_date": "2025-08-01"
}' \
"$PANEL/api/public/v1/assets"
A few things are decided for you, on purpose:
- A new device starts
inactive. In k12panel an active device is one that deployments (Blueprints/Modifiers) run against — so a create never silently arms deployment execution. If you do want the device active immediately, send"state": "active"and you'll get an advisory warning back saying so. - You can't set system-owned fields. Telemetry, hardware inventory, sync identifiers, the device's internal secret and guid — none of those are accepted from a create. They're either assigned by the panel or filled in later by the device's agent. Sending them does no harm; they're simply ignored.
- Dates you supply are pinned. If you send a
manufacture_dateorexpiration_date, the panel treats it as authoritative and won't recalculate it. If you omit the expiration date, the panel fills it in using your organization's standard lifecycle rules — the same behavior as the CSV importer.
Serial numbers must be unique
A serial is optional, but if you supply one it has to be unique within your organization. If a device with that serial already exists, the create is rejected with 409 and hands you back the existing device as a candidate, so your script can switch to editing the record it already has instead of creating a duplicate:
{
"error": "serial_conflict",
"message": "An asset with serial '5CD1234XYZ' already exists (id 4021).",
"candidates": [
{ "id": 4021, "name": "CB-4021", "serial": "5CD1234XYZ", "state": "active" }
]
}
This is the same candidate shape the API uses for ambiguous lookups — read the id from candidates[0] and continue.
Never create the same device twice: Idempotency-Key
Networks drop responses. If you POST a create, don't hear back, and retry, you'd normally risk two identical devices. To prevent that, send an Idempotency-Key header — any unique string you generate for the request (a UUID is ideal):
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 5f2b8c14-9a3e-4d21-bb0f-7c1e6a4d9f88" \
-d '{"name": "CB-4022", "site": {"name": "Middle School"}}' \
"$PANEL/api/public/v1/assets"
If a request with that same key arrives again, the API returns the device it already created (with 200 instead of 201) rather than making a second one. Reuse the key freely on a retry; use a fresh key for each genuinely new device.
For a device with a serial, the key is optional — the serial's uniqueness is already a backstop. For a device without a serial, an Idempotency-Key is required: with no serial to dedupe on, it's the only thing that keeps a retry from duplicating. A serial-less create with no key is rejected with 400.
Editing a device
PATCH the device with just the fields you want to change. You can edit its name, serial, asset class, manufacturer, model, description, notes, Internal Tracking ID, static IP, the two dates, its site, and its state:
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"notes": "Returned from repair", "site": {"name": "High School"}, "state": "active"}' \
"$PANEL/api/public/v1/assets/4021"
The response is the updated device, with a warnings list (see below). Changing the serial follows the same uniqueness rule as create — a collision returns 409 with the conflicting device as a candidate.
Warnings: the API tells you about side effects
Some edits are technically fine but worth knowing about. Rather than blocking those edits, the API applies them and returns a non-blocking warnings list on the response. Your automation can log them, surface them to a technician, or ignore them — the operation still succeeded either way. There are three:
code |
When you'll see it | Meaning |
|---|---|---|
sync_managed_field |
You edited a field on a device that's synced from Google Workspace or an MDM | Your change may be overwritten on the next sync. The response names the field. |
activation_runs_modifiers |
You set a device to active |
Being active arms deployment execution — Blueprints/Modifiers will run against it. |
trashed_asset_may_resync |
You trashed a synced device | If it still exists upstream, the next sync may re-create it. |
A response carrying a warning looks like this:
{
"id": 4021,
"name": "CB-4021",
"state": "active",
"warnings": [
{ "code": "sync_managed_field", "field": "site",
"message": "This device's site is managed by sync; your change may be overwritten on the next sync." },
{ "code": "activation_runs_modifiers",
"message": "Setting this device active arms deployment execution — Modifiers (blueprints) will run against it." }
]
}
Warnings appear only on write responses (create, edit, and trash). Plain GET and list responses don't carry them.
Retiring a device: trash and restore
A DELETE does not permanently delete a device. It moves it to the trashcan — exactly what the "Delete" button does in the panel. This is reversible, and a background job purges trashcan devices for good only after 30 days. There is no way to hard-delete a device through the API, by design: a real delete would erase all of the device's history, telemetry, and detections, and that shouldn't happen from a script.
curl -X DELETE -H "Authorization: Bearer $KEY" \
"$PANEL/api/public/v1/assets/4021"
Because the retire is reversible and can carry a warning, DELETE returns 200 with the now-trashed device body (not an empty 204):
{
"id": 4021,
"name": "CB-4021",
"state": "trashcan",
"warnings": []
}
A few properties make this safe to automate:
- It's idempotent. Trashing a device that's already trashed still succeeds — no need to check its state first.
- Any checkout is left intact. A device someone has out stays "checked out" while it sits in the trashcan, so restoring it puts things back exactly as they were. The checkout clears only if the device is eventually purged.
- Synced devices warn. Trashing a device that Google or an MDM still knows about returns a
trashed_asset_may_resyncwarning, because the next sync may bring it back.
Restoring a device is just an edit that moves it out of the trashcan — set its state back to active or inactive and the trashed date is cleared:
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"state": "inactive"}' \
"$PANEL/api/public/v1/assets/4021"
Note that a device in the trashcan no longer shows up as being in a person's hands: what a person has checked out excludes trashed devices.
Which states you can set
You can move a device to active, inactive, archived, or trashcan, and back out of trashcan to restore it. Two states listed at GET /api/public/v1/meta — onramp and rejected — are not settable through the API: those belong to the agent-enrollment approval workflow, which is handled separately in the panel.
Test first with dry-run
Before a call changes anything, you can add ?dry_run=true to see exactly what would happen — the resulting device and any warnings — without saving a thing. It works on create, edit, and delete, and it's the safest way to check your logic against real data:
# What would this create look like, and would it warn me about anything?
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name": "CB-4099", "site": {"name": "Middle School"}, "state": "active"}' \
"$PANEL/api/public/v1/assets?dry_run=true"
A dry-run create echoes the would-be values with "would_create": true, so you can tell a simulation apart from a real result. Nothing is written, no id is assigned, and no idempotency key is consumed.