Skip to main content

Overview

Xenia integrates with 50+ HRIS (Human Resource Information System) providers through Merge.dev, enabling automatic employee synchronization, role assignment, and location mapping. Supported Providers:
  • BambooHR, Rippling, Workday, ADP
  • Gusto, Paylocity, Paycom, Paychex
  • UKG, Ceridian, Namely, Zenefits
  • And 40+ more HRIS platforms
Key Capabilities:
  • One-time and continuous employee sync
  • Automatic role assignment via job title mapping
  • Multi-location assignment (1:M mapping)
  • Review-based or automatic employee onboarding
  • Real-time sync via webhooks

Prerequisites

Before using HRIS integration:
  1. Feature Flag: HRIS must be enabled for your workspace
  2. Permissions: User must have HRIS management permissions
  3. Role Setup: Define roles in Xenia for job title mapping

Connection Management

Generate a Merge.dev link token to initiate the connection flow.
curl -X POST "https://api.xenia.team/api/v1/mgt/users/hris/merge/link-token" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserEmail": "[email protected]",
    "endUserOrganization": "Company Name",
    "categories": ["hris"]
  }'
Request Body:
FieldTypeRequiredDescription
endUserEmailstringNoAdmin email for Merge notifications
endUserOrganizationstringNoOrganization name displayed in Merge Link
endUserOriginIdstringNoCustom identifier for the connection
integrationSlugstringNoPre-select a specific HRIS (e.g., bamboohr)
categoriesarrayNoFilter integrations (default: ["hris"])
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "linkToken": "link_token_xxx",
    "integrationName": null,
    "expiresAt": "2024-12-24T10:00:00Z"
  }
}

Complete Connection

After the user completes the Merge Link UI, exchange the public token for a permanent connection.
curl -X POST "https://api.xenia.team/api/v1/mgt/users/hris/merge/complete" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "publicToken": "pt_xxx_from_merge_link",
    "integrationSlug": "bamboohr",
    "integrationName": "BambooHR"
  }'
Request Body:
FieldTypeRequiredDescription
publicTokenstringYesToken received from Merge Link callback
integrationSlugstringNoHRIS provider identifier
integrationNamestringNoDisplay name for the integration
linkedAccountIdstringNoMerge’s linked account ID
categoriesarrayNoIntegration categories
metadataobjectNoCustom metadata to store
Response:
{
  "status": true,
  "code": 200,
  "message": "Successfully connected to BambooHR",
  "data": {
    "id": "account-uuid",
    "accountId": "account-uuid",
    "integrationSlug": "bamboohr",
    "integrationName": "BambooHR",
    "provider": "merge",
    "status": "active",
    "categories": ["hris"],
    "linkedAccountId": "merge-linked-account-id",
    "connectedAt": "2024-12-23T10:00:00Z",
    "workspaceId": "workspace-uuid"
  }
}

List Connections

Retrieve all HRIS connections for a workspace with sync status.
curl -X GET "https://api.xenia.team/api/v1/mgt/hris/connections" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "data": [
    {
      "id": "account-uuid",
      "provider": "Merge.dev",
      "integrationSlug": "bamboohr",
      "integrationName": "BambooHR",
      "categories": ["hris"],
      "status": "active",
      "syncState": "synced",
      "hasCompletedInitialSync": true,
      "lastSyncedAt": "2024-12-23T08:00:00Z",
      "lastSyncStatus": "success",
      "syncError": null,
      "employeeCount": 150,
      "syncProgress": {
        "recordsProcessed": 150,
        "recordsSucceeded": 148,
        "recordsFailed": 2,
        "employeesCreated": 10,
        "employeesUpdated": 138
      },
      "createdAt": "2024-12-01T10:00:00Z",
      "updatedAt": "2024-12-23T08:00:00Z"
    }
  ]
}
Sync States:
StateDescription
waiting_for_initial_syncConnection established, waiting for first sync
initial_sync_in_progressFirst sync running
syncingIncremental sync in progress
syncedAll data synchronized
errorSync failed (check syncError)
unlinkedConnection disconnected

Disconnect Connection

Unlink an HRIS connection. This preserves user data but stops sync.
curl -X DELETE "https://api.xenia.team/api/v1/mgt/hris/connections/{accountId}" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "message": "Successfully disconnected from BambooHR. Users and employee data have been preserved.",
  "data": {
    "id": "account-uuid",
    "integration": "BambooHR",
    "disconnectedAt": "2024-12-23T10:00:00Z",
    "deletedMirrors": 5,
    "deletedTokens": 1
  }
}

Employee Management

List Stored Employees

