Humaans to Officient Migration: A Technical Guide
Technical guide for migrating HRIS data from Humaans to Officient. Covers API constraints, data model mapping, compensation history, time-off balances, and step-by-step migration process.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Humaans to Officient Migration: A Technical Guide
Migrating from Humaans to Officient is a data-model translation problem between two fundamentally different HRIS architectures. Humaans is an API-first, globally-oriented HRIS built around a deeply decomposed object model — separate resources for People, Job Roles, Compensations, Bank Accounts, Documents, Equipment, and Time Away (with its own hierarchy of policies, allocations, periods, and adjustments). Officient (now part of Exact) is a Belgian-rooted, SME-focused HR platform with a flatter data structure organized around People, Roles, Wages, Functions, Contracts, Days Off, and Assets.
There is no native migration path between these two platforms. No export-to-Officient button exists in Humaans, and no Officient import wizard understands Humaans's schema. Every migration requires extraction from Humaans (via API or CSV), transformation to match Officient's data model, and loading via the Officient API or manual import.
In architectural terms, this is a move from a schema-on-read system to a schema-on-write system. Schema-on-read means Humaans stores data flexibly across many decomposed objects and interprets structure at query time — you can store nearly anything and assemble it later. Schema-on-write means Officient enforces strict validation rules at the point of data entry — every record must conform to predefined formats, required fields, and relational constraints (like non-overlapping contract dates) before it's accepted. The practical consequence: data that lived comfortably in Humaans may be rejected by Officient's validation layer unless you clean and restructure it during transformation.
This guide covers the architectural differences, API constraints, data mapping, failure modes, and step-by-step process to execute a clean migration from Humaans to Officient.
For the broader HRIS migration framework, see The Ultimate HRIS Data Migration Checklist. If you're handling sensitive payroll data during this move, review How to Safely Migrate Sensitive Employee & Payroll Data.
Why Companies Migrate from Humaans to Officient
The most common reasons:
- Belgian/Benelux payroll compliance: Officient has native integrations with Belgian payroll providers including SD Worx, Securex, Partena Professional, Nmbrs, and The Payroll Office. If your company is consolidating operations into Belgium or the Netherlands, Officient's payroll pipeline is significantly more mature for that region — including handling Belgian DIMONA declarations and NISS/INSZ national number validation.
- Cost consolidation: Smaller teams (25–200 employees) in Western Europe often find Officient's pricing better aligned with their headcount and feature requirements.
- Exact ecosystem: Companies already using Exact Online for accounting want a single-vendor HR stack with native data flow between finance and HR.
- Simpler administration: Teams that don't need Humaans's API-first extensibility may prefer Officient's more guided, workflow-driven approach to employee management, including built-in onboarding flows, contract generation, and self-service portals.
Officient's marketplace pages for Nmbrs, Partena Professional, SD Worx, and The Payroll Office all show imports of employee lists plus personal, wage, and time-off data, but the exact coverage varies by connector. Do not assume every Officient onboarding path supports the same depth of history — verify with Officient support which connector fields are actually supported before designing your transformation.
Core Architecture Differences: Humaans vs Officient
| Dimension | Humaans | Officient |
|---|---|---|
| API style | REST, Bearer token auth, granular scopes (docs) | REST, OAuth 2.0 (docs) |
| Rate limits | 400 req/min (token bucket: 40 burst, 7/sec refill) | 30 requests per 5 seconds (6 req/sec sustained) |
| Pagination | $skip / $limit (max 250 per page) |
Page-based (page=0 start) |
| Employee model | Person object with 50+ fields |
People with detail endpoint; employee types include employee, freelancer, student worker, intern |
| Job history | Separate Job Roles with effective dating |
Role History endpoint |
| Compensation | Separate Compensations resource with effective dating (salary, bonus, equity, commission) |
Current Wage + monthly Components |
| Time off | 5-layer hierarchy: Types → Policies → Allocations → Periods → Entries/Adjustments | Days Off with simpler type-based structure |
| Custom fields | Full Custom Fields + Custom Values API (text, longText, select, multiSelect, link, date, person) | Custom fields (text, number, date, money, email, select_option) — must be pre-created before values can be loaded |
| Documents | Files API + Documents API with typed folders | Document management tied to employee, asset, or car records |
| Equipment | Dedicated Equipment resource with serial numbers, costs | Asset management |
| Working patterns | Dedicated Working Patterns + Allocations with FTE calculation | Weekly schedule |
| Data export | Native Data Exports API (CSV/JSON, filtered, multiple types) | API extraction |
| Webhooks | HMAC SHA-256 signed, 20+ event types | Supported, webhook management API |
| National ID validation | Stores taxId as free-text string |
Validates Belgian NISS/INSZ format (11 digits, modulo-97 checksum) |
| Market focus | Global, distributed teams | Belgium/Netherlands SMEs |
The biggest architectural gap: Humaans decomposes employee data across 30+ resource types with foreign-key relationships, while Officient consolidates data into fewer, flatter endpoints. Humaans treats custom fields and profile extensions as first-class citizens. Officient relies on defined objects mapped strictly to payroll logic. This means Humaans data must be denormalized and reassembled during transformation.
Migration Approaches
There are five ways to move data from Humaans to Officient. The right choice depends on your dataset size, historical data requirements, and engineering bandwidth.
Method 1: CSV Export/Import
How it works: Use Humaans's Data Exports API (POST /api/data-exports) to generate CSV or JSON files for People, Equipment, Time Away, Documents, and Timesheets. Select specific fields via the fields parameter. Download the generated export, clean and transform it to match Officient's expected format, and import via Officient's UI or API.
When to use it: Small teams (under 50 employees) with minimal custom fields and no need for historical job role or compensation data.
Pros: Low technical barrier. Humaans's export API is flexible with field selection and filtering. Cons: No relationship preservation — manager hierarchies break. Compensation history flattens to current values. Time-off balances don't transfer. Documents and attachments require separate handling.
Complexity: Low Scalability: Small datasets only
Method 2: API-to-API Migration
How it works: Extract all data from Humaans via its REST API across relevant endpoints (/api/people, /api/job-roles, /api/compensations, /api/time-away, /api/equipment, /api/documents, /api/bank-accounts, /api/custom-fields, /api/custom-values, /api/locations). Build an ID mapping table linking Humaans IDs to Officient IDs. Transform each record to match Officient's API schema. Load into Officient via its REST API, respecting creation order (locations → custom field definitions → people → roles → wages).
When to use it: Mid-size teams (50–500 employees) needing historical data, or anyone requiring full-fidelity transfer with dedicated engineering resources.
Pros: Full control over transformation logic and data cleaning. Preserves relationships if you build the mapping logic. Repeatable and testable. Cons: Significant development effort — budget 2–4 weeks of engineering time. Officient's rate limit (30 req/5 sec) means loading 500 employees with related data requires careful throttling. Error handling and retry logic are non-trivial.
Complexity: High Scalability: Enterprise-capable with proper batching
Method 3: Unified HRIS API (Apideck or Merge)
Both Humaans and Officient are supported by unified HRIS API providers like Apideck and Merge. These normalize both platforms to a common schema (Employee, Company, Department, Employment).
When to use it: If you're already using a unified API platform, or if you need to abstract away vendor-specific API quirks for a quick prototype.
Pros: Normalized data model reduces mapping work. Handles auth and pagination. Cons: Unified APIs cover only a subset of each platform's data — they typically expose 3–4 resources per platform, missing most detailed objects (compensations, documents, equipment, time-away policies). Not suitable for full-fidelity migrations.
Complexity: Medium Scalability: Limited by unified API coverage
Method 4: iPaaS / Middleware (Zapier, Make)
When to use it: Ongoing lightweight sync of new employees between systems, not a full historical migration. Humaans lists Zapier officially, and Officient has a Zapier marketplace integration.
Pros: No-code setup. Good for triggering actions on employee events. Cons: Not designed for bulk historical migration. These tools handle single-event triggers well but cannot perform bulk relational extraction and transformation. Rate limits become a bottleneck quickly. Cannot handle complex multi-step ETL with relationship rebuilding.
Complexity: Low Scalability: Very limited
Method 5: Custom ETL Pipeline / Managed Migration Service
How it works: Build a dedicated extraction → transformation → loading pipeline using Python, Node.js, or an ETL framework, with logging, error handling, rollback capability, and validation built in. Alternatively, a managed migration partner handles this infrastructure for you.
When to use it: Any migration where data integrity matters, you have complex relational data (contracts, historical time-off, documents), or engineering resources are committed to product work.
Pros: Maximum control and auditability when built in-house. Zero data loss with proper implementation. Frees internal teams when outsourced. Cons: Highest build cost if in-house. Requires budget allocation if outsourced.
Complexity: High (in-house) / Low (outsourced, for the client) Scalability: Enterprise-grade
Migration Approach Comparison
| Approach | Complexity | Best For | Historical Data | Relationships | Custom Fields |
|---|---|---|---|---|---|
| CSV Export/Import | Low | < 50 employees, basic data | ❌ Limited | ❌ Lost | ⚠️ Partial |
| API-to-API | High | 50–500 employees, full fidelity | ✅ Yes | ✅ Rebuilt | ✅ Yes |
| Unified API | Medium | Quick prototype, partial sync | ⚠️ Partial | ⚠️ Partial | ❌ No |
| iPaaS (Zapier/Make) | Low | Ongoing sync, not migration | ❌ No | ❌ No | ❌ No |
| Custom ETL / Managed | High | Enterprise, compliance-critical | ✅ Full | ✅ Full | ✅ Full |
Recommendations by Scenario
- Small business, one-time migration, no dev team: CSV export from Humaans + manual cleanup + Officient import. Accept that historical job roles and compensation changes won't transfer cleanly.
- Mid-size company, needs history, has a developer: API-to-API with a custom script. Budget 2–4 weeks of engineering time.
- Enterprise or compliance-critical: Custom ETL pipeline or managed migration service. Don't risk payroll data or audit trails.
- Ongoing sync between systems: Unified API or iPaaS, but only for basic employee data. For anything beyond a narrow field subset, direct API plus webhooks beats iPaaS.
- No engineering bandwidth: Outsource the migration. The opportunity cost of pulling product engineers into a one-off data pipeline is almost always higher than the service fee.
When to Use a Managed Migration Service
Build in-house when you have engineering bandwidth to spare and the migration is straightforward. Consider outsourcing when:
- You're migrating sensitive payroll data (bank accounts, tax IDs, compensation history) where errors have compliance consequences
- The source data has inconsistencies — duplicate employees, orphaned job roles, missing required fields like NISS numbers
- Your team's time is better spent on product work than writing migration scripts
- You need the migration done in days, not weeks
- Officient's rate limits (30 req/5 sec, no
Retry-Afterheader) mean your script needs sophisticated throttling, retry logic, and checkpoint/resume capability
The cost equation: a senior engineer spending 3–4 weeks writing, testing, and debugging a migration script that runs exactly once represents direct product velocity lost. Additionally, DIY migrations frequently encounter silent data integrity issues — a script might successfully create employee profiles but fail to link their historical time-off requests to the correct contract period, or load a contract with an incorrect start date that only surfaces during the first payroll run.
Why ClonePartner
ClonePartner has completed 1,500+ data migrations across HRIS, CRM, and helpdesk platforms. For HRIS migrations specifically:
- Relationship-aware: We rebuild manager hierarchies, department structures, and reporting lines — the data that breaks silently in CSV exports.
- Historical preservation: We migrate full historical timelines for contracts and compensation, ensuring compliance and accurate payroll baselines.
- Custom field mapping: We handle Humaans's typed custom fields (text, select, multiSelect, date, person) and map them to Officient's schema, transforming data types where needed.
- Sensitive data handling: Bank accounts, tax IDs, and identity documents are handled with encryption in transit and at rest.
- Rate limit management: We've built throttling and retry logic for both APIs, including handling Officient's missing
Retry-Afterheader. - Zero downtime: Your HR team works in Humaans on Friday and logs into Officient on Monday. We handle delta syncs over the weekend.
For a deeper look at our process, read How We Run Migrations at ClonePartner.
Pre-Migration Planning
A successful migration is won during the planning phase. Never extract data before you have a fully documented audit.
Data Audit Checklist
Inventory everything in Humaans before moving:
- People — Active, offboarded, new hires. Total count? Do you need offboarded employees in Officient?
- Employee types — Map each person's contract type to Officient's employee types:
employee,freelancer,student_worker,intern. Humaans uses free-textcontractType; Officient enforces specific types that affect payroll calculation. - Job Roles — How many historical role changes per employee? Full history or current roles only?
- Compensations — Salary, bonus, commission, equity records. How many types? Effective-dated history needed?
- Bank Accounts — Sensitive data. Will Officient need this, or does payroll flow through a separate provider?
- Time Away — Entries, balances, policies, allocations. Can you recalculate balances in Officient, or must you carry them over?
- Documents — Employment agreements, identity documents. Total file count and size?
- Equipment — Laptops, key cards, accessories. Serial numbers and costs?
- Custom Fields — List every custom field, its type, and its section. Which are required in your workflows? Identify fields using unsupported types (
person,multiSelect,longText,link) that need transformation. - Locations — Office locations with timezone and public holiday calendar assignments
- Working Patterns — FTE calculations, part-time schedules
- Contracts — Signed employment agreements that may need separate handling
- National identification numbers — Validate that Belgian employees have correctly formatted NISS/INSZ numbers (11 digits, modulo-97 checksum). Officient will reject invalid formats at load time.
What NOT to Migrate
- Demo users (
demo: trueflag in Humaans) - Test data from sandbox environments
- Expired time-away adjustments with no operational value
- Archived working patterns with no active allocations
- Documents with broken file links
- Obsolete custom fields that have no operational value in Officient
GDPR Considerations
This migration involves transferring EU employee PII between two data processors. Key requirements:
- Data Processing Agreement (DPA): Ensure you have active DPAs with both Humaans and Officient covering the migration period. If using a migration partner, they need a DPA as well.
- Lawful basis: Your lawful basis for processing employee data (typically contract performance or legitimate interest) must cover the migration activity. Document this.
- Data minimization: Only migrate data you have a documented need for in Officient. This is a good opportunity to purge data you no longer need.
- Staging environment security: Any intermediate staging database or file store holding employee PII must be encrypted at rest and in transit, access-logged, and destroyed after migration completion.
- Right to erasure: If any employees have pending deletion requests, process those before migration — don't transfer data that should already be deleted.
Define Migration Scope
Decide if you're bringing over 100% of historical data or setting a cutoff date (e.g., only data from the last 3 years). For many teams, migrating current-state data plus the last 2–3 years of history is the sweet spot between compliance needs and migration complexity.
Migration Strategy
| Strategy | When to Use |
|---|---|
| Big bang | Small team (<50), weekend cutover acceptable, clean data |
| Phased | Migrate foundation data first, then active employees, then historical data |
| Parallel run | Both systems live for 2–4 weeks, validating data before cutover |
For most Humaans → Officient migrations, a phased approach works best: migrate foundation data (locations, departments, functions, custom field definitions) first, then active employees with current roles and compensation, then time-off balances, then historical data. Big bang works for small teams with clean data but creates higher risk if anything goes wrong. Parallel runs are safest but create operational overhead.
If you run both systems concurrently, ensure payroll data lives in only one system at a time. Split-brain payroll data leads to compliance violations. Back up raw Humaans exports outside the platform — data export objects expire after a short period.
Data Model & Object Mapping
This is where migrations succeed or fail. Humaans and Officient don't share a common schema, so every object needs explicit mapping.
Core Object Mapping
| Humaans Object | Officient Equivalent | Notes |
|---|---|---|
Person |
People (person detail) |
Core employee record. Most fields map 1:1 but naming differs. Officient requires specific national identification formats depending on the country — Belgian NISS/INSZ is 11 digits with modulo-97 checksum. |
Person.contractType |
Employee type | Humaans is free-text. Officient requires specific types: employee, freelancer, student_worker, intern. These affect payroll calculation logic. |
Company |
Account / Organization | Officient ties to a single account. |
Location |
Office / Location | Humaans has rich location objects with timezone and holiday calendar. Officient handles this differently. |
Job Role |
Role History |
Humaans has effective-dated roles with department, title, manager. Map to Officient's role update endpoint. |
Compensation |
Current Wage / Components |
Major structural mismatch. Humaans stores compensation as separate objects with effective dates. Officient uses wage + monthly components. See detailed section below. |
Time Away |
Days Off |
Humaans has entries, types, policies, allocations, periods, adjustments. Officient has a simpler days-off structure. |
Equipment |
Assets |
Roughly equivalent. Both track type, name, serial number, cost. |
Documents |
Documents | Both support file attachments. File transfer requires download from Humaans + re-upload to Officient. Officient documents are scoped to employee, asset, or car. |
Bank Account |
Banking details (within person) | Humaans has a separate resource. Officient may store this inline or through payroll integration. |
Custom Fields + Custom Values |
Custom fields | Humaans supports 7 field types; Officient supports 6. Custom fields must be created in Officient (via API or UI) before values can be loaded. This is a hard dependency in the load order. |
Working Pattern + Allocation |
Weekly Schedule |
Humaans has multi-week patterns with FTE calculation. Officient uses a simpler weekly schedule. |
Emergency Contact |
Emergency contact data | Straightforward mapping. |
Identity Document |
ID document data | Passport, visa, driving license records. Map expiry dates explicitly so automated alerts continue to function. |
Key Mapping Challenges
Compensation structure mismatch: Humaans stores every compensation change as a separate object with an effectiveDate. Only the most recent non-future-dated entry is considered active. Officient uses a Current Wage endpoint and monthly Components. You need to decide: migrate only the current compensation, or attempt to reconstruct history in Officient. For most teams, migrating the current salary and archiving the compensation history separately is the pragmatic choice. If you need compensation audit trails, export them to a separate archive before migration.
Employee type mapping: Humaans stores contractType as a free-text string. Different employees might have "Full time", "Full-time", "full time", or "Permanent". Officient requires a specific employee type enum (employee, freelancer, student_worker, intern) that directly affects payroll calculation rules. Build a normalization map that converts all Humaans variants to one of Officient's valid types. This cannot be deferred — Officient will reject records with invalid employee types.
Time-away policy architecture: Humaans has a 5-layer hierarchy (Types → Policies → Allocations → Periods → Entries), each with its own API resource. Officient has a flatter model. Migrating accrued balances requires either recalculating in Officient or carrying over computed values. Use Humaans's GET /api/time-away-periods?isCurrentPeriod=true to get computed current balances, then set them as opening balances in Officient rather than trying to replay the full history.
Manager relationships: In Humaans, reportingTo on a Job Role is a personId reference. You must create all employees in Officient first, build an ID mapping table, then update manager references in a second pass. A manager chain (CEO → VP → Director → Manager → IC) requires all people to exist before any manager references can be set. This is a hard dependency — there is no way around the two-pass approach.
Custom field type narrowing: Humaans supports person-type custom fields (references to other employees), multiSelect fields, longText, and link. Officient's custom field types are limited to text, number, date, money, email, and single select_option. You'll need to flatten person references to employee names, multiSelect values to comma-separated strings, longText to text (with potential truncation), and link to text. Archive original values for unsupported types if data fidelity matters.
Custom field pre-creation dependency: Unlike Humaans where custom fields and values are loosely coupled, Officient requires custom field definitions to exist before any values can be assigned. In your load order, create all custom field definitions in Officient immediately after foundation data (locations, departments) and before loading employee records. Use the Officient custom fields API to create definitions programmatically, or create them manually in the Officient admin UI before running the migration script.
Contract date integrity: Officient enforces strict, non-overlapping contracts. Humaans allows messier data entry. Every role change or salary adjustment must be modeled as a new contract or amendment with non-overlapping date ranges. You must clean overlapping dates in your transformation layer before loading. When Officient detects an overlap, it returns an HTTP 422 Unprocessable Entity with an error body indicating the conflicting contract. This is not something you can fix post-load — the record simply won't be created.
Belgian national number (NISS/INSZ) validation: Officient validates Belgian national identification numbers at the point of entry. The NISS/INSZ is an 11-digit number where the last two digits are a modulo-97 checksum of the first nine digits (for people born before 2000) or of 2 prepended to the first nine digits (for people born in 2000 or later). Humaans stores taxId as a free-text string with no validation. If your employees have incorrectly formatted national numbers in Humaans, Officient will reject the record at creation time. Validate all national numbers in your transformation layer before attempting to load. For non-Belgian employees, check Officient's documentation for the expected format for each country code.
Sample Field-Level Mapping Table
| Humaans Field | Humaans Type | Officient Field | Officient Type | Transform |
|---|---|---|---|---|
person.id |
string | external_id |
string | Store for idempotency |
person.firstName |
string | first_name |
string | Direct (required) |
person.lastName |
string | last_name |
string | Direct (required) |
person.email |
string | email |
string | Direct (must be unique) |
person.personalEmail |
string | personal_email |
string | Direct |
person.phoneNumber |
string | phone |
string | Direct |
person.birthday |
date | date_of_birth |
date | Direct |
person.gender |
free-text string | gender |
enum/string | Map to Officient's allowed values |
person.nationality |
string | nationality |
string | Direct |
person.contractType |
free-text string | employee_type |
enum | Map to: employee, freelancer, student_worker, intern |
person.employmentStartDate |
date | start_date |
date | Direct — drives payroll logic |
person.employmentEndDate |
date | end_date |
date | Direct |
person.taxId |
string | national_number |
string | Validate format per country; Belgian NISS/INSZ requires modulo-97 checksum |
person.address + city + postcode + countryCode |
composite | address fields |
composite | Reassemble |
jobRole.jobTitle |
string | role.title |
string | Direct |
jobRole.department |
string | role.department |
string | Direct |
jobRole.reportingTo |
personId ref | manager_id |
integer | Requires ID remapping (second pass) |
compensation.amount |
string | wage.amount |
number | Parse string to number; validate currency code |
compensation.currency |
ISO 4217 | wage.currency |
string | Direct |
compensation.period |
enum | wage.period |
enum | Map period values |
equipment.type |
string | asset.type |
string | Direct |
equipment.serialNumber |
string | asset.serial |
string | Direct |
timeAway.startDate |
date | day_off.start_date |
date | Direct |
timeAway.endDate |
date | day_off.end_date |
date | Direct |
timeAway.name (type) |
string | day_off.type |
enum | Map to Officient's leave types |
Migration Architecture
Data Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Humaans │────▶│ Transform │────▶│Officient │
│ REST API │ │ (ETL Layer) │ │ REST API │
└──────────┘ └──────────────┘ └──────────┘
│ │ │
Extract Map + Clean Load
- /api/people - ID remapping - Create custom field defs
- /api/job-roles - Type conversion - Create people
- /api/comps - Denormalize - Update manager refs
- /api/time-away - Validate NISS - Update roles
- /api/equipment - Employee type map - Set wages
- /api/documents - Contract dates - Create assets
- /api/custom-* - Normalize enums - Create days off
- /api/locations - Upload docs
- Set custom field values
Humaans API: Extraction Specifics
Humaans's API uses $skip/$limit pagination with a max page size of 250. The rate limit uses a token bucket: 40-request burst, 7/sec refill, 400 requests/minute steady state. Every response includes X-RateLimit-Remaining and Retry-After headers. See the Humaans API documentation for full endpoint reference.
For bulk extraction, use the Data Exports API (POST /api/data-exports) first. It generates filtered CSVs or JSON and supports export types for people, equipment, timeAway, timesheet, document, identityDocument, changes, and events. This is significantly faster than paginating through individual resources for the initial pull. Use paginated REST calls for related resources that need their full object structure (job roles, compensations, custom values).
# Trigger a Humaans data export
import requests
headers = {
"Authorization": "Bearer YOUR_HUMAANS_TOKEN",
"Content-Type": "application/json"
}
export_payload = {
"type": "people",
"fields": [
"firstName", "lastName", "email", "personalEmail",
"phoneNumber", "birthday", "nationality", "gender",
"jobTitle", "department", "manager", "managerEmail",
"employmentStartDate", "employmentEndDate",
"contractType", "employeeId", "taxId",
"placeOfWork", "salary"
],
"includeOffboarded": True,
"json": True
}
resp = requests.post(
"https://app.humaans.io/api/data-exports",
headers=headers,
json=export_payload
)
export_meta = resp.json()
# Download the export from export_meta["url"]Officient API: Loading Specifics
Officient uses OAuth 2.0 and JSON-only payloads. The rate limit is 30 requests per 5 seconds — significantly tighter than Humaans. Exceeding this returns a 429 status code with no Retry-After header, so you must implement your own backoff. The API base URL is https://api.officient.io/1.0/. Pagination is page-based starting at page=0. See the Officient API documentation for endpoint reference.
Officient sandbox/test environment: Officient provides sandbox accounts for testing. Contact Officient support to request a test environment before running any migration. Never load untested data directly into a production Officient account.
Common Officient API error responses:
| HTTP Status | Meaning | Common Cause |
|---|---|---|
400 Bad Request |
Malformed payload | Missing required field, wrong data type |
401 Unauthorized |
Invalid or expired OAuth token | Token refresh needed |
404 Not Found |
Resource doesn't exist | Invalid employee ID, nonexistent custom field |
422 Unprocessable Entity |
Validation failure | Overlapping contract dates, invalid NISS format, duplicate email |
429 Too Many Requests |
Rate limit exceeded | No Retry-After header — implement fixed backoff |
Rate limit math: At 30 requests per 5 seconds (6 req/sec sustained), creating 200 employees with their job roles, wages, and time-off records requires roughly 5–6 API calls per employee (create person + update manager + set role + set wage + create days off + custom fields = ~6 calls). That's 1,200 calls ÷ 6 req/sec = 200 seconds minimum, or ~3.3 minutes at full throttle with zero errors. For 500 employees: 3,000 calls ÷ 6 req/sec = 500 seconds = ~8.3 minutes minimum. Factor in retries, validation calls, and document uploads, and a 500-employee migration load phase can take 30–60 minutes. Your pipeline must implement exponential backoff and checkpoint every batch.
Security: You are handling highly sensitive PII and salary data. Ensure your staging environment is encrypted at rest and in transit. Read our guide on How to Safely Migrate Sensitive Employee & Payroll Data before spinning up any infrastructure.
Step-by-Step Migration Process
Step 1: Set Up Foundation Data in Officient
Create locations, departments/functions, leave types, working patterns, and custom field definitions in Officient first. These are dependencies that must exist before employee records can reference them.
Custom field definitions are a commonly missed dependency. In Officient, you cannot assign a custom field value to an employee unless the custom field definition already exists. Create these via the Officient custom fields API or manually in the admin UI. Map each Humaans custom field ID to its corresponding Officient custom field ID and store this mapping for the load phase.
Step 2: Extract Data from Humaans
Export people data via the Data Exports API with all needed fields. Fetch related resources via paginated REST calls:
GET /api/people(include terminated employees if historical compliance is required)GET /api/job-roles(all historical, not just current)GET /api/compensationsGET /api/bank-accountsGET /api/time-away(with date range filters)GET /api/time-away-periods?isCurrentPeriod=true(for computed balances)GET /api/equipmentGET /api/documents+ download files viaGET /api/files/:idGET /api/custom-fields+GET /api/custom-valuesGET /api/locationsGET /api/emergency-contacts
Store raw data in a secure staging database or structured JSON files. Ensure you capture all pages — Humaans uses $skip/$limit pagination and you must loop until skip + limit >= total.
Step 3: Transform and Cleanse
- Build location and department mappings: Map Humaans
locationIdand department strings to Officient IDs. - Normalize person data: Convert gender values (Humaans is free-text; Officient may require specific values), employee types (free-text
contractType→ Officient's enum:employee,freelancer,student_worker,intern), address formatting, and phone numbers. - Validate national identification numbers: For Belgian employees, validate the NISS/INSZ checksum. For employees born before 2000:
97 - (first_9_digits mod 97) == last_2_digits. For employees born 2000+:97 - ((2_prepended_to_first_9_digits) mod 97) == last_2_digits. Flag invalid numbers for manual correction. - Flatten compensation: Extract the currently active compensation per type using
effectiveDatesorting. Convert to Officient's wage format. Parse amount from string to number. - Map time-away types: Map each Humaans time-away type (PTO, sick, parental) to Officient's leave type IDs.
- Transform custom fields: Map Humaans custom field IDs to Officient custom field IDs. Convert data types where needed. Flatten unsupported types (
person→ employee name string,multiSelect→ comma-separated string,longText→text,link→text). - Handle nulls and required fields: If Humaans has a null value for a field Officient marks as required (like
start_dateorfirst_name), flag it for manual review or apply a documented fallback value. Log every fallback applied. - Resolve overlapping contracts: Sort job role history by
effectiveDate. Ensure no two contracts for the same employee have overlapping date ranges. Adjust end dates where needed and log every adjustment. This must be resolved before loading — Officient returns422for overlapping dates with no automatic resolution. - Normalize free-text enums: Build a normalization map for all free-text fields that map to Officient enums. For
contractType: {"Full time" → "employee", "Full-time" → "employee", "full time" → "employee", "Freelance" → "freelancer", "Student" → "student_worker"}. Review and validate the map before loading.
Step 4: Load into Officient
Load order matters. Create records in dependency order:
- Custom field definitions (if not created manually in Step 1)
- People (without manager references) — get Officient IDs back
- Update manager relationships (second pass, using ID mapping table)
- Roles and wages
- Assets/equipment
- Days off / time-away entries
- Documents (download from Humaans, re-upload to Officient)
- Custom field values
Contracts must be loaded in chronological order to prevent validation errors. Officient will reject a contract with a 422 Unprocessable Entity if its dates overlap with an existing one for the same employee.
Signed contracts: If you're migrating already-signed employment agreements, Officient's contract API supports presigned_pdf. Load historical signed files as presigned_pdf rather than re-sending them for signature.
Step 5: Validate
- Record counts: Compare total people (active + offboarded) in both systems.
- Field-level spot check: Sample 10–20% of employees and verify every mapped field — salary, start date, manager, department, custom fields, employee type, national number.
- Manager hierarchy: Confirm reporting lines are correct for every manager. Traverse the full hierarchy from CEO to individual contributors.
- Compensation: Verify current salary, currency, and period matches for all employees.
- Time-off balances: If carried over, confirm balances match Humaans's computed values from
/api/time-away-periods. - Document audit: Confirm file counts per employee match. Open sample documents to verify they're not corrupted.
- Custom field verification: Check all custom field values transferred correctly, especially
select_optiontypes where value mapping may have introduced errors. - Employee type verification: Confirm every employee has the correct type in Officient, as this directly affects payroll calculation.
Automation Script Outline
Below is a structural Python outline for extracting from Humaans and loading into Officient. This demonstrates the pattern — production code needs comprehensive error handling, logging, checkpoint/resume capability, and validation at every stage.
import requests
import time
import json
import logging
from typing import Dict, List, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("hris_migration")
class HumaansExtractor:
BASE_URL = "https://app.humaans.io/api"
def __init__(self, token: str):
self.headers = {"Authorization": f"Bearer {token}"}
def paginate(self, endpoint: str, params: dict = None) -> List[dict]:
results = []
skip = 0
limit = 250
while True:
p = {"$limit": limit, "$skip": skip, **(params or {})}
resp = requests.get(
f"{self.BASE_URL}/{endpoint}",
headers=self.headers, params=p
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 2))
logger.warning(f"Rate limited on {endpoint}, waiting {retry_after}s")
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
if "data" not in data or "total" not in data:
logger.error(
f"Unexpected response from {endpoint}: "
f"missing 'data' or 'total' key. "
f"Status: {resp.status_code}, Body: {resp.text[:500]}"
)
raise ValueError(f"Malformed API response from {endpoint}")
results.extend(data["data"])
if skip + limit >= data["total"]:
break
skip += limit
logger.info(f"Extracted {len(results)} records from {endpoint}")
return results
def extract_all(self) -> dict:
return {
"people": self.paginate("people"),
"job_roles": self.paginate("job-roles"),
"compensations": self.paginate("compensations"),
"bank_accounts": self.paginate("bank-accounts"),
"equipment": self.paginate("equipment"),
"time_away": self.paginate("time-away"),
"time_away_periods": self.paginate(
"time-away-periods", {"isCurrentPeriod": "true"}
),
"locations": self.paginate("locations"),
"custom_fields": self.paginate("custom-fields"),
"custom_values": self.paginate("custom-values"),
"emergency_contacts": self.paginate("emergency-contacts"),
}
class OfficientLoader:
BASE_URL = "https://api.officient.io/1.0"
def __init__(self, access_token: str):
self.headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
self.id_map: Dict[str, int] = {} # humaans_id -> officient_id
self.failed_records: List[dict] = [] # dead letter queue
self._request_timestamps: List[float] = []
def _throttled_request(self, method: str, url: str, **kwargs) -> requests.Response:
"""Enforce Officient's 30 req/5 sec limit with exponential backoff."""
# Simple rate limiter: ensure at least 0.17s between requests
now = time.time()
self._request_timestamps = [
t for t in self._request_timestamps if now - t < 5
]
if len(self._request_timestamps) >= 29: # leave 1 req headroom
sleep_time = 5 - (now - self._request_timestamps[0])
if sleep_time > 0:
time.sleep(sleep_time)
resp = requests.request(method, url, headers=self.headers, **kwargs)
self._request_timestamps.append(time.time())
if resp.status_code == 429:
# Officient returns no Retry-After header — use exponential backoff
for attempt in range(5):
backoff = 2 ** attempt # 1, 2, 4, 8, 16 seconds
logger.warning(f"429 from Officient, backoff {backoff}s (attempt {attempt+1})")
time.sleep(backoff)
resp = requests.request(method, url, headers=self.headers, **kwargs)
self._request_timestamps.append(time.time())
if resp.status_code != 429:
break
else:
logger.error(f"Persistent 429 after 5 retries: {url}")
return resp
def create_person(self, person_data: dict, humaans_id: str) -> Optional[int]:
resp = self._throttled_request(
"POST", f"{self.BASE_URL}/people",
json=person_data
)
if resp.status_code in (200, 201):
officient_id = resp.json().get("id")
logger.info(f"Created person {humaans_id} -> {officient_id}")
return officient_id
else:
logger.error(
f"Failed to create person {humaans_id}: "
f"HTTP {resp.status_code}, Body: {resp.text[:500]}"
)
self.failed_records.append({
"humaans_id": humaans_id,
"endpoint": "people",
"status": resp.status_code,
"error": resp.text[:500],
"payload_hash": hash(json.dumps(person_data, sort_keys=True))
})
return None
def person_exists(self, email: str) -> Optional[int]:
"""Check if person already exists in Officient (idempotency)."""
resp = self._throttled_request(
"GET", f"{self.BASE_URL}/people",
params={"search": email}
)
if resp.status_code == 200:
results = resp.json().get("data", [])
if results:
return results[0].get("id")
return None
def load_people(self, transformed_people: List[dict]):
for person in transformed_people:
humaans_id = person.pop("_humaans_id")
# Idempotency check: skip if already exists
existing_id = self.person_exists(person.get("email", ""))
if existing_id:
self.id_map[humaans_id] = existing_id
logger.info(f"Skipped existing person {humaans_id} -> {existing_id}")
continue
officient_id = self.create_person(person, humaans_id)
if officient_id:
self.id_map[humaans_id] = officient_id
def save_checkpoint(self, filepath: str):
"""Persist ID map and failed records for resume capability."""
with open(filepath, "w") as f:
json.dump({
"id_map": self.id_map,
"failed_records": self.failed_records
}, f, indent=2)
logger.info(f"Checkpoint saved: {len(self.id_map)} mapped, {len(self.failed_records)} failed")
def load_checkpoint(self, filepath: str):
"""Resume from a previous checkpoint."""
try:
with open(filepath, "r") as f:
data = json.load(f)
self.id_map = data.get("id_map", {})
self.failed_records = data.get("failed_records", [])
logger.info(f"Checkpoint loaded: {len(self.id_map)} mapped, {len(self.failed_records)} failed")
except FileNotFoundError:
logger.info("No checkpoint found, starting fresh")
def validate_niss(niss: str, birth_year: int) -> bool:
"""Validate Belgian NISS/INSZ national number checksum."""
if not niss or len(niss) != 11 or not niss.isdigit():
return False
first_nine = int(niss[:9])
check_digits = int(niss[9:])
# For people born before 2000
if birth_year < 2000:
return (97 - (first_nine % 97)) == check_digits
else:
# For people born 2000+, prepend 2 to the first 9 digits
return (97 - (int(f"2{niss[:9]}") % 97)) == check_digits
# Normalization map for free-text contract types
CONTRACT_TYPE_MAP = {
"full time": "employee",
"full-time": "employee",
"fulltime": "employee",
"part time": "employee",
"part-time": "employee",
"permanent": "employee",
"fixed term": "employee",
"fixed-term": "employee",
"freelance": "freelancer",
"contractor": "freelancer",
"student": "student_worker",
"student worker": "student_worker",
"intern": "intern",
"internship": "intern",
}
def transform_person(humaans_person: dict) -> dict:
"""Map Humaans person to Officient schema."""
contract_type_raw = (humaans_person.get("contractType") or "").strip().lower()
employee_type = CONTRACT_TYPE_MAP.get(contract_type_raw, "employee")
if contract_type_raw and contract_type_raw not in CONTRACT_TYPE_MAP:
logger.warning(
f"Unknown contractType '{humaans_person.get('contractType')}' "
f"for person {humaans_person['id']}, defaulting to 'employee'"
)
return {
"_humaans_id": humaans_person["id"],
"first_name": humaans_person["firstName"],
"last_name": humaans_person["lastName"],
"email": humaans_person.get("email"),
"personal_email": humaans_person.get("personalEmail"),
"phone": humaans_person.get("phoneNumber"),
"date_of_birth": humaans_person.get("birthday"),
"gender": humaans_person.get("gender"),
"nationality": humaans_person.get("nationality"),
"employee_type": employee_type,
"start_date": humaans_person.get("employmentStartDate"),
"end_date": humaans_person.get("employmentEndDate"),
"national_number": humaans_person.get("taxId"),
}This is a structural outline demonstrating the pattern, not production-ready code. A real migration script additionally needs: structured logging with correlation IDs, a dry-run mode that validates without writing, integration tests against an Officient sandbox, and a final reconciliation report comparing source and target record counts per entity type.
Edge Cases & Challenges
Duplicate and inconsistent records: Humaans allows free-text for fields like contractType and gender. If different employees have "Full time", "Full-time", and "full time", these are distinct values in Humaans but should map to one value in Officient. Build a normalization map before loading and log every normalization applied.
Overlapping contracts: Officient returns 422 Unprocessable Entity when a contract's dates overlap with an existing contract for the same employee. The error body will indicate the conflicting date range. Humaans allows messier data entry. Resolve overlapping dates in your transformation layer — Officient does not auto-resolve overlaps, and the record simply won't be created.
Multi-currency compensation: If you have employees in different regions, ensure Officient is configured to accept multiple currencies before pushing compensation data. Officient is primarily designed for EUR-denominated wages; non-EUR currencies may require configuration changes.
Time-off balance reconstruction: Humaans computes time-away balances dynamically from policies, allocations, entries, adjustments, and carry-over rules. Officient requires you to set an opening balance rather than replay the full history. Use Humaans's GET /api/time-away-periods?isCurrentPeriod=true to get computed balances, then set them as starting balances in Officient. Migrate historical requests as read-only records where possible.
Document file transfer: Documents in Humaans are stored as Files with authenticated download URLs. You must download each file, then re-upload to Officient. This is bandwidth-intensive and should be done in batches. Track document expiry dates (e.g., visas) and map them to Officient's document metadata so automated alerts continue.
Company-wide documents: Humaans can hold company-wide documents. Officient documents are tied to employee, asset, or car, and contracts are separate. Company-wide content with no employee association may need archival outside Officient rather than direct migration.
Custom field type incompatibility: Humaans supports person-type custom fields (referencing another employee by ID), multiSelect fields, longText, and link. Officient doesn't support these types. Flatten person references to employee names, multiSelect to comma-separated strings, longText to text, and link to text.
Orphaned manager references: If a manager has left the company, Humaans might leave their direct reports pointing to a terminated person ID. Officient may require you to reassign these reports to an active user during import. Check for terminated managers with active direct reports before loading.
New hire vs active status: Humaans newHire and active records may share person data but differ in email completeness. Decide which email is authoritative before insert. If a newHire record has no work email, flag it for HR review.
Null required fields: When Humaans has a null value for a field Officient requires (such as start_date), Officient will return 400 Bad Request. Your transformation layer must either flag these for manual review, apply a documented fallback value, or route the record to a dead letter queue. Log every fallback applied so HR can review and correct the data post-migration.
Limitations & Constraints
| Constraint | Detail |
|---|---|
| Officient rate limit | 30 requests per 5 seconds — no Retry-After header on 429 responses; implement your own exponential backoff |
| Humaans rate limit | 40 burst / 7 per second refill / 400 per minute; X-RateLimit-Remaining and Retry-After headers provided |
| No bulk import API | Neither platform offers a bulk-create endpoint — records must be created one at a time |
| Compensation model mismatch | Humaans's effective-dated multi-type compensation vs. Officient's current wage + components |
| Time-away policy complexity | Humaans's 5-layer policy structure doesn't map to Officient's simpler model |
| Working pattern precision | Humaans calculates FTE from detailed multi-week patterns; Officient uses weekly schedules |
| Custom field limits | Officient supports 6 types (text, number, date, money, email, select_option) vs. Humaans's 7 (adds person, multiSelect, longText, link). Custom fields must be pre-created before values are loaded. |
| Employee type enforcement | Officient requires specific employee types (employee, freelancer, student_worker, intern) that affect payroll; Humaans uses free-text |
| National number validation | Officient validates Belgian NISS/INSZ format at entry; Humaans stores tax IDs as free-text |
| Document size | Large document libraries (thousands of files) add significant transfer time |
| Audit trail | Humaans's audit events are read-only and don't transfer to Officient |
| Schema rigidity | Officient does not allow the same level of free-form data. Unstructured data may need to go into Notes fields |
| Connector parity | SD Worx, Nmbrs, Partena, and The Payroll Office connectors do not all support identical import behavior |
Validation & Testing
Testing is not optional. Execute at least two dry runs in an Officient sandbox before the final cutover. Contact Officient support to provision a sandbox environment.
Pre-Go-Live Checklist
- Record count parity:
Count(Humaans.People) == Count(Officient.People)for both active and offboarded - Field-level sampling: Pick 20 random employees. Verify every mapped field matches — including salary, start date, manager, department, custom fields, employee type, and national number.
- Manager chain validation: For each manager, confirm their direct reports list matches Humaans. Traverse full hierarchy.
- Compensation spot-check: Verify current salary, currency, and period for all employees
- Employee type verification: Confirm every employee has the correct type (
employee,freelancer,student_worker,intern) as this directly affects payroll calculation - National number validation: Confirm all Belgian employees have valid NISS/INSZ numbers that pass checksum validation
- Time-off balance verification: Compare Humaans period balances (from
/api/time-away-periods?isCurrentPeriod=true) with Officient opening balances - Document audit: Confirm file counts per employee match. Open sample documents to verify they're not corrupted.
- Custom field verification: Check all custom field values transferred correctly, especially
select_optiontypes where value mapping may have introduced errors - Failed record review: Review the dead letter queue for any records that failed to load. Resolve root causes and reload.
UAT Process
- Run the migration against a sandbox Officient instance
- Have 3–5 HR team members validate their own profiles and their team's data
- Have the payroll team confirm wage data and employee types are correct
- Run one payroll simulation cycle before cutting over
- If the numbers match the legacy system, the data model is sound
Rollback Plan
Officient supports deleting people via API, so a rollback means deleting all migrated records and starting fresh. Keep the Humaans account active and read-only during the migration window. Don't cancel the Humaans subscription until Officient is fully validated and at least one payroll run has completed successfully.
Post-Migration Tasks
Once the data is verified in production:
- Rebuild workflows: Officient workflows (onboarding, offboarding) are configured separately from Humaans and won't transfer. Recreate them manually.
- Reconnect integrations: Payroll provider connections, ATS integrations (Officient integrates with Greenhouse, Workable, Recruitee), calendar sync, SSO — all need fresh configuration. Any webhooks or Zapier integrations that previously triggered off Humaans (Slack onboarding alerts, IT provisioning) must be rebuilt against Officient's webhook API.
- Re-invite employees: Officient Self Service is invitation-based. Send invitations and provide documentation on the new UI for time-off requests and self-service tasks.
- Monitor payroll: Watch the first two payroll runs closely for data mapping anomalies — especially employee type mismatches, incorrect wage components, or missing national numbers. Monitor for missing records or broken references for at least two weeks.
- Archive Humaans data: Export a full backup from Humaans before deactivation. Use the Data Exports API for each export type (
people,equipment,timeAway,document,identityDocument,timesheet,changes,events). Store these exports outside the platform in durable storage — Humaans data export objects expire. - GDPR housekeeping: Delete any staging data, intermediate files, and temporary databases used during migration. Document the migration in your data processing records.
Best Practices
- Always back up first. Run a full Humaans export before touching anything. Store it outside the platform in encrypted, durable storage.
- Freeze data entry. Implement a hard freeze on Humaans data entry 48 hours before the final cutover. If the migration window is longer, plan a delta sync pass after the initial load using Humaans filters (
$filter [updatedAfter]=TIMESTAMP) or webhooks. - Run test migrations. Use an Officient sandbox account. Never load untested data into production. Run at least two full dry runs.
- Validate incrementally. Don't wait until the end to check data. Validate after each entity type loads — people, then roles, then wages, then time off.
- Log everything. Your ETL pipeline must log every successful API call and every failure with source ID, endpoint, payload hash, HTTP status, and response body. If a single document fails to upload, you need the exact employee ID and file name.
- Map iteratively. Map core fields first (name, email, start date). Validate. Then move on to complex objects (contracts, time off, compensation). Don't try to map the entire schema in one sprint.
- Document your field mapping. Maintain a living spreadsheet of Humaans field → Officient field mappings with transformation rules. This becomes your migration spec, your audit artifact, and your GDPR documentation.
- Build idempotent scripts. Search Officient by email or national number before creating records to prevent duplicates. Store the Humaans-to-Officient ID map persistently (checkpoint file) so you can resume after failures without re-creating records.
- Handle errors as first-class citizens. Route failed records to a dead letter queue with full context. Review and resolve every failed record before go-live.
What Comes Next
Migrating from Humaans to Officient is a high-stakes engineering project masquerading as an HR task. The technical complexities — mapping flexible data into strict payroll schemas, handling two different rate-limiting regimes, validating Belgian national numbers, preserving historical contract integrity, mapping free-text employee types to payroll-affecting enums, pre-creating custom field definitions, and flattening a 5-layer time-off model into a simpler structure — require serious architectural planning.
For most teams, the safest path is: export from Humaans, transform in a staging layer (with validation, normalization, and NISS checksum verification), load into Officient via API in dependency order (custom fields → people → managers → roles → wages → assets → days off → documents), then run a delta pass and UAT against a sandbox before production cutover.
CSV-only methods work for very small current-state moves. Historical contracts, documents, custom fields, employee type mapping, and wage data are where DIY projects break.
If your engineering team's mandate is to build product — not to reverse-engineer Officient's contract validation rules and NISS checksum algorithms — offloading the migration is the pragmatic move.
Frequently Asked Questions
- Can I export data directly from Humaans to Officient?
- No. There is no native export-to-Officient feature in Humaans. You must extract data via Humaans's REST API or Data Exports API, transform it to match Officient's schema, and load it via the Officient API or manual import.
- What is Officient's API rate limit for data migration?
- Officient allows a maximum of 30 API requests per 5 seconds. Exceeding this returns a 429 status with no Retry-After header, so you must implement your own backoff. For a 500-employee migration with related data, expect the load phase to take 30+ minutes at minimum.
- Does compensation history transfer from Humaans to Officient?
- Not automatically. Humaans stores every salary change as a separate effective-dated Compensation object. Officient uses a current-wage model. You can migrate the current compensation directly, but historical changes must be archived separately or discarded.
- How do I handle custom fields during the migration?
- Humaans supports 7 custom field types including multiSelect and person references. Officient supports fewer types (text, number, date, money, email, select_option). You must audit all custom fields, recreate compatible ones in Officient before migration, and flatten unsupported types to text or archive them.
- How do I preserve signed contracts when moving to Officient?
- Officient's contract API supports a presigned_pdf parameter. Historical signed employment agreements should be loaded as presigned_pdf rather than re-sent for signature. Treat signed contracts as a separate workstream from the core employee data migration.

