Skip to main content

sayhii Organization API

The sayhii Organization API lets your own systems push employee roster and department-hierarchy updates into sayhii programmatically—so your people and org structure stay in sync without manual uploads or spreadsheet wrangling.

Base URL: https://api.sayhii.io Authorization Header: Authorization: Bearer <keyId>.<secret> Roster Source Header: X-Sayhii-Roster-Source: <source-key> (required on every call) Org Context: The API key determines your organization; the roster-source header determines which of your organization's declared push sources the call acts as.

Available by Request

This API isn't self-serve yet. Contact your sayhii account team (support@sayhii.io) to have it enabled for your organization, then generate a key from API Keys.

tip

Only org admins with the API-key permission can create keys. Share keys only with trusted developers or services, and scope them appropriately. Keys are managed in the sayhii Portal—for more information see API Keys.

Push Sources​

Every roster or department call acts on behalf of one of your organization's declared push sources—a named identifier for a system that's allowed to push data (e.g. your HRIS, or a nightly export job). A key is never tied to a single source: one key can act as any source it's scoped for, chosen per-request via the X-Sayhii-Roster-Source header. Sources are declared and managed from the Integrations page (not the API Keys page); a call naming a source that hasn't been declared for your org is rejected outright—sources are never auto-created.

Quick Start​

Check your organization's current roster status in a few lines of code:

curl -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
https://api.sayhii.io/v1/roster/status

Roster​

The Roster endpoints let you push employee records into sayhii and read back what's currently on file.

POST /v1/roster/sync​

Push a batch of employee records in upsert mode—perfect for syncing your HRIS or directory. Requires the roster:write scope.

  • Body: { "employees": RosterRecordInput[] }
  • Batch Size: Maximum 500 employees per call. Larger snapshots must be split across multiple calls.
  • Matching: Records are matched primarily by employeeId within your push source's own namespace. If employeeId doesn't match an existing record, sayhii falls back to matching by email—but only against a record already owned by this same push source. A row is rejected as a conflict (not written) if the email fallback would match a record owned by a different push source, or matches more than one existing record ambiguously.
  • Response: Always 200, with a per-row result for every input record (see Outcomes below)—there is no partial-success status code.

Sync Outcomes​

Each row in the response's results array reports one of four outcomes:

OutcomeMeaning
appliedThe record was fully written.
pendingThe record was written, but its managerEmployeeId and/or departmentCode reference hasn't resolved yet (see pendingFields). It resolves automatically on a future sync once the reference becomes known—no action needed.
conflictNothing was written. The record's email matched an existing user owned by a different push source, or matched more than one existing user ambiguously.
errorThe row was rejected outright—e.g. missing employeeId/email, an invalid status, or a duplicate employeeId elsewhere in the same batch.

A request is only rejected wholesale (4xx, no rows processed) for envelope-level problems—an invalid JSON body, a missing employees array, or a batch over 500 entries. Anything that parses as a valid array of rows proceeds to per-row processing, even if every row ends up error.

Example​

curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
-H "Content-Type: application/json" \
-d '{
"employees": [{
"employeeId": "123",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"title": "Marketing Manager",
"managerEmployeeId": "100",
"departmentCode": "MKT",
"status": "active"
}]
}' \
https://api.sayhii.io/v1/roster/sync

Example Response​

{
"syncId": "b1f2c3d4-...",
"receivedAt": "2026-09-08T15:04:05.000Z",
"results": [
{
"employeeId": "123",
"outcome": "pending",
"userId": "5c9e...",
"pendingFields": ["manager"],
"detail": "applied with manager unresolved, pending a future sync"
}
],
"summary": { "total": 1, "applied": 0, "error": 0, "conflict": 0, "pending": 1 }
}

GET /v1/roster/users​

Get a paginated list of employee records already pushed for your push source. Requires the roster:read scope.

Query Parameters​

  • cursor (string, optional): Opaque pagination cursor returned by a previous call.
  • limit (number, optional): Maximum number of records to return (default 50, max 500).
  • pending (boolean, optional): When true, only returns records with an unresolved pendingFields entry.

Response​

  • results (RosterUserResponse[], required): Array of employee records.
  • cursor (string, optional): Pass as cursor on the next call to fetch the next page. Omitted when there are no more results.
curl -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
"https://api.sayhii.io/v1/roster/users?limit=100"

GET /v1/roster/users/{employeeId}​

Look up a single employee record by employeeId, within your push source's namespace. Requires the roster:read scope. Returns 404 if the employeeId doesn't exist, or exists only under a different push source.

curl -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
https://api.sayhii.io/v1/roster/users/123

Departments​

The Department endpoints let you push and read your organization's department hierarchy, and let a departmentCode referenced from a roster sync resolve correctly. This endpoint only maintains the department tree and its code mappings—it never resolves or writes an individual employee's departmentCode (that happens as part of roster sync).

POST /v1/departments/sync​