Retrieve HRIS employees stored in Xenia with advanced filtering.
curl -X GET "https://api.xenia.team/api/v1/mgt/hris/employees?page=1&pageSize=25&employmentStatus=ACTIVE&includeCompany=true" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Query Parameters:
ParameterTypeDefaultDescription
pagenumber1Page number
pageSizenumber25Items per page (max: 200)
searchstring-Search name, email, employee number
employmentStatusstring/array-Filter: ACTIVE, INACTIVE, TERMINATED
accountIdstring-Filter by HRIS account
homeLocationIdstring-Filter by home location
workLocationIdstring-Filter by work location
includeCompanybooleantrueInclude company details
includeLocationsbooleantrueInclude location details
includeManagerbooleanfalseInclude manager details
includeDeletedbooleanfalseInclude soft-deleted records
sortBystringcreatedAtSort field
sortOrderstringDESCSort direction: ASC or DESC
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "employees": [
      {
        "id": "employee-uuid",
        "WorkspaceId": "workspace-uuid",
        "HRISAccountId": "account-uuid",
        "remoteId": "hris-employee-id",
        "firstName": "John",
        "lastName": "Doe",
        "preferredName": "Johnny",
        "displayFullName": "John Doe",
        "workEmail": "[email protected]",
        "personalEmail": "[email protected]",
        "employeeNumber": "EMP001",
        "employmentStatus": "ACTIVE",
        "startDate": "2023-01-15",
        "endDate": null,
        "remoteCreatedAt": "2023-01-10T00:00:00Z",
        "remoteModifiedAt": "2024-12-20T00:00:00Z",
        "Company": {
          "id": "company-uuid",
          "legalName": "Company Inc.",
          "displayName": "Company"
        },
        "HomeLocation": {
          "id": "location-uuid",
          "name": "Headquarters",
          "remoteId": "hris-location-id"
        },
        "xeniaStatus": {
          "isXeniaUser": true,
          "status": "ACTIVE",
          "statusLabel": "Active User",
          "userId": "user-uuid",
          "workspaceUserId": "workspace-user-uuid",
          "inviteStatus": "Active",
          "canInvite": false,
          "isLinkedToHRIS": true,
          "hrisEmployeeId": "employee-uuid"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "total": 150,
      "totalPages": 6,
      "hasMore": true
    }
  }
}
Xenia Status Values:
StatusDescription
NO_EMAILEmployee has no work email
NOT_INVITEDEligible for invitation
NOT_IN_WORKSPACEUser exists but not in workspace
DELETEDUser was deleted
ACTIVEActive Xenia user
PENDING_INVITEInvitation sent, not accepted
INACTIVEUser deactivated

Bulk Invite Employees

Queue bulk invitations for HRIS employees to Xenia.
# Simple mode - same role for all
curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/accounts/{accountId}/employees/bulk-invite" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "employeeIds": ["emp-uuid-1", "emp-uuid-2", "emp-uuid-3"],
    "roleId": "default-role-uuid"
  }'
# Advanced mode - per-employee configuration
curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/accounts/{accountId}/employees/bulk-invite" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "employees": [
      {
        "employeeId": "emp-uuid-1",
        "roleId": "manager-role-uuid",
        "locationId": "hq-location-uuid"
      },
      {
        "employeeId": "emp-uuid-2",
        "roleId": "staff-role-uuid",
        "locationId": "branch-location-uuid"
      }
    ]
  }'
Request Body (Simple Mode):
FieldTypeRequiredDescription
employeeIdsarrayYes*Employee UUIDs to invite (max: 3000)
roleIdstringNoDefault role for all employees
locationMappingobjectNoHRIS location ID → Xenia location ID map
Request Body (Advanced Mode):
FieldTypeRequiredDescription
employeesarrayYes*Per-employee configuration (max: 3000)
employees[].employeeIdstringYesEmployee UUID
employees[].roleIdstringNoRole for this employee
employees[].locationIdstringNoLocation for this employee
locationMappingobjectNoHRIS location ID → Xenia location ID map
Use either employeeIds OR employees, not both. They are mutually exclusive.
Response:
{
  "status": true,
  "code": 200,
  "message": "Bulk invite job queued. 45 employee(s) will be invited.",
  "data": {
    "jobId": "bull-job-id",
    "total": 50,
    "eligible": 45,
    "skipped": [
      {
        "employeeId": "emp-uuid-4",
        "email": null,
        "name": "Jane Smith",
        "reason": "NO_WORK_EMAIL"
      },
      {
        "employeeId": "emp-uuid-5",
        "email": "[email protected]",
        "name": "Bob Johnson",
        "reason": "EMPLOYMENT_STATUS_INACTIVE"
      }
    ]
  }
}

Job Title Mapping

