Recipes
Request sequences for common integration jobs. Every recipe works against the endpoints and fields in this reference.
export BASE="https://{tenant}.flowstate.inc/api/v1/org/{orgId}"
export KEY="private_…"
Responses are trimmed to the fields that matter. Money fields (salary, bonus, rate, estimatedCost) come back as strings, such as "110000"; fte is a number.
| Recipe | Key permissions |
|---|---|
| Sync people from an HR system | View, Create and Update Employees |
| Record a salary change | View and Create Employees, View Detailed Financials |
| Fill a vacancy | Create Employees |
| Move an employee between teams | View, Create and Update Employees |
| Add a contractor | Create Contractors |
| Set up a project with team allocations | Create Projects, Create Teams |
Sync people from an HR system
An upsert loop keyed on your HR system’s worker ID, stored as externalId. To choose between spreadsheet import, a custom integration and the REST API, see Get your people data into Flowstate.
For each worker:
1. Look up by externalId.
curl "$BASE/employees/HR-5089" -H "Authorization: Bearer $KEY"
200 → step 3. 404 NOT_FOUND (Employee not found: HR-5089) → step 2.
2. Create. The employee, first salary and team assignment are written together, or not at all.
curl -X POST "$BASE/employees" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "HR-5089",
"firstName": "Aisha",
"lastName": "Patel",
"email": "aisha.patel@example.com",
"startDate": "2026-03-01",
"jobRole": { "externalId": "JR-SWE-3", "title": "Senior Software Engineer" },
"geographyId": "cuay2eqyymqe6y2ui26q6ieia",
"salary": {
"effectiveDate": "2026-03-01",
"salary": 92000,
"currencyCode": "GBP",
"reason": "Starting salary"
},
"teamAssignment": { "teamId": "TEAM-PLATFORM", "fte": 1, "startDate": "2026-03-01" }
}'
201:
{
"data": {
"id": "cmmy2ye2iyuq66uq2iay2miyi",
"externalId": "HR-5089",
"firstName": "Aisha",
"lastName": "Patel",
"startDate": "2026-03-01T00:00:00.000Z",
"jobRoleId": "caa2mi6uiy66ma22mmeaeu2e6",
"salary": {
"id": "cumuaq6eymy3yqymaqq6mia26",
"liveEmployeeId": "cmmy2ye2iyuq66uq2iay2miyi",
"effectiveDate": "2026-03-01T00:00:00.000Z",
"salary": "92000",
"currencyCode": "GBP"
},
"teamAssignment": {
"id": "ca6aqym6mmraqeeqe2im2iqye",
"liveTeamId": "cuuaeia6yuemuim62u62u2ee2",
"fte": 1,
"startDate": "2026-03-01T00:00:00.000Z",
"endDate": null
}
}
}
3. Update changed fields only.
curl -X PATCH "$BASE/employees/HR-5089" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "aisha.patel@newdomain.example", "managerId": "HR-4001" }'
On PATCH, nested salary, teamAssignment and projectAssignment always add a new record. Send them only when the value has changed, not on every run.
4. Leavers. PATCH with endDate (and noticeDate if you have it). DELETE /employees/{id} removes the employee.
Notes:
- Managers. An unresolvable
managerIdreturns404. Create everyone in a first pass withoutmanagerId, then set managers in a second pass. - Job roles.
jobRolematches onexternalId, thentitle, and creates the role fromtitleif neither matches.externalIdalone with no match returns404.jobRoleIdwins if both are sent. - Geographies and work types have no REST endpoint. Copy
geographyIdandworkTypeIdfrom an employee that already has them. - Uniqueness.
externalIdandemailare unique within your organisation; a clash returns400. - Pay and team changes: the next two recipes.
Record a salary change
Adds an adjustment to the employee’s pay history. Earlier adjustments aren’t changed; each applies from its own effectiveDate.
curl -X POST "$BASE/employees/HR-5089/salary-adjustments" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"effectiveDate": "2026-07-01",
"salary": 110000,
"bonus": 15000,
"currencyCode": "GBP",
"reason": "Annual review"
}'
201:
{
"data": {
"id": "cq2umaumyeeqmi6aaaq66a6qa",
"liveEmployeeId": "cmmy2ye2iyuq66uq2iay2miyi",
"effectiveDate": "2026-07-01T00:00:00.000Z",
"salary": "110000",
"bonus": "15000",
"currencyCode": "GBP",
"reason": "Annual review"
}
}
- Correct an entry:
PATCH /employees/{id}/salary-adjustments/{adjustmentId}. - Read the history, newest
effectiveDatefirst:GET /employees/{id}/salary-adjustments.
Fill a vacancy
Turns an open vacancy into a new employee in one transaction.
curl -X POST "$BASE/vacancies/REQ-1187/fill" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Sarah",
"lastName": "Okonkwo",
"email": "sarah.okonkwo@example.com",
"startDate": "2026-06-01",
"salary": 140000,
"currencyCode": "USD"
}'
200:
{
"data": {
"employee": {
"id": "ceeqe6eu22qmmaa26umuyiyi6",
"firstName": "Sarah",
"lastName": "Okonkwo",
"startDate": "2026-06-01T00:00:00.000Z",
"managerId": "cmu6yeaiuaaie6i2qiqvimueq",
"jobRoleId": "caa2mi6uiy66ma22mmeaeu2e6"
},
"contractor": null,
"vacancyId": "c226i2imqq6q6u2aeie26uemu",
"teamAllocationsTransferred": 1,
"projectAllocationsTransferred": 2
}
}
What changes:
- A new employee with a salary adjustment effective on
startDate. managerId,jobRoleId,workTypeIdandgeographyIddefault to the vacancy’s hiring manager, job role, work type and geography unless you send them.- The vacancy’s allocations with no end date move to the employee from
startDate, and end on the vacancy the day before. - The vacancy’s
statusbecomesfilledandfilledByLiveEmployeeIdis set.
A vacancy that’s already filled returns 409 CONFLICT. Always send email; without it a placeholder address is used. To fill with a contractor, send "fillerType": "contractor" with name, rate and rateType; see Vacancies.
Move an employee between teams
End the current team assignment, then start a new one.
1. Find the current team assignment.
curl "$BASE/employees/HR-5089?include=assignments" -H "Authorization: Bearer $KEY"
{
"data": {
"id": "cmmy2ye2iyuq66uq2iay2miyi",
"assignments": [
{
"id": "ca6aqym6mmraqeeqe2im2iqye",
"type": "team",
"employeeId": "cmmy2ye2iyuq66uq2iay2miyi",
"targetId": "cuuaeia6yuemuim62u62u2ee2",
"fte": 1,
"startDate": "2026-03-01T00:00:00.000Z",
"endDate": null
}
]
}
}
Take the entry with "type": "team" and "endDate": null.
2. End it on the last day on the old team.
curl -X PATCH "$BASE/assignments/employees/ca6aqym6mmraqeeqe2im2iqye" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "endDate": "2026-06-30" }'
3. Start the new one on the first day on the new team.
curl -X POST "$BASE/assignments/employees" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "employeeId": "HR-5089", "teamId": "TEAM-PAYMENTS", "fte": 1, "startDate": "2026-07-01" }'
201 returns the new assignment with "type": "team". Send exactly one of teamId or projectId.
A teamAssignment on PATCH /employees/{id} also creates an assignment, but doesn’t end the existing one.
Add a contractor
The contractor, first rate and team assignment are written together, or not at all.
curl -X POST "$BASE/contractors" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "SUP-0917",
"name": "Elena Vasquez",
"email": "elena@design-studio.example",
"contractorType": "individual",
"startDate": "2026-05-01",
"endDate": "2026-10-31",
"managerId": "HR-4001",
"rateAdjustment": {
"effectiveDate": "2026-05-01",
"rateType": "daily",
"rate": 950,
"currencyCode": "EUR",
"reason": "Initial rate"
},
"teamAssignment": {
"teamId": "TEAM-PLATFORM",
"fte": 0.8,
"startDate": "2026-05-01",
"endDate": "2026-10-31"
}
}'
201:
{
"data": {
"id": "cae6qm6q6uaau6ueay2ae26uy",
"externalId": "SUP-0917",
"name": "Elena Vasquez",
"contractorType": "individual",
"startDate": "2026-05-01T00:00:00.000Z",
"endDate": "2026-10-31T00:00:00.000Z",
"rateAdjustment": {
"id": "cquqq6yqmi6e2maaee26imeea",
"liveContractorId": "cae6qm6q6uaau6ueay2ae26uy",
"effectiveDate": "2026-05-01T00:00:00.000Z",
"rateType": "daily",
"rate": "950",
"currencyCode": "EUR"
},
"teamAssignment": {
"id": "cue66euieee6e6qayeaeyquem",
"liveTeamId": "cuuaeia6yuemuim62u62u2ee2",
"fte": 0.8,
"startDate": "2026-05-01T00:00:00.000Z",
"endDate": "2026-10-31T00:00:00.000Z"
}
}
}
rateType:hourly,daily,monthlyorannually.contractorTypeis required free text.- Later rate changes:
POST /contractors/{id}/rate-adjustments.
Set up a project with team allocations
1. Create the project.
curl -X POST "$BASE/projects" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "PAY-MIG",
"name": "Payment gateway migration",
"projectCode": "PAY-MIG",
"startDate": "2026-05-01",
"endDate": "2026-11-30",
"estimatedCost": 380000,
"priority": 1
}'
201:
{
"data": {
"id": "cmeeiiyeqaemqee6ay2ii6y2a",
"externalId": "PAY-MIG",
"name": "Payment gateway migration",
"projectCode": "PAY-MIG",
"startDate": "2026-05-01T00:00:00.000Z",
"endDate": "2026-11-30T00:00:00.000Z",
"estimatedCost": "380000",
"priority": 1
}
}
Lower priority ranks higher. Project create and update take no status field.
2. Allocate each team.
curl -X POST "$BASE/assignments/teams" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"teamId": "TEAM-PAYMENTS",
"projectId": "PAY-MIG",
"fte": 3,
"startDate": "2026-05-01",
"endDate": "2026-11-30",
"role": "Primary",
"costCategory": "CapEx"
}'
201:
{
"data": {
"id": "caa2mi6uiy66ma22mmeaeu2e6",
"teamId": "cuuaeia6yuemuim62u62u2ee2",
"projectId": "cmeeiiyeqaemqee6ay2ii6y2a",
"fte": 3,
"startDate": "2026-05-01T00:00:00.000Z",
"endDate": "2026-11-30T00:00:00.000Z",
"role": "Primary",
"costCategory": "CapEx"
}
}
Repeat for each supporting team. role and costCategory are free-text labels.