Interface Control Document (ICD): HRMS - EPC
---
| Version | Status | Update Comment | By | Date |
|---|---|---|---|---|
| 1.0 | Draft | Initial Draft | Retter | 05 Feb 2026 |
| 1.1 | Draft | Standardized payload: HRMS sends full record, overwrites member data | Retter | 10 Feb 2026 |
| 1.2 | Draft | Added companyId (HRMS Business Group ID / BG_ID, required & non-null) and middleName fields; documented company-reassignment use case and the 404/409 BG_ID error responses |
Retter | 23 Jun 2026 |
| 1.3 | Draft | Added optional joiningDate (HRMS JOINING_DATE). Unlike every other field, omitting it or sending null leaves the stored value unchanged rather than clearing it |
Retter | 26 Aug 2026 |
| 1.4 | Draft | EPC-1289: address.address is the HOME address (CUR_RES_ADDRESS, as in the Emirates push ICD); new optional addressOptional is the WORK location. Country compared after parsing. Pushes for members not registered through HRMS are accepted and not applied (skipped) |
Retter | 16 Oct 2026 |
| 1.5 | Draft | EPC-1290: maritalStatus is optional and never rejected (Divorced, Widowed and Separated are stored as single; an unknown value keeps the stored one). birthdate accepts the Oracle timestamp and a date-time with offset and is stored as YYYY-MM-DD. An active status reactivates only a member HRMS deactivated. The email is not written when another member already uses it. A company change moves the dependants too |
Retter | 20 Oct 2026 |
| 1.6 | Draft | EPC-1259: pushes for members of a company managed by hand in EPC, or that would move a member into such a company, are accepted and not applied (skipped: company_hrms_sync_disabled) |
Retter | 19 Oct 2026 |
---
| Abbreviation | Description |
|---|---|
| HRMS | Human Resource Management System |
| EPC | Emirates Platinum Card |
| API | Application Programming Interface |
| SSM | AWS Systems Manager (Parameter Store) |
---
When employee data changes in the HRMS (name, email, phone, grade, status, etc.), the HRMS system will push the updates to the Platinum Application. This ensures that member profiles remain synchronized with the authoritative HR data source.
> Standardized Payload: HRMS should always send the full employee record in every request. The EPC system overwrites the member record with the incoming HRMS data.
---
| Risk | Mitigation |
|---|---|
| Network timeouts between HRMS and EPC | Configure appropriate timeout settings (recommended: 30s) |
| Employee not found in EPC | HRMS should handle 404 responses gracefully and log for review |
---
| Sr No. | Non-Functional Requirement | Value |
|---|---|---|
| 1 | PHI or Credit Card data? | No credit card data & no patient health information |
| 2 | Authentication Pattern | API Key (X-API-Key header) |
| 3 | Secure Channel / Encryption in Transit | TLS 1.2+ (HTTPS only) |
| 4 | Input validation | Yes - all fields validated via JSON Schema |
| 5 | Internet exposed? IP whitelisting? | Yes, exposed via CloudFront. IP whitelisting can be implemented via WAF rules |
| 6 | Logging | All requests logged with sanitized payloads (sensitive fields redacted) |
| 7 | Data exposure | Only necessary fields returned. Sensitive member data not included in response |
---
| Component | Value |
|---|---|
| Source | HRMS (Human Resource Management System) |
| Source Protocol | HTTPS REST API |
| Source Data Format | JSON |
| Target | EPC (Emirates Platinum Card) |
| Target Protocol | HTTPS |
| Target Data Format | JSON |
┌─────────────┐ ┌─────────────┐
│ │ HTTPS / JSON │ │
│ HRMS │ ─────────────────────────▶ │ EPC API │
│ (Source) │ POST │ (Target) │
│ │ ◀───────────────────────── │ │
└─────────────┘ HTTPS / JSON └─────────────┘
Flow:
X-API-Key headerAPI Monitoring
Exception Handling
| HTTP Status | Scenario | HRMS Action |
|---|---|---|
| 200 | Success | Log success, continue |
| 400 | Validation error | Fix request data, retry |
| 401 | Invalid API key | Check API key, contact EPC team |
| 404 | Employee not found, or companyId (BG_ID) not mapped to any Platinum company |
Log for manual review |
| 409 | companyId (BG_ID) resolves to a company of a different group type than the member's current company |
Log for manual review; do not retry without correction |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Retry later |
Actors: HRMS System (automated)
Trigger: Employee data change in HRMS (name, email, phone, grade, address)
Flow:
Actors: HRMS System (automated)
Trigger: Employee's staff ID changes in HRMS
Flow:
employeeId and oldStaffIdemployeeIdoldStaffIdpreviousStaffIds arraystaffIdMigrated: trueActors: HRMS System (automated)
Trigger: Employee status changes to terminated/inactive in HRMS
Flow:
status: "terminated" and optional leavingReasonInactiveActors: HRMS System (automated)
Trigger: Previously terminated employee is rehired
Flow:
status: "active"Actors: HRMS System (automated)
Trigger: Employee moves to a different business group in HRMS
Flow:
companyId set to the employee's current HRMS Business Group ID (BG_ID) — this key is always sent (required, non-null)bgId matches companyId404 (HRMS_COMPANY_NOT_FOUND, code 11016) and rejects the whole requestcompany is not added to updatedFieldsinternal ↔ external) than the member's current company, EPC returns 409 (HRMS_COMPANY_GROUP_TYPE_MISMATCH, code 11017) and rejects the requestcompany in updatedFieldsTrigger: The staff ID belongs to a member who registered another way (self-registered, associate company or created in the back office)
Flow:
200 with updatedFields: [] and skipped: "not_hrms_registered"Trigger: The member belongs to a company whose EPC setting "Disable integrations and self-registrations" is on, or companyId (BG_ID) resolves to such a company while the member belongs to another one
Flow:
200 with updatedFields: [] and skipped: "company_hrms_sync_disabled"---
> Null convention: For optional fields that have no value, send null — not an empty string (""). Empty strings are rejected by validation.
>
> Full-record convention: HRMS should always send the full employee record in every request. The EPC system overwrites the member record with the incoming HRMS data.
>
> Exception — joiningDate, maritalStatus and addressOptional are retained, not cleared: for these fields omitting the key, or sending null (or an empty string for maritalStatus and addressOptional), means *"no value supplied"* rather than *"clear this value"*. EPC keeps whatever value it already holds (from a previous push, or from the outbound getEmployeeDetails fetch). Every other nullable field follows the same retention rule in practice, but these are the fields where HRMS may legitimately omit the key entirely.
>
> Same rules on every HRMS path (EPC-1290): this push, the dependant push and the back office "Update HRMS data" pull apply one rule set, so the same HRMS record always gives the same member record.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| employeeId | String | Yes | Employee's current staff ID (primary lookup key) | "EK123456" |
| name | String | Yes | Employee's first name | "John" |
| middleName | String | Optional | Employee's middle name | "William" |
| surname | String | Yes | Employee's last name | "Doe" |
| String | Yes | Employee's email address (must be valid email format). Not written when the member's current email is on an SSO domain, or when another member already uses this address; the rest of the request is still applied and the skip is recorded in the member's history | "john.doe@emirates.com" | |
| phone | String | Yes | Employee's phone number | "+971501234567" |
| grade | String | Yes | Employee's job grade | "EK.07" |
| employmentType | String | Yes | Type of employment. Must match an active member type configured in the backoffice. Returns 400 if invalid. | "EMP" |
| status | String | Yes | HR status: "active", "inactive", "terminated" | "active" |
| address | Object | Yes | Address information | See below |
| address.country | String | Yes | Country (Staff Cost Centre belongs to) | "Emirates Group - UAE" |
| address.address | String | Yes | HOME address (CUR_RES_ADDRESS). Stored as the member's home address | "Al Nahda, Dubai" |
| addressOptional | String | Optional | WORK location (WORK_LOC_ADDRESS). Stored as the member's work address. Omitted, null or empty leaves the stored work address unchanged | "Dubai Airport Free Zone" |
| birthdate | String (date) | Yes | Birth date. Accepts YYYY-MM-DD, the HRMS form 1990-01-15 00:00:00.0 or a date-time with an offset such as 1990-01-15T00:00:00.000+04:00. Stored as the date part, YYYY-MM-DD, never converted to another time zone |
"1990-01-15" |
| gender | String | Yes | Employee's gender: M, F, Male or Female (any case). Any other value returns 400 | "Male" |
| maritalStatus | String | Optional / Nullable | Employee's marital status, any case. M or Married is stored as married. S, Single, Divorced, Widowed or Separated is stored as single. Any other value keeps the stored marital status and is recorded in the member's history. Never returns 400. Changing to married deactivates the member's active companion. See the retention note above | "Married" |
| coreGrade | String | Yes | Core grade code | "EK.07" |
| dependantsExists | String | Yes | Whether employee has dependants ("Y" or "N") | "Y" |
| oldStaffId | String | Nullable | Previous staff ID (for migration scenarios) | "EK000001" |
| lastDayOfService | String (date) | Nullable | Termination date (ISO 8601) | "2026-03-31" |
| leavingReason | String | Nullable | Reason for leaving (terminations) | "Retirement" |
| joiningDate | String (date) | Optional / Nullable | Date employment started (HRMS JOINING_DATE). Accepts YYYY-MM-DD or the raw HRMS form 2008-09-23 00:00:00.0; both are stored as YYYY-MM-DD. A present-but-unparseable value returns 400. See the retention note above. |
"2008-09-23" |
| companyId | String | Yes | HRMS Business Group ID (BG_ID). Always send the employee's current BG_ID. The member is reassigned to the Platinum company whose bgId matches this value; sending the unchanged BG_ID is a no-op. Returns 404 (code 11016) if unmapped, 409 (code 11017) on group-type mismatch. |
"61" |
| Field | Type | Description |
|---|---|---|
| success | Boolean | True if operation succeeded |
| message | String | Human-readable message |
| body.memberId | String (UUID) | Member's ID in EPC system |
| body.staffId | String | Current staff ID |
| body.updatedFields | Array[String] | List of fields that were updated |
| body.staffIdMigrated | Boolean | True if staff ID was migrated |
| body.skipped | String | Present when the push was accepted but not applied; updatedFields is then empty. not_hrms_registered: member not registered through HRMS. company_hrms_sync_disabled: the member's company, or the company the BG_ID points to, is managed by hand in EPC |
---
Authentication: API Key via X-API-Key header
X-API-Key: <your-api-key>
Content-Type: application/json
HRMS should always send the complete employee record with every request. The EPC system overwrites the member record with the incoming HRMS data. The updatedFields array in the response tells HRMS which fields were modified.
> Null convention: For fields that have no value, send null — not an empty string (""). Empty strings are rejected by validation.
Request:
POST /members/hr/employee HTTP/1.1
Host: uat.api.platinum.retter.io
X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"employeeId": "EK123456",
"oldStaffId": null,
"email": "john.doe@emirates.com",
"phone": "+971501234567",
"name": "John",
"middleName": "William",
"surname": "Doe",
"grade": "EK.07",
"employmentType": "EMP",
"status": "active",
"address": {
"country": "Emirates Group - UAE",
"address": "Al Nahda, Dubai"
},
"addressOptional": "Dubai Airport Free Zone",
"lastDayOfService": null,
"birthdate": "1990-01-15",
"gender": "Male",
"maritalStatus": "Married",
"dependantsExists": "Y",
"coreGrade": "EK.07",
"joiningDate": "2008-09-23",
"leavingReason": null,
"companyId": "61"
}
Response (200 OK) — only grade and phone differed from current record:
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK123456",
"updatedFields": ["phone", "grade"]
}
}
Response (200 OK) — nothing changed (all values match current record):
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK123456",
"updatedFields": []
}
}
Response (200 OK) — member not registered through HRMS, push not applied:
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK123456",
"updatedFields": [],
"skipped": "not_hrms_registered"
}
}
Response (200 OK) — member's company managed by hand in EPC, push not applied:
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK123456",
"updatedFields": [],
"skipped": "company_hrms_sync_disabled"
}
}
| Scenario | Key Fields | Response Indicator |
|---|---|---|
| Profile Update | Any non-null field | updatedFields lists written fields |
| Staff ID Migration | oldStaffId provided, member found by oldStaffId |
staffIdMigrated: true in response |
| Termination | status: "terminated" or "inactive" |
updatedFields includes "status" |
| Reactivation | status: "active" (member currently Inactive, deactivated by HRMS) |
updatedFields includes "status" |
Staff ID Migration example — employee's ID changed from EK123456 to EK999999:
{
"employeeId": "EK999999",
"oldStaffId": "EK123456",
"email": "john.doe@emirates.com",
"phone": "+971501234567",
"name": "John",
"surname": "Doe",
"grade": "EK.07",
"employmentType": "EMP",
"status": "active",
"...": "... (all other fields)"
}
Response:
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK999999",
"updatedFields": ["staffId", "previousStaffIds"],
"staffIdMigrated": true
}
}
Termination example — status changed to "terminated":
{
"employeeId": "EK123456",
"status": "terminated",
"leavingReason": "Retirement",
"lastDayOfService": "2026-03-31",
"...": "... (all other fields)"
}
Response:
{
"success": true,
"message": "Employee updated successfully.",
"body": {
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"staffId": "EK123456",
"updatedFields": ["status", "statusReason"]
}
}
---
{
"success": false,
"error": {
"code": 10001,
"message": "Validation error"
},
"details": {
"errors": {
"employeeId": ["Required"]
}
}
}
{
"success": false,
"error": {
"code": 10072,
"message": "Invalid API key"
}
}
{
"success": false,
"error": {
"code": 10073,
"message": "Employee not found"
}
}
Returned when companyId is non-null but no Platinum company is configured with that HRMS Business Group ID.
{
"success": false,
"error": {
"code": 11016,
"message": "No Platinum company is mapped to the supplied HRMS business group id (BG_ID)."
}
}
Returned when the company resolved from companyId belongs to a different group type (internal ↔ external) than the member's current company.
{
"success": false,
"error": {
"code": 11017,
"message": "Cannot move member: target company belongs to a different company group type."
}
}
{
"success": false,
"error": {
"code": 10000,
"message": "Internal server error"
}
}
---
| Environment | URL |
|---|---|
| Staging | https://staging.api.platinum.retter.io/members/hr/employee |
| UAT | https://uat.api.platinum.retter.io/members/hr/employee |
---
| Status | Description |
|---|---|
| Active | Member is active and can use benefits |
| Inactive | Member is deactivated (terminated/suspended) |
| Pending | Member registration pending (not applicable for HR updates) |
All HR integration calls are logged with:
Successful updates create audit log entries with: