API Reference

https://mesh.langgenius.ai/api/v1
Last Updated: 2026-04-07 17:21 PDT · v1 · REST + JSON

Quick Start

Getting an Access Key

To use the Mesh API, you need an Access Key. Access Keys are created through the mesh CLI.

Step 1. Log in to the CLI with your Google account:

$ mesh login

Step 2. Create a personal Access Key:

$ mesh accesskey create --name "my-script"

  Key ID:   ak_01JQ...
  Secret:   mesh_sk_abc123...
  Role:     viewer
  Expires:  never

  Save this key — it cannot be retrieved later.

Step 3. Use the key in API requests:

curl https://mesh.langgenius.ai/api/v1/employees \
  -H "Authorization: Bearer mesh_sk_abc123..."
Personal Access Keys inherit your role (viewer/editor/admin). For system integrations (bots, CI), ask an admin to create a system key via mesh admin accesskey create --name "slack-bot" --role viewer.

Conventions

Authentication

Most endpoints require authentication via one of the following methods:

// Access Key (for scripts and integrations)
Authorization: Bearer mesh_sk_xxxx

// Session Token (for CLI sessions)
Authorization: Bearer mesh_at_xxxx

// Stub (NODE_ENV=development only)
Authorization: Bearer stub:you@langgenius.ai

Response Format

All responses include an X-Request-Id header.

// Success
{ "data": <payload> }

// Success + Pagination
{
  "data": [...],
  "meta": { "page": 1, "limit": 20, "total": 86, "totalPages": 5 }
}

// Error
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Employee not found",
    "requestId": "req_abc123def456"
  }
}

Error Codes

HTTPCodeDescription
400VALIDATION_ERRORRequest parameter validation failed
401UNAUTHORIZEDNot authenticated or token invalid
403FORBIDDENInsufficient permissions
404NOT_FOUNDResource not found
409CONFLICTGeneral conflict
409EMPLOYEE_NUMBER_TAKENEmployee number already in use
409DEPARTMENT_NAME_EXISTSDepartment name already exists
409EMPLOYEE_IM_PLATFORM_EXISTSDuplicate IM platform for the same employee
409LAST_ADMIN_CANNOT_DEMOTEThe last admin cannot be demoted
409OFFBOARD_HAS_DEPENDENTSCannot offboard while employee has direct reports
409OFFBOARD_HAS_PROJECT_OWNERSHIPCannot offboard while employee is a project owner
401ACCESS_KEY_EXPIREDAccess Key has expired
500INTERNAL_ERRORInternal server error

Pagination

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page, max 100

Deletion Policy

Hard deletion is not supported for employees, departments, projects, legal entities, or offices. To "delete" these resources, use PATCH to set isActive = false for a soft deactivation. Sub-resources (phones, IMs, titles, employment records, mentorship relations, department members, project members) support hard deletion via DELETE.

Permission Roles

RoleScope
viewerAll GET queries + self-service profile updates
editorviewer + all business write operations
admineditor + Google Sync, Access Key, and role management

Employee Reference Values

All :id parameters accept three formats: email, employeeNumber (e.g. LG-0003), or internal id. The first two are recommended.

Health & Meta

GET /health No Auth — Health Check

Response

{ "status": "ok" }
GET /meta/cli-version No Auth — Latest CLI version

Returns the latest CLI version declared by the server, used for lightweight upgrade prompts. Source: environment variable LATEST_CLI_VERSION.

Response

{ "data": { "latestVersion": "0.2.0" } }
GET /meta/sdk-version No Auth — SDK version and API compatibility info

Response

{
  "data": {
    "latestVersion": "0.2.0",
    "apiVersion": "v1",
    "supportedApiVersions": ["v1"]
  }
}

Auth & Session

POST /auth/stub/session No Auth — Issue session in development mode (development only)

Only available when NODE_ENV=development. Pass an email address to receive session tokens.

Request Body

{ "email": "luyu@langgenius.ai" }

Response

