Syncing users from your HR system

Keep Spendesk members in line with your HR system: invite new hires, update cost centers, managers and roles, offboard leavers, and monitor the sync.

Your HR system knows first when someone joins, moves team, changes manager or leaves. Through the API, an integration can bring each of these events into Spendesk, so finance does not re-type them:

In your HR systemIn SpendeskEndpoint
A cost center (department, team, project) is created or renamedThe cost center is created or renamed, with its ownerCreate / Update cost centers
Someone is hiredThe member is created and invited, with role, cost center, manager and spending policyCreate users
Someone changes nameThe member's first and last nameUpdate a user
Someone moves to another cost centerThe member's default cost centerSet the cost center
Someone changes managerThe member's managerSet the manager
Someone becomes, or stops being, a finance controllerThe member's role: requester or controllerSet the role
Someone leavesThe member is deactivated in that entityDeactivate a user
%%{init: {"themeVariables": {"fontSize": "18px"}, "flowchart": {"nodeSpacing": 30, "rankSpacing": 45, "wrappingWidth": 400}}}%%
flowchart TB
  HR["<b>Your HR system</b><br/>hires, moves, leavers"] --> INT["<b>Your integration</b><br/>key created by an active Admin"]
  INT -->|Spendesk API| SP
  SP["<b>Spendesk</b><br/>members and invitations<br/>cost centers · managers · roles"]
  SP -.-> LOG["<b>API audit log</b><br/>Settings → Integrations"]
  SP ~~~ KEEP["<b>Kept in Spendesk</b><br/>Admins, Account Owners, Travel Managers<br/>email changes<br/>leavers with a pending transfer"]
  style SP fill:#7136ED,stroke:#5B3FD9,color:#ffffff
  style INT fill:#ECE8FD,stroke:#CFC4FA,color:#1B1740
  style HR fill:#ffffff,stroke:#E2E8F0,color:#191B1B
  style LOG fill:#DDF4EC,stroke:#B7E6D5,color:#0B7A5A
  style KEEP fill:#ffffff,stroke:#D9801F,stroke-dasharray:4 4,color:#B5660F
📘

Check the native HR integrations first

Spendesk connects natively to Personio, BambooHR, HiBob, Workday, Deel, PayFit and Lucca, on Scale and Premium plans — see Spendesk's native HR tools integrations. If your HR system is one of them, the native integration does all of the above without code. Build with the API when your HR system is not listed, or when you need rules the native integration does not offer.

The two cannot run on the same entity. When an entity's members are managed by a native HR integration, every member write on this page is refused with a 403 for that entity ("This company is managed by an HRIS; users must be managed from the HRIS, not the API."). If Spendesk cannot check this at that moment, the call answers 503: retry later.

📘

Experimental

The /v2/users endpoints are experimental: they may still change. Request access from your CSM or at [email protected] — see Getting access.

Before you start

Scopes

To…Scope
Read membersuser:read
Create, rename, move, offboard membersexperimental:user:manage
Change a member's roleexperimental:user:role:manage — not included in the scope above; ask for it on its own
Read cost centerscost-center:read
Create, rename, delete cost centerscost-center:manage

Who the integration acts as. Every member write — create, rename, cost center, manager, role, deactivate — runs as a person, who must be an active Admin or Account Owner of the entity (not suspended, not still invited); otherwise the call answers 403. With an API key, that person is whoever created the key; with OAuth, the person who approved the connection. Create the integration's key from an Admin or Account Owner account that will stay active — if that person leaves or loses the role, the sync stops — and, with an organisation-level key, make sure they hold that role in every entity you sync.

Several entities. With an organisation-level key, send the X-Company-Id header on every call to say which entity you act on — see Organisation-level access. Deactivation, cost center and manager are per entity. A member's name is shared by every entity of the organisation: renaming them in one renames them everywhere.

Features to switch on in Spendesk. Cost centers, reporting managers and spending policies each have to be enabled for the entity. When one is off, the matching field is refused: costCentersDisabled or approvalPoliciesDisabled at creation, a 403 on update ("Cost centers are not activated for this company.", "Reporting managers are not activated for this company.").

How to map your HR data

