Skip to main content

Partner API: Employees & Groups

Create, update, terminate and bulk-import employees, and manage the groups used to assign handbooks.

Employees

All employee endpoints are under /organizations/{orgId}/employees and require an access token.

The employee resource

{
"id": "123…",
"employeeId": "R-10482",
"firstName": "Sam",
"lastName": "Okafor",
"email": "sam.okafor@acme.com",
"jobTitle": "Account Executive",
"department": "Sales",
"teamName": "EMEA Sales",
"location": "Lisbon",
"country": "PT",
"managerEmployeeId": "R-10021",
"hireDate": "2025-03-01",
"status": "active",
"terminatedDate": null,
"groups": ["Sales", "Lisbon Office"],
"activatedAt": "2025-03-02T09:14:00.000Z",
"createdAt": "2025-03-01T08:00:00.000Z",
"updatedAt": "2026-01-12T10:30:00.000Z"
}

status is one of invited (created, not yet activated), active, or terminated.

List employees

GET /organizations/{orgId}/employees

Optional filters: status, email, group (group name), updatedSince (ISO timestamp). Without a status filter the list returns every employee, terminated ones included; pass status=active or status=invited (or both calls) to exclude them.

Get an employee

GET /organizations/{orgId}/employees/{employeeRef}

employeeRef is the AirMason id or ext:<employeeId>.

Create an employee

POST /organizations/{orgId}/employees

{
"employeeId": "R-10482",
"firstName": "Sam",
"lastName": "Okafor",
"email": "sam.okafor@acme.com",
"jobTitle": "Account Executive",
"department": "Sales",
"location": "Lisbon",
"country": "PT",
"managerEmployeeId": "R-10021",
"hireDate": "2025-03-01",
"groups": ["Sales", "Lisbon Office"],
"sendWelcomeEmail": true
}

employeeId, email, firstName and lastName are required. employeeId and email must be unique among the organization's non-terminated employees (409 conflict otherwise). Groups named in groups are created if they don't exist.

What if the email or employeeId belongs to a terminated employee?

Posting the employeeId or the email of a terminated employee does not return 409. What happens depends on the organization's employee rehire settings. With the default (enforceUniqueEmployees: true), the terminated employee is rehired: the existing row keeps its AirMason id, its fields are overwritten with the request, and its status goes back to invited.

With enforceUniqueEmployees: false, a new employee is created instead, unless a rehireSignatureWindowDays window is set and the employee was terminated within it.

Set sendWelcomeEmail: false if you deliver the activation link yourself via the employee.created webhook. The link is single-use and valid for 7 days; treat activationLinkExpiresAt in the event payload as the authoritative expiry.

Update an employee

PATCH /organizations/{orgId}/employees/{employeeRef}

Any field from the create request except employeeId. Passing null or an empty string clears an optional field.

Passing groups replaces the employee's full group membership, including groups added by the organization's group rules. Rules run again after every create, update and import, so rule-based groups come back. If the organization manages membership with rules, omit groups.

A PATCH against a terminated employee returns 409 conflict (Employee is terminated); reactivate first.

Terminate an employee

POST /organizations/{orgId}/employees/{employeeRef}/terminate

{ "terminatedDate": "2026-02-28" }

terminatedDate defaults to today. Terminated employees lose portal access immediately, stop receiving reminders, are excluded from employeeCount and signature-completion percentages, and are preserved (with their signature history) for audit purposes. Terminating is idempotent: a second call does not move terminatedDate. To bring someone back, POST …/{employeeRef}/reactivate.

Bulk import

PUT /organizations/{orgId}/employees/import — either multipart/form-data with a file field (CSV), or a JSON body (Content-Type: application/json). Both up to 100 MB.

Creates or updates employees in bulk, matching on employeeId. The import runs asynchronously: the response is 202 with a job you can poll at GET /organizations/{orgId}/employees/import/{jobId}. status goes from pending to completed.

{
"jobId": "imp_01J8…",
"status": "completed",
"totalRows": 412,
"createdCount": 20,
"updatedCount": 392,
"deactivatedCount": 0,
"startedAt": "2026-01-16T09:00:00.000Z",
"completedAt": "2026-01-16T09:00:41.000Z",
"error": null,
"errorInRows": []
}

deactivatedCount is always 0: an import never terminates employees. Only one import can run per organization at a time (409 conflict otherwise).

Send complete rows. The update half of the import replaces the whole employee row, so any column absent from the CSV is cleared on existing employees (for example an employee loses teamName, location, country and managerEmployeeId after a six-column import).

Accepted CSV headers: email, employee_id, first_name, last_name, dob, team_name, job_title (also jobTitle / JobTitle), location, state, country, department, division, manager_name, manager_email, manager_id, cost_center, hire_date, employee_type, pay_type, company, leave_status, username, code, source_id, source.

The import does not validate email format. Imported employees receive no welcome email, so their employee.created event carries no activationLink (null).

Import with a JSON body

Send Content-Type: application/json with an employees array. It runs exactly like a CSV import (same job, same polling, same 202 response) and uses the same field names as the single-employee endpoints:

{
"employees": [
{
"employeeId": "E-1001",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@acme.com",
"jobTitle": "Store Manager",
"department": "Sales, East",
"teamName": "Team A",
"location": "Montreal",
"country": "Canada",
"managerEmployeeId": "E-0001",
"hireDate": "2024-03-15"
}
]
}

employeeId and firstName are required. The other supported fields are lastName, email, jobTitle, department, teamName, location, country, managerEmployeeId and hireDate (YYYY-MM-DD). Every value is a string — send numeric IDs as strings, e.g. "1234". employeeId must not start with ext:.

  • Values may contain commas, accents and apostrophes, but not double quotes ("), pipes (|) or line breaks.

  • Any other field (for example groups or state) rejects that row. Assign groups with the employee or group endpoints.

  • Invalid rows are skipped and the valid ones are imported. Skipped rows are listed in errorInRows by their position in the array, e.g. { "index": 11, "fieldName": "employeeId", "message": "must be a string" }. If no row is valid, the request returns 400 with rowErrors and nothing is imported.

  • Include an email on every row, and list each employeeId only once per request. A row without an email is not matched to the existing employee and creates a separate record instead of updating it.

  • Rows replace the employee's imported fields (see "Send complete rows" above). To change a single field, use PATCH /organizations/{orgId}/employees/{employeeId}.

  • Errors: 400 validation_error (employees: must be an array, employees: must contain at least one employee, Request body is not valid JSON, No valid employees in request), 409 conflict (organization deactivated, employees managed by an HRIS/SFTP sync, or an import already running), and 429 rate_limited with Retry-After when too many JSON imports are being processed at once.

A 1,000-employee JSON import is typically accepted in about a second and completes within seconds of starting; the first import after a quiet period can stay pending for a few minutes while the import worker starts.

Groups

Groups are how you assign handbooks to sets of employees. All under /organizations/{orgId}/groups, access token. Requests that take group names (employee groups, handbook assignments) accept at most 50 names.

Method & path

Description

GET /groups

List groups with employeeCount

POST /groups

Create
body { "name": "Lisbon Office" } — a duplicate name returns 409 conflict

PATCH /groups/{groupId}

Rename
body { "name": "Lisbon Office" }

DELETE /groups/{groupId}

Delete. The group is removed from every employee's groups (employees themselves are not affected) and the name is freed for reuse.


Partner API documentation:

Did this answer your question?