{
  "data": {
    "accessToken": "mesh_at_...",
    "refreshToken": "mesh_rt_...",
    "expiresAt": "2026-04-06T12:00:00.000Z",
    "employee": {
      "id": "clx...", "employeeNumber": "LG-0001",
      "email": "luyu@langgenius.ai", "displayName": "Luyu Zhang",
      "role": "admin", "isActive": true
    }
  }
}
POST /auth/refresh No Auth — Rotate session token

Use a refresh token to obtain a new token pair. The old refresh token is invalidated immediately; if replay is detected, all active sessions for the employee are revoked.

Request Body

{ "refreshToken": "mesh_rt_..." }

Response

{
  "data": {
    "accessToken": "mesh_at_...",
    "refreshToken": "mesh_rt_...",
    "expiresAt": "2026-04-06T13:00:00.000Z"
  }
}
POST /auth/google No Auth — Exchange Google ID token for a one-time code

The web login page sends a Google ID token to this endpoint. After validation, the API generates a one-time code (valid for 5 minutes).

Request Body

{ "idToken": "eyJhbGciOiJSUzI1NiIs..." }

Response

{
  "data": {
    "code": "MESH-7K3F",
    "expiresAt": "2026-04-06T12:05:00.000Z",
    "employee": { "email": "luyu@langgenius.ai", "displayName": "Luyu Zhang" }
  }
}
If the INITIAL_ADMIN_EMAIL logs in for the first time and no matching employee exists, an employee record is automatically created with the admin role.
GET /auth/login No Auth — CLI login page (browser)

A lightweight login page opened in the browser with Google Sign-In integration. The parameter cli=true enables CLI mode; api_url specifies the API base URL.

POST /auth/code-exchange No Auth — Exchange one-time code for session tokens

The CLI sends the code pasted by the user to this endpoint in exchange for Mesh session tokens. The code is invalidated immediately after use and expires automatically if unused within 5 minutes.

Request Body

{ "code": "MESH-7K3F" }

Response

{
  "data": {
    "accessToken": "mesh_at_...",
    "refreshToken": "mesh_rt_...",
    "expiresAt": "2026-04-06T12:00:00.000Z",
    "employee": { "id": "clx...", "email": "luyu@langgenius.ai", "role": "admin" }
  }
}
GET /auth/me viewer — Current identity info

Response (Personal Key / OAuth)

{
  "data": {
    "principal": {
      "actorType": "user", "role": "viewer",
      "authKind": "api_key", "email": "minco@langgenius.ai"
    },
    "employee": {
      "id": "emp_minco", "employeeNumber": "LG-0003",
      "email": "minco@langgenius.ai", "displayName": "Minco Wang",
      "role": "viewer", "isActive": true
    }
  }
}
For system Access Keys, employee is null.
POST /auth/accesskey viewer — Create personal Access Key

Request Body

{
  "name": "my-bot",
  "expiresAt": "2026-07-01T00:00:00.000Z"  // optional
}

Response

{
  "data": {
    "id": "key_01", "key": "mesh_sk_abc123...",
    "name": "my-bot", "role": "viewer",
    "createdAt": "2026-04-05T10:00:00Z",
    "expiresAt": "2026-07-01T00:00:00Z"
  }
}
The key is returned only once at creation time and cannot be retrieved later.
GET /auth/accesskey viewer — List own Access Keys

Response

{
  "data": [{
    "id": "key_01", "name": "my-bot", "role": "viewer",
    "createdAt": "2026-04-05T10:00:00Z",
    "lastUsedAt": "2026-04-05T12:30:00Z",
    "expiresAt": "2026-07-01T00:00:00Z"
  }]
}
DELETE /auth/accesskey/:keyId viewer — Revoke personal Access Key

Response: 204 No Content

Passkey

POST /auth/passkey/login-options No Auth — Generate Passkey login challenge

Returns WebAuthn public-key credential request options for initiating a Passkey login ceremony.

POST /auth/passkey/login No Auth — Verify Passkey login

Validates the WebAuthn assertion and returns a one-time login code. Response: 201 Created

POST /auth/passkey/register-options-with-code No Auth — Registration options via login code

