---
title: Creating, editing, and removing People (Person objects) with the API
description: "Beyond looking people up and seeing what they have checked out, the API can create a person record, edit their details, manage their group membership, and remove them. This article covers that full lifecycle, plus the same safety features the asset endpoints use: advisory warnings, an Idempotency-Key so a retry never creates a duplicate, and a dry-run mode so you can test a call before it changes anything."
---

[Skip to content](https://k12panel.com/kb/developer-api/creating-editing-and-removing-people-person-objects-with-the-api#main-content)

![test-full-word-logo.png\]](https://k12panel.com/hs-fs/hubfs/test-full-word-logo.png?height=35&name=test-full-word-logo.png)

Open main navigation

Close main navigation

- [Go to k12panel.com](https://k12panel.com)

[Go to k12panel.com](https://k12panel.com?hsLang=en)

 Hello. How can we help you?

- There are no suggestions because the search field is empty.

1. [Online Help and Knowledge Base](https://k12panel.com/kb?hsLang=en)
2. [Developer API](https://k12panel.com/kb/developer-api?hsLang=en)

# Creating, editing, and removing People (Person objects) with the API

## Beyond looking people up and seeing what they have checked out, the API can create a person record, edit their details, manage their group membership, and remove them. This article covers that full lifecycle, plus the same safety features the asset endpoints use: advisory warnings, an Idempotency-Key so a retry never creates a duplicate, and a dry-run mode so you can test a call before it changes anything.

Beyond looking people up and seeing what they have checked out, the API can **create** a person record, **edit** their details, manage their **group membership**, and **remove** them. This article covers that full lifecycle, plus the same safety features the asset endpoints use: advisory **warnings**, an **Idempotency-Key** so a retry never creates a duplicate, 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, removing, and managing group membership for people all need a key with the **people:write** permission. It's separate from `people:read` (which only views people), so a reporting key that reads people doesn't also have to be able to change them. An administrator sets a key's permissions under **Settings → API Keys**.

#### Creating a person

`POST` to the people collection. Only a **first name** and **last name** are required:

```
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{"first_name": "Alex", "last_name": "Rivera", "email": "arivera@school.org"}' \  "$PANEL/api/public/v1/people"
```

A successful create returns `201 Created` with the new person, including the `id` the panel assigned them:

```
{  "id": 5210,  "first_name": "Alex",  "last_name": "Rivera",  "email": "arivera@school.org",  "org_unit": { "id": 12, "name": "High School" },  "groups": [],  "warnings": []}
```

You can send more in the same call — notes and the AD (Active Directory) fields — and you can place the person in a specific org unit by id or name:

```
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{        "first_name": "Alex",        "last_name": "Rivera",        "email": "arivera@school.org",        "org_unit": {"name": "High School"},        "ad_domain": "SCHOOL",        "ad_winlogin": "arivera",        "notes": "Fall onboarding"      }' \  "$PANEL/api/public/v1/people"
```

A few things are decided for you, on purpose:

- **A new person lands in the org root** if you don't name an `org_unit` — and once created, they're immediately added to any **dynamic groups** whose rules they match, so membership is correct right away.
- **You can't set system-owned fields.** The cloud/directory sync identifiers (Google, Entra/Azure, Verkada) are assigned by the sync, never accepted from a create. Sending them does no harm; they're simply ignored.

#### Email must be unique

Email is optional, but if you supply one it has to be unique within your organization. If someone already has that email, the create is rejected with `409` and hands you back the existing person(s) as **candidates**, so your script can switch to editing the record it already has instead of creating a duplicate:

```
{  "error": "email_conflict",  "message": "A person with email 'arivera@school.org' already exists (id 5210).",  "candidates": [    { "id": 5210, "first_name": "Alex", "last_name": "Rivera", "email": "arivera@school.org",      "org_unit": "High School", "groups": ["Students"] }  ]}
```

This is the same candidate shape used for [ambiguous lookups](https://k12panel.com/kb/developer-api/KB_API_ERRORS.md?hsLang=en) — read the id from `candidates` and continue.

#### Never create the same person twice: Idempotency-Key

If you `POST` a create, don't hear back, and retry, you'd normally risk creating the person twice. 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: 7c1e6a4d-9f88-4d21-bb0f-5f2b8c149a3e" \  -d '{"first_name": "Sam", "last_name": "Lee"}' \  "$PANEL/api/public/v1/people"
```

If a request with that same key arrives again, the API **returns the person it already created** (with `200` instead of `201`) rather than making a second one.

For a person **with** an email, the key is optional — the email's uniqueness is already a backstop. For a person **without** an email, an `Idempotency-Key` is **required**: with no email to dedupe on, it's the only thing that keeps a retry from duplicating. An email-less create with no key is rejected with `400`.

#### Editing a person

`PATCH` the person with just the fields you want to change — first name, last name, email, notes, the AD fields, or their org unit:

```
curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{"email": "a.rivera@school.org", "org_unit": {"name": "Middle School"}}' \  "$PANEL/api/public/v1/people/5210"
```

The response is the updated person, with a `warnings` list (below). Changing the email follows the same uniqueness rule as create — a collision returns `409` with the conflicting person as a candidate.

#### Warnings: edits to synced people

If a person is synced from a directory (Google Workspace, Entra/Azure, or Verkada), editing a field that sync manages is allowed, but your change may be overwritten the next time the sync runs. Rather than blocking the edit, the API applies it and returns a non-blocking **`warnings`** entry so you know:

```
{  "id": 5210,  "first_name": "Alexis",  "warnings": [    { "code": "sync_managed_field", "field": "first_name",      "message": "This person's first_name is managed by sync; your change may be overwritten on the next sync." }  ]}
```

Notes are never managed by sync, so editing a synced person's notes never warns. Warnings appear only on write responses (create and edit); plain `GET` and list responses don't carry them.

#### Managing group membership

To add a person to a group, `POST` to their `groups` collection, naming the group by id or name:

```
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{"group": {"name": "Robotics Club"}}' \  "$PANEL/api/public/v1/people/5210/groups"
```

To remove them, `DELETE` the group from the person (by name or id):

```
curl -X DELETE -H "Authorization: Bearer $KEY" \  "$PANEL/api/public/v1/people/5210/groups/Robotics%20Club"
```

Adding returns the updated person; removing returns `204 No Content`. Both are **idempotent** — adding a group the person already has, or removing one they don't, still succeeds, so your automation can safely retry.

Two rules to know:

- **The group must already exist** in your organization. Asking to add an unknown group returns an error rather than creating it — this keeps a typo from silently adding a new group to your org. (A name that matches several groups returns the candidates so you can retry with an id.)
- **Only static groups can be edited this way.** A **dynamic** group fills itself from its rules, so adding or removing a member by hand would just be undone on the next rebuild. Targeting a dynamic group returns `422 group_is_dynamic` — to change who's in it, change the attributes its rule matches on (for example, the person's org unit).

#### Removing a person

A `DELETE` **permanently removes** the person — there is no trashcan for people, and the removal also clears their login and activity history. Because it can't be undone, the API guards it the same way the web interface does, and refuses in two cases:

```
curl -X DELETE -H "Authorization: Bearer $KEY" \  "$PANEL/api/public/v1/people/5210"
```

- **`409 person_sync_blocked`** — the person is synced from a cloud service (Google or Entra/Azure). Remove them in that service first, or the next sync just re-creates them.
- **`409 person_has_checkouts`** — the person still has one or more assets checked out. Check those devices in first (this keeps the check-in history intact); then the delete will succeed.

When neither guard applies, the delete succeeds and returns `204 No Content`. Re-deleting a person who's already gone returns a normal `404`.

#### Test first with dry-run

Before a call changes anything, add `?dry_run=true` to see what *would* happen — the resulting person and any warnings, or which guard *would* block a delete — without saving a thing. It works on create, edit, and delete:

```
# Would this create warn me about anything? Does it look right?curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \  -d '{"first_name": "Jordan", "last_name": "Kim", "email": "jkim@school.org"}' \  "$PANEL/api/public/v1/people?dry_run=true"# Would deleting this person be blocked?curl -X DELETE -H "Authorization: Bearer $KEY" \  "$PANEL/api/public/v1/people/5210?dry_run=true"
```

A dry-run create echoes the would-be values with `"would_create": true`. Nothing is written, no id is assigned, and no idempotency key is consumed.

- [Getting started](https://k12panel.com/kb/getting-started?hsLang=en)
- [Menu Functions](https://k12panel.com/kb/menu-functions?hsLang=en)
- [Settings](https://k12panel.com/kb/settings?hsLang=en)
- [3rd Party Integrations](https://k12panel.com/kb/3rd-party-integrations?hsLang=en)
- [PC Health and SOAR](https://k12panel.com/kb/pc-health-and-soar?hsLang=en)
- [FAQ](https://k12panel.com/kb/faq?hsLang=en)
- [K12Panel Agent](https://k12panel.com/kb/k12panel-agent?hsLang=en)
- [Blueprints Catalog](https://k12panel.com/kb/blueprints-catalog?hsLang=en)
- [Writing your own Blueprint](https://k12panel.com/kb/writing-your-own-blueprint?hsLang=en)
- [MSP Tools](https://k12panel.com/kb/msp-tools?hsLang=en)
- [Developer API](https://k12panel.com/kb/developer-api?hsLang=en)

# K12Panel Inc

k12panel.com Help Center

Copyright © 2026, K12Panel Inc