Spendesk matches people by work email. Keep it as the key between the two systems, and store the Spendesk user id on your side once you know it.

HR fieldSpendeskAt creationAfterwards
Work emailemail — the identity of the member, compared without caseRequired; must not belong to any Spendesk user yet, in this organisation or anotherCannot be changed through the API — change it in Spendesk
First name, last namefirstName, lastNameRequiredUpdate a user
Department, team or projectDefault cost centercostCenter: the cost center's nameSet the cost center: its id, or null to clear
Managermanagermanager: the manager's emailSet the manager: managerId or managerEmail, or null to remove
Finance roleroles: requester or controllerrole, default requesterSet the role
Approval rulesSpending policyspendingPolicy: the policy's nameNot through the API — change it in Spendesk
Teamsteams—Read only (include=teams)
Employment statusstatus: pending (invited), active, archivedCreated pendingDeactivate a user → archived
🚧

The API only manages requesters and controllers

A member can be updated, have their role changed or be deactivated through the API only if all their roles in the entity are requester and/or controller. Anyone holding another role — Admin, Account Owner, Travel Manager… — is refused with a 403, for offboarding too. A rename is also refused when the member holds such a role in any entity of the organisation, since the name is shared. Manage these few people in Spendesk, and route them to a person in your sync.

1. Sync cost centers first

New hires and moves point to cost centers, so create them before the people. Get cost centers lists them; match on the name you use in your HR system.

curl https://public-api.spendesk.com/v1/cost-centers \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Marketing France", "owner": "OWNER_USER_ID", "coOwnerIds": [] }'

Update cost centers renames one or changes its owner and co-owners; Delete cost centers removes one. The owner and co-owners are Spendesk user ids — sync your managers first if they own cost centers. An archived cost center can no longer be given to a member.

2. Read the members Spendesk already has

Build the email → id map before writing anything. Get company users lists the members of an entity, with what you need to compare:

curl -G https://public-api.spendesk.com/v2/companies/COMPANY_ID/users \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  --data-urlencode "status=pending" \
  --data-urlencode "include=manager" \
  --data-urlencode "include=costCenter"
🚧

Invited members are only listed with status=pending

A member you just created stays pending until they accept the invitation. Until then, the default list does not return them, and reading them by id answers 404. Read the list once per status — active, pending and archived — to see everyone.

Reads are cached for up to 20 minutes: changes made through the API are returned straight away, changes made in the Spendesk app can take up to 20 minutes to appear.

3. Invite new hires

Create users takes an array of 1 to 50 people, creates them one after the other and sends each one an invitation e-mail straight away — call it on the hire's start date, not when the contract is signed.

curl https://public-api.spendesk.com/v2/users \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    { "email": "[email protected]", "firstName": "Ana", "lastName": "Silva",
      "role": "requester", "costCenter": "Marketing France",
      "manager": "[email protected]", "spendingPolicy": "Standard" },
    { "email": "[email protected]", "firstName": "Tom", "lastName": "Weber",
      "role": "controller", "costCenter": "Finance" }
  ]'

The answer is a 201 with one result per person, in the order you sent them, each failure carrying the email it concerns. One failure does not stop the others:

{
  "items": [
    { "outcome": "created", "user": { "id": "USER_ID", "email": "[email protected]", "status": "pending" } },
    { "outcome": "error", "reason": "invalidCostCenter", "details": { "email": "[email protected]" } }
  ]
}
reasonWhat to do
emailUnavailableThe email already belongs to a Spendesk user — a member of this entity (perhaps created by your previous attempt: read the members again and store their id), or someone in another Spendesk organisation, who cannot be added: ask the employee or Spendesk support
duplicateInBatchThe same email twice in one call: send it once
managerNotFoundThe manager's email is not a member of this entity: create the manager first, then set it with step 4
invalidCostCenter, costCentersDisabledUnknown cost center name, or cost centers are off for the entity
invalidApprovalPolicy, approvalPoliciesDisabledUnknown spending policy name, or spending policies are off
partiallyCreatedThe creation failed half-way and could not be undone: the member may exist without all their settings. Read them back (a retry answers emailUnavailable) and set what is missing with step 4
internalErrorThe member was not created: retry
seatLimitReached, companyChurnedThe entity changed during the call — no seat left, or churned: contact the Spendesk admin