Returns WebAuthn credential creation options for registering a new Passkey. Authenticates via loginCode in the request body.

POST /auth/passkey/register-with-code No Auth — Register Passkey via login code

Completes Passkey registration using a one-time login code for authentication. Response: 201 Created

POST /auth/passkey/register-options viewer — Registration options (Bearer auth)

Returns WebAuthn credential creation options for registering a new Passkey. Requires Bearer token authentication.

POST /auth/passkey/register viewer — Register Passkey (Bearer auth)

Completes Passkey registration using Bearer token authentication. Response: 201 Created

GET /auth/passkey viewer — List registered Passkeys

Returns all Passkeys registered to the authenticated employee.

DELETE /auth/passkey/:passkeyId viewer — Delete a Passkey

Removes a registered Passkey. Response: 204 No Content

SSO / OAuth2

Integrating your app with Mesh SSO

1. Register your app in the OAuthClient database table (client_id, secret hash, redirect URIs).
2. Redirect users to /auth/authorize?client_id=...&redirect_uri=...&state=...
3. After login, Mesh redirects back to your redirect_uri with ?code=...&state=...
4. Exchange the code via POST /auth/token with your client_secret.
5. Use the returned accessToken to call GET /auth/me for user identity.

GET /auth/authorize No Auth — SSO authorization endpoint

Renders the login page in SSO mode. The user authenticates and is redirected back to the client application.

Query Parameters

ParameterTypeDescription
client_idstringRequired. The registered client identifier.
redirect_uristringRequired. URI to redirect after authorization.
statestringRequired. Opaque value for CSRF protection.
POST /auth/authorize/complete No Auth — Exchange login code for authorization code

Exchanges a one-time loginCode (from Passkey or Google login) for an OAuth2 authorization code. Response: 201 Created

POST /auth/token No Auth — OAuth2 token exchange

Standard OAuth2 token endpoint. Exchanges an authorization code for access and refresh tokens (authorization_code grant type).

Employees

GET /employees viewer — List employees

Query Parameters

ParameterTypeDescription
page, limitnumberPagination
departmentIdstringFilter by department
primaryOfficeIdstringFilter by primary office
legalEntityIdstringFilter by legal entity
countryCodestringFilter by country (CN, US)
employmentTypestringFULL_TIME, INTERN, LABOR, CONSULTANT
rolestringviewer, editor, admin
isActivebooleanActive status
hireDateAfterstringHire date lower bound (YYYY-MM-DD)
hireDateBeforestringHire date upper bound (YYYY-MM-DD)
includestringComma-separated: phones, imAccounts, titles
searchstringFuzzy search by name, email, or employee number

Response

{
  "data": [{
    "id": "clx...", "employeeNumber": "LG-0003",
    "legalName": "Haiming Wang", "displayName": "Minco Wang",
    "email": "minco@langgenius.ai", "employmentType": "FULL_TIME",
    "isActive": true,
    "primaryOffice": { "name": "Shanghai", "countryCode": "CN" },
    "manager": { "displayName": "Alice Chen", "email": "alice@langgenius.ai" },
    "departments": [{ "name": "Backend", "isPrimary": true }],
    "primaryTitle": "Senior Engineer"
  }],
  "meta": { "page": 1, "limit": 20, "total": 86, "totalPages": 5 }
}
GET /employees/:id viewer — Employee detail (with all sub-resources)

:id accepts email, employeeNumber, or internal id. Returns the full employee object including all sub-resources: titles, phones, imAccounts, badges, employments, departments, etc.

Response

{
  "data": {
    "id": "clx...", "employeeNumber": "LG-0003",
    "legalName": "Haiming Wang", "displayName": "Minco Wang",
    "romanizedName": "Wang Haiming", "englishName": "Minco Wang",
    "email": "minco@langgenius.ai", "githubHandle": "minco",
    "hireDate": "2023-06-15", "employmentType": "FULL_TIME",
    "isActive": true,
    "primaryOffice": { "name": "Shanghai", "timezone": "Asia/Shanghai" },
    "manager": { "displayName": "Alice Chen" },
    "directReports": [...], "departments": [...],
    "titles": [...], "phones": [...],
    "imAccounts": [...], "badges": [...],
    "employments": [...]
  }
}
PATCH /employees/:id editor — Update employee