Create or update department nodes in upsert mode. Requires the department:write scope.

  • Body: { "departments": [{ "code", "name", "parentCode" }] }
  • Batch Size: Maximum 500 nodes per call.
  • Each node is matched on code within your push source's namespace.
  • parentCode may reference another node in the same payload (forward references are fine) or an already-mapped code from a previous sync. Omit parentCode entirely to leave a node's current parent untouched; pass null explicitly to make it a root node.
  • This endpoint only ever creates or updates nodes—it never deletes tree structure. A node's absence from a payload is not a delete.
  • Response: Always 200, with a per-node result (applied or error—there's no pending/conflict state for departments).

Example​

curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
-H "Content-Type: application/json" \
-d '{
"departments": [
{ "code": "ENG", "name": "Engineering" },
{ "code": "ENG-PLAT", "name": "Platform", "parentCode": "ENG" }
]
}' \
https://api.sayhii.io/v1/departments/sync

GET /v1/departments/nodes​

Get a paginated list of department nodes mapped for your push source. Requires the department:read scope. Same cursor/limit query parameters as GET /v1/roster/users.

GET /v1/departments/nodes/{code}​

Look up a single department node by code. Requires the department:read scope. Returns 404 if the code doesn't exist, or exists only under a different push source.

GET /v1/departments/pending-codes​

Read-only list of department codes referenced by a roster sync (via departmentCode) that haven't been mapped to a real node yet—the same codes that produce a pending roster-sync outcome. Requires the department:read scope. Not paginated. An admin resolves these from sayhii's department hierarchy management screens, not via this API.

curl -H "Authorization: Bearer $API_KEY" \
-H "X-Sayhii-Roster-Source: $ROSTER_SOURCE" \
https://api.sayhii.io/v1/departments/pending-codes

Status​

GET /v1/roster/status​

A snapshot summary of your push source's roster and/or department state. Requires roster:read and/or department:read—each section of the response is included only if your key holds the matching scope, so a read-only integration can call this with whichever single scope it has.

{
"users": {
"total": 812,
"active": 790,
"terminated": 22,
"pendingDepartment": 3,
"pendingManager": 1
},
"departments": {
"total": 41,
"pendingCodes": 2
}
}

Schemas​

RosterRecordInput​

FieldTypeRequiredNotes
employeeIdstring✅Stable identifier within your push source's namespace.
emailstring✅Matching fallback; see Matching.
status"active" | "terminated"✅
firstNamestring
lastNamestring
titlestring
managerEmployeeIdstring | nullThis same push source's employeeId namespace, not email. null clears the link; omit to leave it untouched.
departmentCodestring | nullConnection-scoped, see Departments. null clears it; omit to leave it untouched.
terminatedAtstring | nullISO-8601 date/time.

RosterUserResponse​

FieldTypeNotes
employeeIdstring
emailstring
firstNamestring
lastNamestring
status"active" | "terminated"
terminatedAtstring | null
departmentCodestring | null
managerEmployeeIdstring | null
pendingFields("department" | "manager")[]Always present, [] when nothing is pending.

DepartmentSyncNodeInput​

FieldTypeRequiredNotes
codestring✅Unique within your push source's namespace.
namestring✅
parentCodestring | nullOmit to leave the current parent untouched; null for a root node.

DepartmentNodeResponse​

FieldTypeNotes
codestring
namestring
parentCodestring | nullnull if this node is a root, or if the parent has no code mapped for your push source.
departmentIdstringsayhii's internal department identifier.
createdAtstringISO-8601.

Errors​

RequestError​

FieldTypeDescription
messagestringHuman-readable description.

Common Status Codes​

CodeMeaning
200Success (sync endpoints always return 200; check per-row outcome for partial results)
400Bad request—malformed body, batch over 500 entries, or a missing/unrecognized X-Sayhii-Roster-Source header
401Unauthorized—missing/malformed Authorization header, or an unknown, revoked, or expired key
403Your key doesn't hold the scope this endpoint requires
404Not found (read endpoints only)
429Rate limit exceeded—see Rate Limits

Rate Limits​

Each key has a rate class set at creation:

Rate ClassLimit
standard (default)60 requests/minute
burst300 requests/minute

A 429 response means you've exceeded your key's limit for the current one-minute window. Use exponential backoff and retry.


Best Practices​

  • Batch Size: Keep sync requests at or under 500 entries per call.
  • Idempotency: Re-sending the same snapshot is safe—both sync endpoints are full upsert, not append.
  • Handle pending: A pending roster-sync outcome isn't an error—resend the same employee (or the referenced department/manager) in a later sync once it's known, or check GET /v1/departments/pending-codes to see which department codes still need mapping.
  • Retries: Use exponential backoff for 429 and 5xx responses.
  • Security: Provide keys only to trusted parties. Keys are org-scoped and can act as any push source they're issued scope for—prefer one key per integration.

Changelog & Support​

This is a versioned API (/v1/...); a breaking change ships as a new version (/v2/...) alongside the existing one rather than changing this contract in place.

For help or questions: support@sayhii.io