Map HRIS job titles to Xenia roles for automatic role assignment during invite.

List Job Titles

Get unique job titles from synced employees with mapping status.
curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/job-titles?accountId={accountId}&sortBy=employeeCount&sortOrder=DESC" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "data": [
    {
      "jobTitle": "Software Engineer",
      "employeeCount": 25,
      "isMapped": true,
      "mappedToRole": {
        "roleId": "developer-role-uuid",
        "roleTitle": "Developer"
      }
    },
    {
      "jobTitle": "Store Manager",
      "employeeCount": 12,
      "isMapped": false,
      "mappedToRole": null
    }
  ],
  "meta": {
    "total": 15,
    "page": 1,
    "pageSize": 50,
    "totalPages": 1
  }
}

List Job Title Mappings

curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/job-title-mappings?accountId={accountId}" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"

Create/Upsert Job Title Mappings

Create or update job title mappings in bulk.
curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/job-title-mappings" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "account-uuid",
    "create": [
      {
        "hrisJobTitle": "Software Engineer",
        "roleId": "developer-role-uuid"
      },
      {
        "hrisJobTitle": "Store Manager",
        "roleId": "manager-role-uuid"
      }
    ],
    "delete": ["old-mapping-uuid-1"]
  }'
Request Body:
FieldTypeRequiredDescription
accountIdstringYesHRIS account UUID
createarrayNoMappings to create/update
create[].hrisJobTitlestringYesJob title from HRIS
create[].roleIdstringYesXenia role UUID
deletearrayNoMapping UUIDs to delete
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "created": [
      {
        "id": "mapping-uuid-1",
        "hrisJobTitle": "Software Engineer",
        "roleId": "developer-role-uuid",
        "roleTitle": "Developer",
        "isActive": true,
        "createdAt": "2024-12-23T10:00:00Z"
      }
    ],
    "updated": [
      {
        "id": "mapping-uuid-2",
        "hrisJobTitle": "Store Manager",
        "roleId": "manager-role-uuid",
        "roleTitle": "Manager",
        "isActive": true,
        "updatedAt": "2024-12-23T10:00:00Z"
      }
    ],
    "deleted": [
      {
        "id": "old-mapping-uuid-1",
        "deleted": true
      }
    ],
    "errors": []
  }
}

Location Mapping

Map HRIS locations to Xenia locations. Supports 1:M mapping (one HRIS location to multiple Xenia locations).

List HRIS Locations

curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/locations?accountId={accountId}" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "locations": [
      {
        "id": "hris-location-uuid",
        "WorkspaceId": "workspace-uuid",
        "HRISAccountId": "account-uuid",
        "remoteId": "bamboo-location-123",
        "name": "Downtown Office",
        "phoneNumber": "+1-555-0100",
        "street1": "123 Main St",
        "city": "New York",
        "state": "NY",
        "zipCode": "10001",
        "country": "US",
        "locationType": "office"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "total": 8,
      "totalPages": 1
    }
  }
}

Create/Upsert Location Mappings

curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/location-mappings" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "account-uuid",
    "create": [
      {
        "hrisLocationId": "hris-location-uuid",
        "locationId": "xenia-location-uuid-1",
        "isPrimary": true
      },
      {
        "hrisLocationId": "hris-location-uuid",
        "locationId": "xenia-location-uuid-2",
        "isPrimary": false
      }
    ]
  }'
Request Body:
FieldTypeRequiredDescription
accountIdstringYesHRIS account UUID
createarrayNoMappings to create/update
create[].hrisLocationIdstringYesHRIS location UUID
create[].locationIdstringYesXenia location UUID
create[].isPrimarybooleanNoSet as primary location (default: false)
deletearrayNoMapping UUIDs to delete
1:M Mapping: One HRIS location can map to multiple Xenia locations. When an employee is invited, they’ll be assigned to ALL mapped locations, with defaultLocationId set from the primary mapping.

Auto-Sync Configuration

Configure automatic employee synchronization behavior.

Get Auto-Sync Settings

curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/settings" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "id": "settings-uuid",
    "WorkspaceId": "workspace-uuid",
    "HRISAccountId": "account-uuid",
    "autoSyncEnabled": true,
    "autoInviteEnabled": true,
    "autoDeactivateEnabled": true,
    "inviteMode": "review_required",
    "defaultRoleId": "basic-user-role-uuid",
    "defaultLocationId": "hq-location-uuid",
    "notifyAdmins": true,
    "notifyOnNewHires": true,
    "notifyOnTerminations": true,
    "DefaultRole": {
      "id": "basic-user-role-uuid",
      "title": "Basic User"
    },
    "DefaultLocation": {
      "id": "hq-location-uuid",
      "name": "Headquarters"
    }
  }
}