All fields are optional; only include the fields you want to update.

Request Body

{
  "employeeNumber": "LG-0003",
  "legalName": "Haiming Wang", "displayName": "Minco Wang",
  "romanizedName": "Wang Haiming", "englishName": "Minco Wang",
  "githubHandle": "minco", "hireDate": "2023-06-15",
  "employmentType": "FULL_TIME",
  "primaryOfficeId": "...", "managerId": "..."
}

Response

Returns the full updated employee object.

Employee Lifecycle

POST /employees/:id/offboard admin — One-click offboarding

When confirm is omitted or set to false, only a preview is returned (dependency checks, action list, blockers). Set confirm: true to execute the offboarding.

Request Body

{ "confirm": true }

Response (Preview)

{
  "data": {
    "canProceed": true,
    "actions": [
      "Deactivate employee",
      "Close 1 active title: Engineer",
      "Remove from 2 projects: Loom, Dify Enterprise"
    ],
    "blockers": []
  }
}
When canProceed: false, blockers lists the reasons (e.g. has direct reports, is a project owner).
POST /employees/:id/reinstate admin — Reinstate offboarded employee

Restores from the offboard snapshot: reactivates the employee and restores titles, departments, projects, and mentorship relations. Also supports confirm preview mode.

Employee Relations

GET/employees/:id/managerviewer— Direct manager

Response

{
  "data": {
    "id": "...",
    "displayName": "Alice Chen",
    "email": "alice@langgenius.ai",
    "avatarUrl": "https://..."
  }
}
GET/employees/:id/reportsviewer— Direct reports

Response

{
  "data": [
    {
      "id": "...",
      "displayName": "Bob Li",
      "email": "bob@langgenius.ai",
      "avatarUrl": "..."
    }
  ]
}
GET/employees/:id/mentorviewer— Mentorship relations

Response

{
  "data": {
    "myMentors": [{ "displayName": "Alice", "note": "..." }],
    "myMentees": [{ "displayName": "Dave", "note": "..." }]
  }
}
POST/employees/:id/mentoreditor— Create mentorship relation

Request Body

{ "mentorId": "alice@langgenius.ai", "note": "Q1 onboarding" }

Response: 201 Created

DELETE/employees/:id/mentor/:relationIdeditor— Delete mentorship relation

Response: 204 No Content

GET/employees/:id/projectsviewer— Employee's projects

Each project includes owner (dotted-line manager) information.

GET/employees/:id/departmentsviewer— Employee's departments

Response

{
  "data": [
    { "departmentId": "...", "name": "Backend", "isPrimary": true },
    { "departmentId": "...", "name": "AI Platform", "isPrimary": false }
  ]
}
POST/employees/:id/departmentseditor— Join department

Request Body

{ "departmentId": "...", "isPrimary": false }
If isPrimary: true, the isPrimary flag of all other departments for this employee is automatically set to false.
DELETE/employees/:id/departments/:departmentIdeditor— Remove from department

Removing the primary department is not allowed. To change the primary department, first set the new department as primary, then remove the old one. Response: 204 No Content

Employments

GET/employees/:id/employmentsviewer— List employment records

Sorted by isCurrent DESC, startDate DESC.

POST/employees/:id/employmentseditor— Create employment record

Request Body

{
  "legalEntityId": "...", "officeId": "...",
  "employmentType": "FULL_TIME", "startDate": "2024-01-01",
  "isCurrent": true, "jobTitle": "Senior Engineer",
  "note": "Transferred from CN entity"
}
If isCurrent: true, all other records will have their isCurrent set to false and endDate set to this record's startDate.
PATCH/employees/:id/employments/:employmentIdeditor— Update employment record

Request Body (all fields optional)

