> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ontime.hosai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync employees with an external HRIS

> Read the OnTime directory, reconcile it against another system by employee code, and understand where writes have to happen.

<Warning>
  **This is a one-way read.** The public API has no write endpoints, so OnTime cannot be the target of an automated sync. You can detect drift; applying it happens elsewhere. See [Applying changes](#applying-changes) below.
</Warning>

## Reading the directory

`GET /api/public/v1/employees` returns the whole directory in one response — there is no pagination.

```json theme={null}
{
  "meta": { "count": 3 },
  "data": [
    {
      "id": "3f1c9b2a-5d84-4e7a-9c11-2b6f8d0a4e73",
      "code": "EMP-0421",
      "name": "Rhea Varma",
      "email": "rhea.varma@example.com",
      "department": "Operations",
      "status": "Active"
    },
    {
      "id": "8a2e4d61-7c93-4b05-8f2d-1e5c9a3b7d40",
      "code": "EMP-0518",
      "name": "Arjun Nair",
      "email": "arjun.nair@example.com",
      "department": "Sales",
      "status": "Probation"
    },
    {
      "id": "c05b7f38-1a92-4e6d-b840-9f3c2e1a5d67",
      "code": "EMP-0733",
      "name": "Meera Iyer",
      "email": null,
      "department": null,
      "status": "Exited"
    }
  ]
}
```

<Note>
  The public employee shape is deliberately minimal: identity, department and status. It does **not** include manager, location, shift, joining date, compensation or contact details.
</Note>

## Choosing a match key

| Field   | Use as key?                                                                                                                                   |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`    | **Yes, once you have it.** A stable UUID. Store it in your system after the first match.                                                      |
| `code`  | **Yes, for the initial match.** Unique within the organization, and usually the field both systems share. Can be changed by an administrator. |
| `email` | **No.** It identifies the *person*, who may be a member of several organizations, and it can be `null`.                                       |
| `name`  | **No.**                                                                                                                                       |

<Steps>
  <Step title="First run: match on code">
    Both systems typically know an employee number. Match on it, then record the OnTime `id` against each of your records.
  </Step>

  <Step title="Subsequent runs: match on the stored id">
    Immune to a code being reassigned or corrected.
  </Step>
</Steps>

## The reconciliation

```typescript theme={null}
const BASE = "https://app.ontime.hosai.app";

type OnTimeEmployee = {
  id: string;
  code: string;
  name: string;
  email: string | null;
  department: string | null;
  status: string;
};

type Drift = {
  kind: "missing_in_ontime" | "missing_in_hris" | "field_mismatch";
  code: string;
  detail?: string;
};

async function reconcile(hris: Map<string, { name: string; department: string | null; active: boolean }>) {
  const res = await fetch(`${BASE}/api/public/v1/employees`, {
    headers: { Authorization: `Bearer ${process.env.ONTIME_API_KEY}` },
  });
  if (!res.ok) throw new Error(`OnTime ${res.status}: ${await res.text()}`);

  const { data } = (await res.json()) as { data: OnTimeEmployee[] };
  const ontime = new Map(data.map((e) => [e.code, e]));
  const drift: Drift[] = [];

  for (const [code, source] of hris) {
    const target = ontime.get(code);
    if (!target) {
      drift.push({ kind: "missing_in_ontime", code });
      continue;
    }
    if (source.name !== target.name) {
      drift.push({ kind: "field_mismatch", code, detail: `name: "${target.name}" → "${source.name}"` });
    }
    if (source.department !== target.department) {
      drift.push({ kind: "field_mismatch", code, detail: `department: "${target.department}" → "${source.department}"` });
    }
    // "Exited" in OnTime vs active in the HRIS is the one that matters most —
    // an exited employee who is still active elsewhere is an access problem.
    const targetActive = target.status !== "Exited";
    if (source.active !== targetActive) {
      drift.push({ kind: "field_mismatch", code, detail: `status: OnTime ${target.status}, HRIS ${source.active ? "active" : "inactive"}` });
    }
  }

  for (const [code] of ontime) {
    if (!hris.has(code)) drift.push({ kind: "missing_in_hris", code });
  }

  return drift;
}
```

## Applying changes

Because the public API is read-only, you have three options for acting on the drift:

<CardGroup cols={3}>
  <Card title="Report it" icon="file-text">
    Emit a drift report and let an administrator apply it in the console. Simple, auditable, and the right choice when drift is rare.
  </Card>

  <Card title="CSV import" icon="upload">
    For bulk onboarding, produce a CSV and use the directory's bulk import. Remember imported employees need a password set before they can sign in.
  </Card>

  <Card title="MCP" icon="bot">
    The MCP server **does** expose employee create and update tools, acting as a bound employee with that person's permissions. This is the only programmatic write path. See the **AI & MCP** tab.
  </Card>
</CardGroup>

<Warning>
  **Do not attempt to drive the internal `/api/employees` routes.** They authenticate with a session cookie, are not versioned, and may change without notice.
</Warning>

## Status values

| Status       | Meaning                                             |
| ------------ | --------------------------------------------------- |
| `Onboarding` | Record created, not yet started.                    |
| `Probation`  | Started, within probation.                          |
| `Active`     | The normal state.                                   |
| `Exited`     | Left. Retained for history, payroll and compliance. |

<Note>
  **`Exited` employees remain in the directory** and continue to be returned by this endpoint — their attendance, payslips and documents are part of the statutory record. Filter them out yourself if your target system should not see them.
</Note>

## Scheduling

There is no employee-change webhook, so this is a **poll**. Daily is usually right; the directory does not change fast enough to justify more, and each run is a single request.