Update Auto-Sync Settings

curl -X PUT "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/settings" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "autoSyncEnabled": true,
    "autoInviteEnabled": true,
    "autoDeactivateEnabled": true,
    "inviteMode": "review_required",
    "defaultRoleId": "basic-user-role-uuid",
    "defaultLocationId": "hq-location-uuid",
    "notifyAdmins": true,
    "notifyOnNewHires": true,
    "notifyOnTerminations": true
  }'
Request Body:
FieldTypeDescription
autoSyncEnabledbooleanEnable automatic data sync
autoInviteEnabledbooleanAutomatically invite new employees
autoDeactivateEnabledbooleanAuto-deactivate terminated employees
inviteModestringautomatic or review_required
defaultRoleIdstringDefault role for new employees
defaultLocationIdstringDefault location for new employees
notifyAdminsbooleanSend admin notifications
notifyOnNewHiresbooleanNotify on new hire detection
notifyOnTerminationsbooleanNotify on termination detection

Trigger Manual Sync

curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/trigger-sync" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "message": "Sync job queued successfully",
    "jobId": "bull-queue-job-id"
  }
}

Employee Review Workflow

When inviteMode is set to review_required, new employees are queued for admin review.

List Pending Employees

curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/pending-employees?changeType=new_hire" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"
Query Parameters:
ParameterTypeDescription
changeTypestringFilter: new_hire, update, termination, re_hire
pagenumberPage number (default: 1)
pageSizenumberItems per page (default: 50)
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "mirrors": [
      {
        "id": "mirror-uuid",
        "HRISAccountId": "account-uuid",
        "HRISEmployeeId": "employee-uuid",
        "status": "pending_review",
        "changeType": "new_hire",
        "matchConfidence": "high",
        "MatchedUserId": null,
        "HRISEmployee": {
          "id": "employee-uuid",
          "remoteId": "hris-123",
          "firstName": "John",
          "lastName": "Doe",
          "workEmail": "[email protected]"
        },
        "MatchedUser": null,
        "createdAt": "2024-12-23T08:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 50,
      "total": 5,
      "totalPages": 1
    }
  }
}
Change Types:
TypeDescription
new_hireNew employee detected in HRIS
updateEmployee data changed
terminationEmployee marked as terminated
re_hirePreviously terminated employee reactivated

Approve Employees

curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/approve-employees" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "mirrorIds": ["mirror-uuid-1", "mirror-uuid-2"]
  }'
Response:
{
  "status": true,
  "code": 200,
  "data": {
    "approved": 2,
    "processing": {
      "queued": 2,
      "deactivated": 0,
      "errors": 0
    }
  }
}

Reject Employees

curl -X POST "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/reject-employees" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}" \
  -H "Content-Type: application/json" \
  -d '{
    "mirrorIds": ["mirror-uuid-3"],
    "rejectionReason": "Contractor - not eligible for Xenia access"
  }'

View Sync History

curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/employee-history?status=completed&page=1" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"

View Webhook Logs

For debugging sync issues, view Merge.dev webhook events.
curl -X GET "https://api.xenia.team/api/v1/mgt/hris-integration/auto-sync/accounts/{accountId}/webhook-logs?processingStatus=failed" \
  -H "x-client-key: YOUR_CLIENT_KEY" \
  -H "x-client-secret: YOUR_CLIENT_SECRET" \
  -H "workspace-id: {workspaceId}"

Permissions Reference

PermissionDescription
CAN_ACCESS_HRISBase access to HRIS features (required for all)
CAN_VIEW_HRIS_INTEGRATIONRead-only access to connections and employees
CAN_MANAGE_HRIS_INTEGRATIONFull management of HRIS connections
CAN_MANAGE_HRIS_EMPLOYEESApprove/reject employees, bulk invite
CAN_MANAGE_HRIS_LOCATIONSManage location mappings
CAN_MANAGE_HRIS_ROLESManage job title mappings
CAN_MANAGE_HRIS_CONFIGURATIONSConfigure auto-sync settings

Common Workflows

Initial HRIS Setup

Employee Onboarding Flow


Best Practices

Set up job title → role mappings and location mappings before inviting employees. This ensures automatic role and location assignment.
Start with inviteMode: "review_required" to manually verify the first batch of employees before switching to automatic mode.
Check connection status and webhook logs periodically to catch sync issues early. Set up admin notifications for failures.
Enable autoDeactivateEnabled to automatically deactivate users when they’re terminated in the HRIS. This maintains security without manual intervention.
When an employee works across multiple Xenia locations, map their HRIS location to all relevant Xenia locations with one marked as primary.