Retrying is safe. Creating never updates an existing user: people created by an earlier attempt come back as emailUnavailable, without their id — read them back from the list. The teams field is refused: teams are not set through the API. The whole call is refused, and nobody is created, when the entity has no seat left (409), or when it is churned or managed by a native HR integration (403).

Create managers before their reports, or leave manager out at creation and set it in step 4 once everyone exists.

4. Keep each member up to date

Compare what your HR system says with what you read in step 2, and send only what changed.

Name — Update a user:

curl -X PATCH https://public-api.spendesk.com/v2/users/USER_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "lastName": "Silva Martins" }'

Cost center — Set the cost center, by id; null clears it:

curl -X PUT https://public-api.spendesk.com/v2/users/USER_ID/cost-center \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "costCenterId": "COST_CENTER_ID" }'

Manager — Set the manager, with exactly one of managerId or managerEmail; null removes it. A member has one manager: a new one replaces the previous one.

curl -X PUT https://public-api.spendesk.com/v2/users/USER_ID/manager \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "managerEmail": "[email protected]" }'

Sending the same value again changes nothing, so a daily full comparison is harmless. A 409 on the manager means someone changed it at the same moment: read the member again before retrying. A 422 means the value is not valid in this entity — an unknown or archived cost center ("Cost center not found in this company."), a manager who is not an active member ("The manager must be an existing member of this company."), or a member set as their own manager. A 403 means the feature is switched off, or the member holds a role the API does not manage.

5. Change roles

Set the role makes a member a requester or a controller in the entity: they get that role and lose the other one. Send the roles you last read in currentRoles — the change only applies if they are still the member's roles:

curl -X PUT https://public-api.spendesk.com/v2/users/USER_ID/role \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "role": "controller", "currentRoles": ["requester"] }'

A 409 means the roles changed since you read them: nothing was changed; read the member again and decide. The token can never change its own role (403).

6. Offboard leavers

Deactivate a user removes the member from the entity — membership, roles and delegations — and their cards and subscriptions are handled by Spendesk. They are fully removed only if this was their last entity. Run it on the employee's last day.

curl -X DELETE https://public-api.spendesk.com/v2/users/USER_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

The answer is 200 with "outcome": "deactivated", and the member is then listed with status=archived. Deactivating someone already archived answers 200 again, so a retry is harmless. Refusals to send to a person rather than retry:

AnswerWhy
403The member holds a role other than requester or controller — Admin, Account Owner, organisation owner, Travel Manager…
422 "This user has pending transfers and cannot be deactivated yet."Reimbursements or transfers are still on their way: finance clears them, then deactivate
422 "User cannot be deactivated: …"Another blocking reason, given in the message

Monitor the sync

There is no webhook for member changes: the integration's own run is the monitor.

  • Log every result. Keep the per-person outcome and reason of each creation, and every refused update with its message, and send the ones that need a person (emailUnavailable for someone new, managerNotFound, seatLimitReached, a 403 or 422 on deactivation) to whoever owns the HR process.
  • Watch the key's owner. A 403 on every write usually means the person who created the key is no longer an active Admin or Account Owner — or the entity has since been connected to a native HR integration.
  • Reconcile on a schedule. Once a day, read the three statuses and compare with your HR system: people missing, people still active after their leaving date, cost centers or managers that differ. Run deltas more often if you need to.
  • Check the API audit log. Admins and Account Owners see every call made with the integration's key — operation, status code, member concerned — in Settings → Integrations → API audit log: see See what your API keys and integrations did.
  • Respect the limits. On a 429, wait for the time given in Retry-After before retrying — see Rate limiting and Error Handling. Send people by batches of up to 50 rather than one call each.

What the API does not do

  • Change a member's email, or update or deactivate members who hold a role other than requester or controller (Admin, Account Owner, Travel Manager…) — do it in Spendesk.
  • Grant or remove the Admin and Account Owner roles.
  • Change a spending policy after creation, or teams.
  • Delay the invitation — it is sent when the member is created.
  • Notify you of changes made in Spendesk — read the members on a schedule.

Going further