Skip to content
PortalBase

API

Sync people from your HR system

For the program your IT team writes. It does what Setup's Sync from a file does, on the schedule you set.

Your HR or payroll system already knows who works where. People sync keeps PortalBase the same. It adds new hires, changes names, emails and places, and can turn off who left.

Over the API, your program sends the list on its own schedule. It's the same sync as Setup, under Sync from a file, with the same mapping. Each change goes in your company's audit log as its own entry, under the key's service account.

1. Before you start

  • An admin makes a service account in Setup, under Connections, with a role that has people.manage for the whole company. Admin at the top of your company has it.
  • Then makes a key for it that can make changes. A key that only looks things up is refused, and so is an AI tool.
  • Map your columns, job titles and location codes once in Setup, under Sync from a file. The API uses that mapping. The template there shows every column.

2. Send rows as JSON

POST to https://portalbaseai.com/api/v1/people/sync with your key as Authorization: Bearer pbk_... One row per person.

curl https://portalbaseai.com/api/v1/people/sync \
  -H "Authorization: Bearer $PORTALBASE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3b9f2c1e-7d4a-4e8b-9c6f-2a1d5e8b7c40" \
  -d '{
    "preview": true,
    "rows": [
      {
        "employee_id": "E-104",
        "given_name": "Rosa",
        "family_name": "Diaz",
        "email": "rosa.diaz@example.com",
        "hire_date": "2026-03-02",
        "places": [{ "org_unit": "NY", "role": "Worker" }],
        "status": "active",
        "fields": { "shirt_size": "L" }
      }
    ]
  }'
  • employee_id finds each person, then email. given_name is needed for anyone new.
  • places: each place with the role there. A place is its code, name or path in PortalBase, or a location code your mapping knows. A role is its name, or a job title your mapping knows.
  • status: active or terminated, or a word your mapping knows. Left out, it doesn't change.
  • language is a code like en or es. hire_date is a date like 2026-03-02.
  • fields: your company fields on people, by key. A field left out keeps its value. A sync never blanks one.
  • A value left out, or empty, is never cleared. A sync only fills in and changes.

3. Or send the file as CSV

Send the same file you'd load in Setup, with Content-Type: text/csv. The options go in the address.

curl "https://portalbaseai.com/api/v1/people/sync?preview=true" \
  -H "Authorization: Bearer $PORTALBASE_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @hr-export.csv

Each column is read by its name, through your saved mapping. A column that looks like a PIN or a password is refused. A sync never sets those.

4. Check first

preview: true works it all out and writes nothing. The answer has the counts (added, changed, unchanged, turned_off, turned_on, refused) and a line for each row: what it would do, which fields would change, or why it can't. Send it again without preview to apply it.

In JSON, preview, turn_off_missing and max_turn_off go in the body beside rows. With CSV, they go in the address.

5. Turning off who left

  • Nobody is turned off unless you send turn_off_missing: true.
  • With it, people in the places your list covers who aren't in the list are turned off. The places it covers are the ones your rows name, and every place under them.
  • Say max_turn_off too: the most it may turn off. If it would turn off more, nobody is changed and the answer says how many.
  • Someone who also works somewhere your list doesn't cover stays on. So does anyone whose row was refused.
  • Someone turned off by hand stays off. A sync turns back on only the people it turned off.
  • Service accounts, the key's own account and people your directory keeps in step are never changed.
curl "https://portalbaseai.com/api/v1/people/sync?turn_off_missing=true&max_turn_off=10" \
  -H "Authorization: Bearer $PORTALBASE_KEY" \
  -H "Content-Type: text/csv" \
  -H "Idempotency-Key: 8e2d4b6a-1c3f-4a5e-b7d9-0f2e4c6a8b13" \
  --data-binary @hr-export.csv

6. Retries

Send an Idempotency-Key header, a UUID you make for each sync. If the call times out, send it again with the same key. You get the first run's answer and nothing changes twice. The same key with a different list is refused with conflict.

7. The answer

A sync answers with its run_id, the counts and a line for each row. row is the line it would be in a file, so the first JSON row is row 2. A row gives the person's id, not their name or email.

{
  "ok": true,
  "result": {
    "run_id": "5c0e7a92-3f1b-4d8e-a6c2-9b4f1e7d3a58",
    "counts": { "added": 1, "changed": 3, "unchanged": 208, "turned_off": 0, "turned_on": 0, "refused": 1 },
    "rows": [
      { "row": 2, "person_id": "1f8a3c5e-7b9d-4e2f-8a6c-4d2b0e9f7a31", "action": "changed", "fields": ["family_name"], "reason": null },
      { "row": 3, "person_id": null, "action": "refused", "fields": [], "reason": "No place matches \"4412\". Map it in Setup." }
    ]
  }
}

GET https://portalbaseai.com/api/v1/people/sync/runs/{run_id} reads a run later. Runs are kept a year and their rows 90 days. Admins see every run in Setup, under Sync from a file.

8. Limits

  • Up to 2,000 rows, and 1 MB, in one call. Split a bigger list by place.
  • Each key may make 120 requests a minute, the same as the rest of the API.
  • A refusal answers ok: false with error.code and a sentence saying what to fix. Every path is listed at https://portalbaseai.com/api/v1/openapi.json.