Employees

Invite employees, read and update their records, change their role, and remove them.

Before You Start

Read the following guides before proceeding:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow an invited employee claims their account
PermissionsWhich permission each call requires
RolesAn 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
StatusDescription
InvitedRecord created, not yet claimed. Holds no wallet address
ActiveClaimed and usable
DisabledSuspended. Login is refused with Employee is disabled
InvalidThe 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"
  }
}
FieldTypeRequiredDescription
first_namestringYesGiven name
last_namestringYesFamily name
emailstringYesEmail address. The employee logs in with this value
role_idstringYesRole id (UUID). Must exist for this corporation — a default role or one of its custom roles
phonestringYesE.164 phone number
departmentstringNoFree text
job_titlestringNoFree text
manager_addressstringNoManager's EVM address. Used by the manager filter on employee search
residence_addressobjectNoResidence address
residence_address.line1stringConditionalRequired when residence_address is present
residence_address.line2stringNoSecond address line
residence_address.citystringConditionalRequired when residence_address is present
residence_address.statestringNoState or province
residence_address.countrystringConditionalISO 3166-1 alpha-2, uppercase. Required when residence_address is present
residence_address.postal_codestringConditionalRequired 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_card field in the issuance request
, whenever first_name, last_name, email and
phone are all present. Since all four are required at invite, that is every employee — to emboss a
corporate name such as Acme Inc IT instead, issue the card without employee_id. See
Issuing a Card.

Field Validation

FieldValidation (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_idMust parse as a UUIDAlso 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üller and O'Brien-Smith behave
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 Invited and carries no wallet address. A record already bound to a
    wallet is refused with 401 and Employee registration is already completed.
  • The record is not Disabled — refused with 401 and Employee is disabled.

See Authentication.


List Employees

GET /api/v1/employees

Requires EmployeeSu, EmployeeManage or EmployeeView.

Query parameters:

ParameterDescription
page_number1-indexed page number
page_sizePage size. Defaults to 25
sortname (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"
    }
  ]
}
FieldDescription
idEmployee id (UUID). The value passed as employee_id at card issuance
corporation_addressThe corporation the employee belongs to
owner_addressThe employee's own wallet address once claimed. Empty while Invited
emailEmail the employee logs in with
role_idAssigned role
statusInvited, Active, Disabled or Invalid
manager_addressManager's wallet address, when set
manager_nameManager's display name, resolved from the manager's own employee record
residence_addressResidence address, when supplied
created_at / updated_atISO 8601 timestamps

Search Employees

GET /api/v1/employees/search

Requires EmployeeSu, EmployeeManage or EmployeeView.

Query parameters:

ParameterTypeRequiredDescription
termstringConditionalSearch term. Required when manager_address is not provided, and then must be at least 2 characters matching ^[A-Za-z0-9@._%+' -]{2,100}$
manager_addressstringConditionalFilter to one manager's reports. EVM address, ^0x[a-fA-F0-9]{40}$
page_numberintegerNoNon-negative, at most 2147483647
page_sizeintegerNoDefaults 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:

ParameterDescription
{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"
  }
}
FieldTypeRequiredDescription
first_namestringNoGiven name
last_namestringNoFamily name
emailstringNoAccepted and ignored — email cannot be changed
phonestringNoE.164 phone number. Omit to leave it unchanged; an empty string is rejected
departmentstringNoFree text
job_titlestringNoFree text
manager_addressstringNoManager's EVM address
residence_addressobjectNoResidence address

Response:

{}

An update may omit phone, but it may not clear it. Phone is required on every employee, so
sending "phone": "" is rejected with 400 — omit the field entirely to leave the current value
alone.

email is bound at invitation and is the employee's login identity. Sending a new value here changes
nothing and returns 200 — 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"
}
FieldTypeRequiredDescription
role_idstringYesRole 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:

ConditionMessage
The employee belongs to another corporationEmployee 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 recordEmployee 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 KeyDescriptionResolution
first_nameMissing or does not match the name patternLatin letters, apostrophes, hyphens and spaces; up to 50 characters
last_nameMissing or does not match the name patternAs above
emailMissing or malformedUse a valid address
role_idNot a UUIDRead role ids from GET /api/v1/roles
phoneMissing on invite, or not E.164Required at invite. + followed by up to 15 digits, no separators
manager_addressNot an EVM addressUse the manager's owner_address
termFewer than 2 characters, or contains disallowed charactersProvide a longer term, or filter by manager_address instead
page_sizeGreater than 100Request at most 100 per page

Authorization Errors (401, 403)

HTTPError ReasonDescriptionResolution
401ErrorGeneralEmployee does not belong to corporationThe id belongs to another corporation
401ErrorGeneralOwner cannot be deletedTransfer ownership before removing the owner
401ErrorGeneralEmployee cannot delete itselfHave another EmployeeManage holder do it
403ErrorGeneralInsufficient permission to read this employeeGrant EmployeeView, or read only your own record
403ErrorPermissionDeniedUser does not have required permissionsSee Permissions

Server Errors (500)

Error ReasonDescriptionResolution
ErrorGeneralFailed to get rolerole_id does not exist for this corporation
ErrorGeneralFailed to invite employeeDownstream rejection — most often an email already invited to this corporation
ErrorGeneralFailed to get employeeNo employee with that id
ErrorGeneralFailed to update employee roleDownstream rejection. The role was validated, so retry
ErrorGeneralFailed to delete employeeDownstream rejection. Retry

Did this page help you?