FieldTypeDescription
legalEntityIdstringLegal entity ID
officeIdstringOffice ID
employmentTypestringFULL_TIME / INTERN / LABOR / CONSULTANT
startDatestringYYYY-MM-DD
endDatestringYYYY-MM-DD
isCurrentbooleanSetting to true automatically closes other records
jobTitlestringJob title
notestringNote

Response: the full updated employment record object.

Phones

GET/employees/:id/phonesviewer— List phones

Response

{
  "data": [
    {
      "id": "...",
      "number": "13800138000",
      "countryCode": "+86",
      "label": "WORK",
      "isPrimary": true
    }
  ]
}
POST/employees/:id/phoneseditor— Add phone

Request Body

{
  "number": "13800138000", "countryCode": "+86",
  "label": "WORK",  // WORK | PERSONAL | WHATSAPP | WECHAT | OTHER
  "isPrimary": true
}
PATCH/employees/:id/phones/:phoneIdeditor— Update phone

Request Body (all fields optional)

FieldTypeDescription
numberstringPhone number
countryCodestringCountry code, e.g. +86, +1
labelstringWORK / PERSONAL / WHATSAPP / WECHAT / OTHER
isPrimarybooleanSetting to true automatically unmarks other numbers as primary
DELETE/employees/:id/phones/:phoneIdeditor— Delete phone

Response: 204 No Content

IM Accounts

GET/employees/:id/im-accountsviewer— List IM accounts

Response

{
  "data": [
    {
      "platform": "FEISHU",
      "accountId": "ou_xxx",
      "username": "minco.wang",
      "note": ""
    },
    {
      "platform": "SLACK",
      "accountId": "U0123ABC",
      "username": "minco",
      "note": ""
    }
  ]
}
POST/employees/:id/im-accountseditor— Add/update IM account

Request Body

FieldTypeRequiredDescription
platformstringYesFEISHU / WECOM / DINGTALK / SLACK
accountIdstringYesPlatform user ID
usernamestringNoPlatform username (for display)
notestringNoNote
{
  "platform": "FEISHU",
  "accountId": "ou_xxx",
  "username": "minco.wang",
  "note": ""
}
If the same platform already exists, it will be updated (upsert).

Response: 201 Created

DELETE/employees/:id/im-accounts/:platformeditor— Delete IM account

Path Parameters

ParameterDescription
:platformFEISHU / WECOM / DINGTALK / SLACK

Response: 204 No Content

Titles

GET/employees/:id/titlesviewer— List titles

Query Parameters

ParameterTypeDescription
currentbooleantrue returns only currently active titles

Response

{
  "data": [
    {
      "id": "...",
      "title": "Senior Engineer",
      "isPrimary": true,
      "isCurrent": true,
      "startDate": "2024-01-01",
      "endDate": null,
      "note": ""
    }
  ]
}
POST/employees/:id/titleseditor— Add title

Request Body

FieldTypeRequiredDescription
titlestringYesTitle name
isPrimarybooleanNoSetting to true automatically unmarks other primary titles
startDatestringNoYYYY-MM-DD, defaults to today
notestringNoNote
{
  "title": "Senior Engineer",
  "isPrimary": true,
  "startDate": "2024-01-01"
}

Response: 201 Created

PATCH/employees/:id/titles/:titleIdeditor— Update title

Request Body (all fields optional)

FieldTypeDescription
titlestringTitle name
isPrimarybooleanSetting to true automatically unmarks other primary titles
startDatestringYYYY-MM-DD
endDatestringYYYY-MM-DD
notestringNote
POST/employees/:id/titles/:titleId/closeeditor— Close title

Request Body

FieldTypeRequiredDescription
endDatestringNoYYYY-MM-DD, defaults to today

Marks the title as isCurrent: false and isPrimary: false, and sets the endDate.

DELETE/employees/:id/titles/:titleIdeditor— Delete title

Hard deletes the title record. Response: 204 No Content

Org Chart

GET/org-chartviewer— Org chart view

Query Parameters

ParameterTypeDefaultDescription
formatstringtreetree (mixed department + employee tree) or graph (matrix relationship graph)
rootIdstringSpecify root node (employee reference or department ID); returns only the subtree/subgraph

