Employees
Invite employees, read and update their records, change their role, and remove them.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How an invited employee claims their account |
| Permissions | Which permission each call requires |
| Roles | An invite must name an existing role |
Overview
An employee is a person acting inside one corporation. The employee record holds the identity used for
card embossing and delivery, the role that resolves into permissions, and the wallet address the person
signs with.
Employees are created by invitation, not by registration. The invite creates the record in status
Invited; the person claims it by logging in with their own wallet and email, which binds those values
and moves the record to Active.
Invited → Active ←→ Disabled
| Status | Description |
|---|---|
Invited | Record created, not yet claimed. Holds no wallet address |
Active | Claimed and usable |
Disabled | Suspended. Login is refused with Employee is disabled |
Invalid | The record could not be mapped |
Step 1: Invite an Employee
POST /api/v1/employees/invite
Requires EmployeeSu or EmployeeManage.
Request body:
{
"first_name": "Alex",
"last_name": "Grey",
"email": "[email protected]",
"role_id": "7386b48f-df9c-49fe-8ccc-38b5b0b55932",
"phone": "+447700900123",
"department": "Finance",
"job_title": "Analyst",
"manager_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"residence_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"state": "",
"country": "GB",
"postal_code": "SW1A 2AA"
}
}| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | Yes | Given name |
last_name | string | Yes | Family name |
email | string | Yes | Email address. The employee logs in with this value |
role_id | string | Yes | Role id (UUID). Must exist for this corporation — a default role or one of its custom roles |
phone | string | Yes | E.164 phone number |
department | string | No | Free text |
job_title | string | No | Free text |
manager_address | string | No | Manager's EVM address. Used by the manager filter on employee search |
residence_address | object | No | Residence address |
residence_address.line1 | string | Conditional | Required when residence_address is present |
residence_address.line2 | string | No | Second address line |
residence_address.city | string | Conditional | Required when residence_address is present |
residence_address.state | string | No | State or province |
residence_address.country | string | Conditional | ISO 3166-1 alpha-2, uppercase. Required when residence_address is present |
residence_address.postal_code | string | Conditional | Required when residence_address is present |
Response:
{}Personal data on the employee record is what gets embossed on their cards. Card issuance uses
the employee's"<first_name> <last_name>", truncated to 25 characters, and ignores the
name_on_cardfield in the issuance request, wheneverfirst_name,last_name,
phoneare all present. Since all four are required at invite, that is every employee — to emboss a
corporate name such asAcme Inc ITinstead, issue the card withoutemployee_id. See
Issuing a Card.
Field Validation
| Field | Validation (regex) | Notes |
|---|---|---|
first_name | ^[a-zA-Z][a-zA-Z\-' ]{0,49}$ | Starts with a letter, up to 50 characters. No digits or accented characters |
last_name | ^[a-zA-Z][a-zA-Z\-' ]{0,49}$ | Same rule as first_name |
email | ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ | Format only |
role_id | Must parse as a UUID | Also checked against the corporation's roles |
phone | ^\+\d{1,15}$ | E.164. Leading +, digits only — no spaces or dashes |
manager_address | ^0x[a-fA-F0-9]{40}$ | EVM address only; a Stellar or Tron address is rejected |
residence_address.line1, .line2 | ^[A-Za-z0-9&.,'’\-/() :+#]{2,100}$ | 2–100 characters |
residence_address.city | ^[A-Za-z\s\-'.]{2,100}$ | Letters only — a city with a digit is rejected |
residence_address.state | ^[A-Za-z][A-Za-z\s\-']{1,99}$ | Letters only |
residence_address.postal_code | ^[A-Za-z0-9\s\-]{3,12}$ | 3–12 characters |
residence_address.country | ^[A-Z]{2}$ | Uppercase ISO 3166-1 alpha-2 |
Names are restricted to unaccented Latin letters.
José,MüllerandO'Brien-Smithbehave
differently: the apostrophe and hyphen are allowed, accented characters are not. Transliterate before
sending, and keep the transliterated form — it is what will be printed on the card.
Code
const response = await fetch(`${baseUrl}/api/v1/employees/invite`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
first_name: 'Alex',
last_name: 'Grey',
email: '[email protected]',
role_id: roleId,
phone: '+447700900123'
})
});response = requests.post(
f"{base_url}/api/v1/employees/invite",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"first_name": "Alex",
"last_name": "Grey",
"email": "[email protected]",
"role_id": role_id,
"phone": "+447700900123",
},
)body, _ := json.Marshal(map[string]string{
"first_name": "Alex",
"last_name": "Grey",
"email": "[email protected]",
"role_id": roleID,
"phone": "+447700900123",
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/employees/invite", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+corporationToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()Step 2: The Employee Claims the Record
There is no accept-invitation endpoint. The invited person calls
POST /api/v1/corporations/login with a partner token and their own X-User-Wallet and
X-User-Email. The handler finds the Invited record by email, binds the wallet address and email to
it, and returns a corporation token.
Two conditions must hold:
- The record is still in status
Invitedand carries no wallet address. A record already bound to a
wallet is refused with401andEmployee registration is already completed. - The record is not
Disabled— refused with401andEmployee is disabled.
See Authentication.
List Employees
GET /api/v1/employees
Requires EmployeeSu, EmployeeManage or EmployeeView.
Query parameters:
| Parameter | Description |
|---|---|
page_number | 1-indexed page number |
page_size | Page size. Defaults to 25 |
sort | name (default, ascending) or usage (descending usage count) |
Response:
{
"employees": [
{
"id": "64120850-73a1-4df5-a074-d463258c9deb",
"corporation_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"owner_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"email": "[email protected]",
"first_name": "Alex",
"last_name": "Grey",
"phone": "+447700900123",
"department": "Finance",
"job_title": "Analyst",
"manager_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"manager_name": "John Smith",
"role_id": "7386b48f-df9c-49fe-8ccc-38b5b0b55932",
"status": "Active",
"residence_address": {
"line1": "10 Downing Street",
"line2": "Flat 2",
"city": "London",
"state": "",
"country": "GB",
"postal_code": "SW1A 2AA"
},
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:05:00Z"
}
]
}| Field | Description |
|---|---|
id | Employee id (UUID). The value passed as employee_id at card issuance |
corporation_address | The corporation the employee belongs to |
owner_address | The employee's own wallet address once claimed. Empty while Invited |
email | Email the employee logs in with |
role_id | Assigned role |
status | Invited, Active, Disabled or Invalid |
manager_address | Manager's wallet address, when set |
manager_name | Manager's display name, resolved from the manager's own employee record |
residence_address | Residence address, when supplied |
created_at / updated_at | ISO 8601 timestamps |
Search Employees
GET /api/v1/employees/search
Requires EmployeeSu, EmployeeManage or EmployeeView.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
term | string | Conditional | Search term. Required when manager_address is not provided, and then must be at least 2 characters matching ^[A-Za-z0-9@._%+' -]{2,100}$ |
manager_address | string | Conditional | Filter to one manager's reports. EVM address, ^0x[a-fA-F0-9]{40}$ |
page_number | integer | No | Non-negative, at most 2147483647 |
page_size | integer | No | Defaults to 25, maximum 100 |
Providing manager_address alone returns that manager's reports. Providing neither is rejected with
Search term must contain at least 2 characters.
The response shape matches GET /api/v1/employees.
Read One Employee
GET /api/v1/employees/{employeeId}
Path parameters:
| Parameter | Description |
|---|---|
{employeeId} | Employee id from GET /api/v1/employees |
The route declares no permission, but the handler enforces one: a caller may always read their own
record. Reading anyone else's requires Su, EmployeeSu, EmployeeManage or EmployeeView — without
one, the call returns 403 with Insufficient permission to read this employee.
The record must belong to the caller's corporation. An employee id from another corporation returns
401 with Employee does not belong to corporation, not 404 — the same response as a non-existent
id, so it cannot be used to probe for other tenants' ids.
The response shape matches one item of GET /api/v1/employees.
Update Personal Data
PUT /api/v1/employees/{employeeId}
Requires EmployeeSu or EmployeeManage.
Request body:
{
"first_name": "Alex",
"last_name": "Grey",
"phone": "+447700900123",
"department": "Treasury",
"job_title": "Senior Analyst",
"manager_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
"residence_address": {
"line1": "10 Downing Street",
"city": "London",
"country": "GB",
"postal_code": "SW1A 2AA"
}
}| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | No | Given name |
last_name | string | No | Family name |
email | string | No | Accepted and ignored — email cannot be changed |
phone | string | No | E.164 phone number. Omit to leave it unchanged; an empty string is rejected |
department | string | No | Free text |
job_title | string | No | Free text |
manager_address | string | No | Manager's EVM address |
residence_address | object | No | Residence address |
Response:
{}An update may omit
phone, but it may not clear it. Phone is required on every employee, so
sending"phone": ""is rejected with400— omit the field entirely to leave the current value
alone.
nothing and returns200— it does not report that the field was ignored. To move an employee to a
different address, delete the record and invite again.
Updating personal data does not re-emboss existing cards. The name on an issued card is fixed at
issuance.
Change an Employee's Role
PUT /api/v1/employees/{employeeId}/role
Requires EmployeeSu or EmployeeManage.
Request body:
{
"role_id": "31eba1db-0809-4d44-a479-0b696c4a2603"
}| Field | Type | Required | Description |
|---|---|---|---|
role_id | string | Yes | Role id. Must exist for this corporation |
Response:
{}The change takes effect at the employee's next login. Permissions are resolved from the role when
the corporation token is minted and frozen into it. An employee holding a live token keeps their old
permissions until it expires or they log in again. Where a role change is a revocation, treat the
token lifetime as the revocation delay.
Delete an Employee
DELETE /api/v1/employees/{employeeId}
Requires EmployeeSu or EmployeeManage.
Response:
{}Three deletions are refused with 401:
| Condition | Message |
|---|---|
| The employee belongs to another corporation | Employee does not belong to corporation |
The employee holds the Owner role (ed7a5845-8727-4b06-9ba0-7d43ddc9e6aa) | Owner cannot be deleted |
| The caller is deleting their own record | Employee cannot delete itself |
Deleting an employee does not close their cards. Close or block cards first — see
Managing a Card.
Error Handling
{
"error_reason": "ErrorGeneral",
"error_description": "Request failed validation",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "first_name", "details": "First name is invalid" }
]
}Employee validation errors carry the field name as the error_details key, not as a field entry.
Read error_details[].key to find which field failed.
Validation Errors (400)
| Error Details Key | Description | Resolution |
|---|---|---|
first_name | Missing or does not match the name pattern | Latin letters, apostrophes, hyphens and spaces; up to 50 characters |
last_name | Missing or does not match the name pattern | As above |
email | Missing or malformed | Use a valid address |
role_id | Not a UUID | Read role ids from GET /api/v1/roles |
phone | Missing on invite, or not E.164 | Required at invite. + followed by up to 15 digits, no separators |
manager_address | Not an EVM address | Use the manager's owner_address |
term | Fewer than 2 characters, or contains disallowed characters | Provide a longer term, or filter by manager_address instead |
page_size | Greater than 100 | Request at most 100 per page |
Authorization Errors (401, 403)
| HTTP | Error Reason | Description | Resolution |
|---|---|---|---|
| 401 | ErrorGeneral | Employee does not belong to corporation | The id belongs to another corporation |
| 401 | ErrorGeneral | Owner cannot be deleted | Transfer ownership before removing the owner |
| 401 | ErrorGeneral | Employee cannot delete itself | Have another EmployeeManage holder do it |
| 403 | ErrorGeneral | Insufficient permission to read this employee | Grant EmployeeView, or read only your own record |
| 403 | ErrorPermissionDenied | User does not have required permissions | See Permissions |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | Failed to get role | role_id does not exist for this corporation |
ErrorGeneral | Failed to invite employee | Downstream rejection — most often an email already invited to this corporation |
ErrorGeneral | Failed to get employee | No employee with that id |
ErrorGeneral | Failed to update employee role | Downstream rejection. The role was validated, so retry |
ErrorGeneral | Failed to delete employee | Downstream rejection. Retry |
Updated 20 days ago

