> ## Documentation Index
> Fetch the complete documentation index at: https://platform.doodocs.kz/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync all employees

> Initial export, incremental polling, and reconciling deletions

The goal is to keep a local copy of the Keruen LLP employee directory up to date. The
scheme is simple: export everyone once, then fetch only changes, and reconcile deletions
with a periodic full re-read.

<Note>
  Everything below requires the `employees:read` scope. The key sees only the employees
  and fields available to the owner's permission profile — effective access is the
  intersection of the key's scopes and the owner's permissions.
</Note>

<Steps>
  <Step title="Initial export">
    Walk the list page by page with the maximum `limit=1000`, following
    `next_page_token` until it comes back empty. In `expand`, list the related entities
    you need up front — that way you avoid hitting the graph with separate requests.

    ```bash theme={null}
    curl -H "X-API-Key: ddp_YOUR_KEY" \
      "https://app.doodocs.kz/api/developer/v1/employees?limit=1000&expand=person&expand=department&expand=job_title"
    ```

    ```json Response theme={null}
    {
      "employees": [
        {
          "id": "3f2a9c7e-…",
          "employee_number": "R-0042",
          "status": "EMPLOYEE_STATUS_ACTIVE",
          "department_id": "d1…",
          "job_title_id": "j5…",
          "work_email": "kulyash@keruen.kz",
          "update_time": "2026-02-10T09:00:00Z",
          "redacted_fields": [],
          "person": { "display_name": "Kulyash Baiseitova" }
        }
      ],
      "next_page_token": "CgYIyAE.7f3a"
    }
    ```

    Pass the returned `next_page_token` into the next request — and so on until the token
    is empty. Save each record by `id` (upsert).

    ```python theme={null}
    import requests

    BASE = "https://app.doodocs.kz/api/developer/v1"
    HEADERS = {"X-API-Key": "ddp_YOUR_KEY"}
    params = {"limit": 1000, "expand": ["person", "department", "job_title"]}

    token = None
    while True:
        page = {**params, **({"page_token": token} if token else {})}
        data = requests.get(f"{BASE}/employees", headers=HEADERS, params=page).json()
        for emp in data["employees"]:
            upsert(emp)                       # save by emp["id"]
        token = data["next_page_token"]
        if not token:
            break
    ```

    <Warning>
      A token is bound to the filter set it was issued for. Passing it into a request with
      different `status`, `department_id`, `updated_since`, or `limit` returns
      `INVALID_ARGUMENT`. When you change filters, start the walk again without a token.
    </Warning>
  </Step>

  <Step title="Incremental polling">
    Remember the moment you started the sync (UTC) and store it as a checkpoint. Next
    time, request only what changed since the checkpoint via `updated_since` (RFC 3339),
    then advance the checkpoint to the time of the new run.

    ```bash theme={null}
    curl -H "X-API-Key: ddp_YOUR_KEY" \
      "https://app.doodocs.kz/api/developer/v1/employees?updated_since=2026-02-10T09:00:00Z&limit=1000"
    ```

    Paging is the same as for a full export: follow `next_page_token` until the token is
    empty. Take the checkpoint from the *start* of the run, not the end, so you do not
    lose changes that happened during the walk.

    What `updated_since` covers and what it does not:

    | Change                                                                             | Moves `updated_since`? | How to catch               |
    | ---------------------------------------------------------------------------------- | :--------------------: | -------------------------- |
    | Fields of the record itself (`status`, `department_id`, `work_email`, `end_date`…) |           yes          | `updated_since`            |
    | Rename of the related person (name, IIN, contacts)                                 |           no           | webhook `employee.changed` |
    | Rename of a department or job title                                                |           no           | webhook `employee.changed` |

    <Note>
      `updated_since` reflects changes to the employee record itself. Renaming related
      entities (person, department, job title) does **not** land in this field — for those,
      subscribe to [webhooks](/en/api-reference/webhooks): a person rename arrives as
      `employee.changed` for each affected employee.

      Encode the timezone offset in the query as `%2B05:00`, or send the time in `Z`
      (UTC): a "plus" in a URL is otherwise interpreted as a space.
    </Note>
  </Step>

  <Step title="Reconcile deletions">
    Incremental polling does not report deletions — a deleted employee simply stops
    appearing in lists, and `GET /employees/{id}` for it returns `404`
    `EMPLOYEE_NOT_FOUND`. So periodically (for example, once a day) do a full export and
    subtract it from your store: anything not in the fresh full list has been deleted on
    the Doodocs side — mark it in your store.

    <Tip>
      Do not confuse termination with deletion. A terminated employee stays in the lists
      with status `EMPLOYEE_STATUS_TERMINATED` and a filled-in `end_date` — it is visible
      through normal polling. Deletion, on the other hand, removes the record from the
      output entirely, and you can only catch it with a full re-read or the
      `employee.deleted` webhook.
    </Tip>
  </Step>
</Steps>

## The resulting cycle

* **Once:** a full paged export → you fill the store.
* **Often (minutes):** `updated_since` from the checkpoint → upsert the changed records.
* **Rarely (once a day):** a full re-read → reconcile and mark deletions.
* **Instantly:** the `employee.changed` / `employee.deleted` webhooks cover what polling
  by `updated_since` does not see (renames of related entities, deletions).

## Next

<Columns cols={2}>
  <Card title="React to hire and termination" icon="user-plus" href="/en/guides/recipes/react-to-hire">
    Replace polling with webhooks and handle events in real time.
  </Card>

  <Card title="Webhooks" icon="bolt" href="/en/api-reference/webhooks">
    Subscribing, the thin payload, and signature verification.
  </Card>
</Columns>