format=tree Response

A mixed department + employee tree. Employees are placed under their department. When managerId is inconsistent with department membership, a conflict object is annotated on the node.

{
  "data": [{
    "type": "employee", "id": "...",
    "displayName": "CEO",
    "employeeNumber": "LG-0001", "email": "ceo@langgenius.ai",
    "children": [{
      "type": "department", "id": "...", "name": "Engineering",
      "children": [{
        "type": "employee", "displayName": "Engineer A",
        "conflict": {
          "type": "manager_department_mismatch",
          "message": "Employee primary department differs from manager."
        },
        "children": []
      }]
    }]
  }]
}

format=graph Response

Returns a flat nodes + edges structure.

Edge TypeSource
departmentDepartment parent-child / DepartmentMember
managerEmployee.managerId
project_ownerProjectMember + Project.ownerId
mentorEmployeeRelation
{
  "data": {
    "nodes": [
      { "id": "dept_backend", "type": "department", "label": "Backend" },
      { "id": "emp_minco", "type": "employee", "label": "Minco Wang",
        "employeeNumber": "LG-0003", "email": "minco@langgenius.ai" }
    ],
    "edges": [
      { "from": "dept_eng", "to": "dept_backend", "type": "department" },
      { "from": "emp_alice", "to": "emp_minco", "type": "manager" }
    ]
  }
}

Departments

GET/departmentsviewer— List departments

Response

{
  "data": [{
    "id": "...", "name": "Backend",
    "description": "...",
    "parent": { "id": "...", "name": "Engineering" },
    "children": [{ "id": "...", "name": "Backend Infra" }],
    "memberCount": 12
  }]
}
GET/departments/:idviewer— Department detail

Returns department info + parent department + child departments + member list.

GET/departments/:id/membersviewer— Department members

Query Parameters

ParameterTypeDefaultDescription
recursivebooleanfalseWhen true, recursively includes members from all child departments

Response

{
  "data": [{
    "id": "...", "displayName": "Minco Wang",
    "employeeNumber": "LG-0003", "email": "minco@langgenius.ai",
    "isPrimary": true,
    "department": "Backend"  // indicates source when recursive=true
  }]
}
POST/departmentseditor— Create department

Request Body

FieldTypeRequiredDescription
namestringYesDepartment name (unique)
descriptionstringNoDepartment description
parentIdstringNoParent department ID; omit for a top-level department
{
  "name": "AI Platform",
  "description": "AI platform and infrastructure",
  "parentId": "..."
}

Response: 201 Created

PATCH/departments/:ideditor— Update department

Request Body (all fields optional)

FieldTypeDescription
namestringDepartment name (unique)
descriptionstringDepartment description
parentIdstringParent department ID
isActivebooleanSet to false to deactivate the department

Projects

GET/projectsviewer— List projects

Response

{
  "data": [{
    "id": "...", "name": "Loom",
    "description": "Internal HR API",
    "isActive": true,
    "owner": {
      "id": "...", "employeeNumber": "LG-0001",
      "displayName": "John Smith", "email": "john@langgenius.ai"
    },
    "memberCount": 8
  }]
}
GET/projects/:idviewer— Project detail

