Documentation Get help

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.

RecipeKey permissions
Sync people from an HR systemView, Create and Update Employees
Record a salary changeView and Create Employees, View Detailed Financials
Fill a vacancyCreate Employees
Move an employee between teamsView, Create and Update Employees
Add a contractorCreate Contractors
Set up a project with team allocationsCreate 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 managerId returns 404. Create everyone in a first pass without managerId, then set managers in a second pass.
  • Job roles. jobRole matches on externalId, then title, and creates the role from title if neither matches. externalId alone with no match returns 404. jobRoleId wins if both are sent.
  • Geographies and work types have no REST endpoint. Copy geographyId and workTypeId from an employee that already has them.
  • Uniqueness. externalId and email are unique within your organisation; a clash returns 400.
  • 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 effectiveDate first: 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, workTypeId and geographyId default 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 status becomes filled and filledByLiveEmployeeId is 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, monthly or annually. contractorType is 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.