Returns project info + owner + member list (including each member's role and employeeNumber).

POST/projectseditor— Create project

Request Body

FieldTypeRequiredDescription
namestringYesProject name
descriptionstringNoProject description
ownerIdstringYesEmployee reference (email / employeeNumber / id)
{
  "name": "Loom",
  "description": "Internal HR API",
  "ownerId": "luyu@langgenius.ai"
}

Response: 201 Created

PATCH/projects/:ideditor— Update project

Request Body (all fields optional)

FieldTypeDescription
namestringProject name
descriptionstringProject description
ownerIdstringEmployee reference (email / employeeNumber / id)
isActivebooleanSet to false to deactivate the project
POST/projects/:id/memberseditor— Add project member

Request Body

FieldTypeRequiredDescription
employeeIdstringYesEmployee reference (email / employeeNumber / id)
rolestringNoRole within the project (e.g. Backend Dev)
{ "employeeId": "minco@langgenius.ai", "role": "Backend Dev" }

Response: 201 Created

DELETE/projects/:id/members/:employeeIdeditor— Remove project member

Path Parameters

ParameterDescription
:employeeIdEmployee reference (email / employeeNumber / id)

Response: 204 No Content

Legal Entities

GET/legal-entitiesviewer— List entities

Query Parameters

ParameterTypeDescription
countryCodestringFilter by country (CN / US / JP)
isActivebooleanFilter by status
GET/legal-entities/:idviewer— Entity detail (with offices and employees)

Response

{
  "data": {
    "id": "entity_cn",
    "name": "LangGenius CN Co., Ltd.",
    "localName": "Hangzhou LangGenius Technology Co., Ltd.",
    "shortName": "LG-CN",
    "countryCode": "CN", "currency": "CNY",
    "taxId": "913301...", "isActive": true,
    "offices": [
      { "id": "...", "name": "Shanghai", "city": "Shanghai", "countryCode": "CN" }
    ],
    "activeEmployees": [
      { "id": "...", "employeeNumber": "LG-0003", "displayName": "Minco Wang" }
    ]
  }
}
POST/legal-entitiesadmin— Create entity

Request Body

FieldTypeRequiredDescription
namestringYesFull legal entity name
localNamestringNoLocal language name
shortNamestringYesShort name (e.g. LG-CN)
legalRegisteredNamestringNoRegistered business name
countryCodestringYesISO country code (CN / US / JP)
currencystringYesISO currency code (CNY / USD)
taxIdstringNoTax ID / Unified Social Credit Code
{
  "name": "LangGenius CN Co., Ltd.",
  "shortName": "LG-CN",
  "countryCode": "CN", "currency": "CNY",
  "taxId": "913301..."
}

Response: 201 Created

PATCH/legal-entities/:idadmin— Update entity

Request Body (all fields optional)

FieldTypeDescription
namestringFull legal entity name
localNamestringLocal language name
shortNamestringShort name
legalRegisteredNamestringRegistered business name
countryCodestringISO country code
currencystringISO currency code
taxIdstringTax ID
isActivebooleanSet to false to deactivate the entity

Offices

GET/officesviewer— List offices

Query Parameters

ParameterTypeDescription
countryCodestringFilter by country
legalEntityIdstringFilter by owning entity
isActivebooleanFilter by status
GET/offices/:idviewer— Office detail (with entity and employees)

Response

{
  "data": {
    "id": "office_shanghai",
    "name": "Shanghai", "localName": "Shanghai Office",
    "countryCode": "CN", "city": "Shanghai",
    "address": "Pudong", "timezone": "Asia/Shanghai",
    "isActive": true,
    "legalEntity": { "id": "...", "shortName": "LG-CN" },
    "activeEmployees": [
      { "id": "...", "employeeNumber": "LG-0003", "displayName": "Minco Wang" }
    ]
  }
}
POST/officesadmin— Create office

Request Body

FieldTypeRequiredDescription
namestringYesOffice name
localNamestringNoLocal language name
countryCodestringYesISO country code
citystringYesCity
addressstringNoFull address
timezonestringYesIANA timezone (e.g. Asia/Shanghai)
legalEntityIdstringYesOwning legal entity ID
{
  "name": "Shanghai Office", "countryCode": "CN",
  "city": "Shanghai", "timezone": "Asia/Shanghai",
  "legalEntityId": "..."
}
PATCH/offices/:idadmin— Update office

Request Body (all fields optional)

FieldTypeDescription
namestringOffice name
localNamestringLocal language name
countryCodestringISO country code
citystringCity
addressstringFull address
timezonestringIANA timezone
legalEntityIdstringOwning legal entity ID
isActivebooleanSet to false to deactivate the office

Badges

GET/badgesviewer— List badge templates

Response

{
  "data": [{
    "id": "badge_01",
    "name": "Founding Member",
    "description": "Joined before Series A",
    "asciiArt": "...",
    "colors": ["#FFC330", "#FFB118", "#EB8900", "#FFC634", "#FF9600"],
    "createdAt": "2026-04-01T08:00:00Z",
    "awardedCount": 12
  }]
}
POST/badgesadmin— Create badge template

Request Body

{
  "name": "Founding Member",
  "description": "Joined before Series A",
  "designId": 22  // from GET /badges/designs
}
DELETE/badges/:idadmin— Delete badge template

:id accepts template id or name. Templates that have been awarded cannot be deleted (409).

GET/badges/designsadmin— Badge design candidates

Returns 4 random preset designs each time, including asciiArt, colors, and hint.

GET/employees/:id/badgesviewer— Employee badges

Response

{
  "data": [{
    "id": "award_01",
    "awardedAt": "2026-04-06T10:00:00Z",
    "citation": "Day-one builder",
    "badge": {
      "id": "badge_01", "name": "Founding Member",
      "description": "Joined before Series A",
      "asciiArt": "...",
      "colors": ["#FFC330", "..."]
    },
    "awardedBy": {
      "id": "...", "employeeNumber": "LG-0001",
      "displayName": "Luyu Zhang",
      "email": "luyu@langgenius.ai"
    }
  }]
}
POST/employees/:id/badgeseditor— Award badge (single employee)

Request Body

{
  "badge": "Founding Member",  // or badgeId
  "citation": "Day-one builder"
}
POST/badges/awardeditor— Bulk award (by project)

Request Body

{
  "badge": "Mesh Launch",
  "projectId": "proj_loom",
  "citation": "Launch team"
}

Automatically skips employees who already have the same badge; returns awardedCount and skippedCount.

DELETE/employees/:id/badges/:badgeIdadmin— Revoke badge

:badgeId accepts template id or name. Response: 204 No Content

Google Sync

POST/sync/googleadmin— Trigger Google Workspace sync

Query: ?dryRun=true returns a preview without writing changes.

Response

{
  "data": {
    "synced": 86, "created": 2, "updated": 84,
    "departments": 12, "errors": []
  }
}
GET/sync/statuseditor— Sync status

Returns the last sync time, result statistics, and the number of unresolved warnings.

GET/sync/warningseditor— Unresolved sync warnings

Returns a list of SUSPENDED / DELETED events.

POST/sync/warnings/:id/resolveadmin— Resolve sync warning

Request Body

{ "action": "offboard" }  // offboard | dismiss

Import & Audit

POST/import/employeeseditor— Bulk import employee data

Matches existing employees by email (does not create new employees). Empty fields do not overwrite existing values.

Request Body

{
  "dryRun": true,
  "rows": [{
    "email": "minco@langgenius.ai",
    "legalName": "Haiming Wang", "displayName": "Minco Wang",
    "employmentType": "FULL_TIME", "hireDate": "2023-06-15",
    "primaryOffice": "Shanghai",
    "phone": "13800138000", "phoneCountryCode": "+86"
  }]
}
GET/audit/completenesseditor— Data completeness check

Query: ?level=required|recommended|all, ?department=..., ?employee=...

Returns a summary (totalActiveEmployees, fullyComplete, requiredAction, recommendedAction) and a per-employee issues list.

GET/audit/consistencyeditor— Organizational consistency check

Query: ?department=..., ?employee=...

Checks for manager-department conflicts, missing primary departments, orphaned employees, etc.

Admin

POST/admin/accesskeyadmin— Create system Access Key

Request Body

{
  "name": "ticket-system",
  "role": "viewer",
  "expiresAt": "2027-04-01T00:00:00.000Z"
}
GET/admin/accesskeyadmin— List all Access Keys

Includes both system-level and personal Keys, with owner information.

DELETE/admin/accesskey/:keyIdadmin— Revoke any Access Key

Response: 204 No Content

PATCH/admin/roles/:employeeIdadmin— Set employee role

Request Body

{ "role": "editor" }  // viewer | editor | admin

Response

{
  "data": {
    "employeeId": "...",
    "email": "minco@langgenius.ai",
    "role": "editor"
  }
}