Documentation Get help

Webhooks

Flowstate sends an HTTP POST to your endpoint when a record in live data is created, updated or deleted. Each request carries one event, signed with your endpoint’s secret.

What sends events

Sourceinitiator.type
Changes people make in the Flowstate appuser
The REST API requests listed belowapi_key
Merging a scenario: one event for each record the merge changesmerge
Changes a custom integration PULL hook makes to the functional structureuser

REST API requests that send events, with paths relative to /api/v1/org/:orgId:

RequestEvents
POST /employees, PATCH /employees/:idemployee create or update, plus a create event for each team allocation, project allocation and salary adjustment created in the same request
DELETE /employees/:idemployee delete
POST, PATCH and DELETE on /employees/:employeeId/salary-adjustmentssalary_adjustment
POST /contractors, PATCH /contractors/:idcontractor create or update, plus a create event for each team allocation, project allocation and rate adjustment created in the same request
DELETE /contractors/:idcontractor delete
POST, PATCH and DELETE on /contractors/:contractorId/rate-adjustmentscontractor_rate
POST, PATCH and DELETE on /vacanciesvacancy
POST /vacancies/:id/fillvacancy update, and a create event for the employee or contractor it creates. An employee filler also sends salary_adjustment create.
POST, PATCH and DELETE on /teamsteam
POST, PATCH and DELETE on /projectsproject
Writes under /functional-groupsfunctional_group, functional_position, functional_assignment

Not sent:

  • Changes inside a scenario, until the scenario is merged.
  • Changes a custom integration PULL hook makes to employees, contractors, vacancies, projects, teams and assignments.

Manage endpoints in Flowstate

Endpoints are managed in the app. There’s no API for endpoint configuration.

PermissionAllows
View WebhooksOpening the Webhooks page and seeing endpoints
Configure WebhooksAdding, editing, switching off, testing, rotating the secret of, and deleting endpoints

The Webhooks page isn’t in the Settings menu. Open https://{tenant}.flowstate.inc/settings/webhooks, where {tenant} is your organisation’s Flowstate address.

Add an endpoint

  1. Select Add Endpoint.
  2. In Name, enter up to 100 characters.
  3. In Endpoint URL, enter a URL Flowstate can reach over the internet. Use HTTPS.
  4. Under Entity Types, select Deselect all, then tick the types to receive.
  5. Under Event Types, tick Create, Update and Delete as needed.
  6. Select Create Endpoint.
  7. Copy the secret shown in Webhook Signing Secret. It isn’t shown again.
  8. Tick I have copied this secret and understand it will not be shown again, then select Done.

An endpoint needs at least one entity type and one event type. Each option in the form subscribes to one entity type:

Optionentity_type
Employeesemployee
Contractorscontractor
Teamsteam
Projectsproject
Vacanciesvacancy
Cost Centrescost_center
Value Streamsvalue_stream
Locationsgeography

Edit, switch off or delete an endpoint

ActionHow
Switch off or onUse the switch on the endpoint’s row. A switched-off endpoint keeps its settings and receives nothing.
EditSelect ⋯, then Edit Endpoint. Make changes and select Save Changes.
DeleteSelect ⋯, then Delete Endpoint. In Delete Webhook Endpoint, select Delete.

A change to an endpoint, including switching it off, deleting it or rotating its secret, can take up to a minute to reach deliveries.

Test an endpoint

Select ⋯, then Test Connection. You’ll see Connectivity test passed, or a message starting Test failed with the HTTP status or error.

The test event:

  • has no X-Flowstate-Signature or X-Flowstate-Event-Id header
  • has entity_type employee, change_type update, entity_id test_entity_id, you as the initiator, and { "test": true } as both before and after
  • has the same 5-second timeout as a delivery
  • updates the endpoint’s delivery status

A handler that rejects unsigned requests fails the test with HTTP 401. The test checks that the endpoint can be reached, not that your signature check works.

Rotate the signing secret

  1. Select ⋯, then Rotate Secret.
  2. Copy the new secret shown in Webhook Signing Secret.
  3. Tick the acknowledgement, then select Done.

Deliveries for up to a minute afterwards can still be signed with the old secret. Accept both secrets until then.

Delivery status

Each endpoint’s row shows the result of its last delivery or test.

BadgeMeaning
HealthyThe last delivery got a 2xx response.
FailingThe last delivery got another status, timed out or couldn’t connect.
No deliveriesNothing has been sent yet.

Beside the badge is when the last delivery happened, or Never delivered. There’s no per-event delivery history in the app.

Entity types

Subscribe to the entity types you need. An endpoint receives events only for the types it’s subscribed to.

Core

entity_typeRecord
employeeAn employee
contractorA contractor
vacancyA vacancy
teamA team
projectA project
initiativeAn initiative

Reference data

entity_typeRecord
cost_centerA cost centre
value_streamA value stream
geographyA location

Allocations

entity_typeRecord
employee_team_allocationAn employee’s allocation to a team
employee_project_allocationAn employee’s allocation to a project
team_project_allocationA team’s allocation to a project
contractor_team_allocationA contractor’s allocation to a team
contractor_project_allocationA contractor’s allocation to a project
vacancy_team_allocationA vacancy’s allocation to a team
vacancy_project_allocationA vacancy’s allocation to a project

Compensation

entity_typeRecord
salary_adjustmentAn employee’s dated salary
bonusAn employee’s bonus
contractor_rateA contractor’s dated rate

AI

entity_typeRecord
ai_agentAn AI agent in the workforce
agent_policyThe organisation’s agent policy
ai_policyThe organisation’s AI usage policy or budget caps

Functional structure

entity_typeRecord
functional_groupAn area, such as a division or department
functional_positionA seat in an area
functional_assignmentA person’s time in a seat: an employee, contractor or vacancy
  • These records are never deleted, so delete never arrives. Ending an area, a seat or somebody’s time in a seat is an update whose after has the new endDate. Subscribe to update to hear about leavers.
  • before and after are the changed area, seat or assignment only, not the structure around it.
  • Sharing an area with a manager, or revoking that, sends a functional_group update for the area.
  • A custom attribute change on an area or seat sends an update for that area or seat. Its before and after are the attribute value, which has a definitionId.

Event types

change_typebeforeafter
createnullThe new record
updateThe record beforeThe record after
deleteThe deleted recordnull

Event object

{
  "id": "5b0f6e3c-9a4d-4f1e-8c7b-2d1e0a9f3b64",
  "timestamp": "2026-03-18T15:30:00.412Z",
  "entity_type": "employee",
  "entity_id": "clx1a2b3c4d5e6f7g8h9",
  "change_type": "create",
  "initiator": {
    "type": "user",
    "id": "clx9u8s7e6r5",
    "email": "jane.chen@example.com"
  },
  "organization_id": "clx9o8r7g6i5d4",
  "before": null,
  "after": {
    "id": "clx1a2b3c4d5e6f7g8h9",
    "firstName": "Alex",
    "lastName": "Rivera",
    "email": "alex.rivera@example.com",
    "startDate": "2026-04-01T00:00:00.000Z",
    "endDate": null,
    "jobRoleId": "clx9r8q7w6e5",
    "geographyId": "clx3g2h1j0k9",
    "defaultCurrencyCode": "GBP",
    "updatedAt": "2026-03-18T15:30:00.281Z"
  }
}

after is shortened in these examples.

FieldTypeDescription
idstringEvent ID, a UUID. Same as the X-Flowstate-Event-Id header. An event sent to several endpoints has the same id at each.
timestampstringISO 8601 time, in UTC, that Flowstate sent the event.
entity_typestringSee Entity types.
entity_idstringFlowstate ID of the record.
change_typestringcreate, update or delete.
initiatorobjectWho made the change.
initiator.typestringSee Initiator types.
initiator.idstringA user ID for user and merge. An API key ID for api_key.
initiator.emailstringThe user’s email. Omitted when it isn’t known.
organization_idstringOrganisation ID.
beforeobject or nullThe record before the change.
afterobject or nullThe record after the change.

before and after

  • They carry the record’s fields with Flowstate’s field names. The shape isn’t versioned and can differ by entity type and by source. Treat fields as optional, and read the record from the REST API when you need a fixed shape.
  • Dates are ISO 8601 strings.
  • Decimal values, such as money and functional FTE, are strings without trailing zeros: "85000", "0.5". Other numbers are JSON numbers.

Ending a seat is an update:

{
  "id": "9b1c2d3e-4f5a-4b6c-8d7e-0f1a2b3c4d5e",
  "timestamp": "2026-06-30T09:12:44.107Z",
  "entity_type": "functional_position",
  "entity_id": "cmfa1b2c3d4e5f6g7h8i",
  "change_type": "update",
  "initiator": { "type": "api_key", "id": "clx4k3e2y1" },
  "organization_id": "clx9o8r7g6i5d4",
  "before": {
    "id": "cmfa1b2c3d4e5f6g7h8i",
    "functionalGroupId": "cmf0z9y8x7w6v5u4t3s2",
    "name": "Staff Engineer, Payments",
    "externalId": "pos-payments-staff-eng",
    "requiredFte": "1",
    "startDate": "2026-01-01T00:00:00.000Z",
    "endDate": null
  },
  "after": {
    "id": "cmfa1b2c3d4e5f6g7h8i",
    "functionalGroupId": "cmf0z9y8x7w6v5u4t3s2",
    "name": "Staff Engineer, Payments",
    "externalId": "pos-payments-staff-eng",
    "requiredFte": "1",
    "startDate": "2026-01-01T00:00:00.000Z",
    "endDate": "2026-06-30T00:00:00.000Z"
  }
}

Initiator types

typeChange made byidemail
userA person in the Flowstate app. Also functional structure changes by an assistant tool, or by a PULL hook acting as the integration’s creator.User IDIn the app only
api_keyA REST API requestAPI key IDOmitted
mergeMerging a scenarioID of the user who mergedOmitted
systemReserved. Not currently sent.——

Signature verification

Headers

HeaderValue
Content-Typeapplication/json
X-Flowstate-Signaturesha256= followed by the lower-case hex HMAC-SHA256 of the raw request body
X-Flowstate-Event-IdThe event id

Verify a delivery

  1. Read the raw request body before parsing it.
  2. Compute HMAC-SHA256 over those bytes, with the signing secret as the key. Use the secret exactly as shown, a 64-character hex string, as text. Don’t hex-decode it.
  3. Hex-encode the result in lower case and prefix sha256=.
  4. Compare it with X-Flowstate-Signature in constant time. Reject the request if the header is missing or doesn’t match.

The signature covers the body only. To guard against replays, store the id of each event you process and ignore repeats. You can also reject events with an old timestamp.

Node.js

Express, reading the raw body:

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.FLOWSTATE_WEBHOOK_SECRET;

function isValidSignature(rawBody, header, secret) {
  if (typeof header !== 'string') return false;
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(header);
  const computed = Buffer.from(expected);
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

function handleEvent(event) {
  console.log(`${event.change_type} ${event.entity_type} ${event.entity_id}`);
}

app.post('/flowstate/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!isValidSignature(req.body, req.get('X-Flowstate-Signature'), secret)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.status(204).end(); // acknowledge within 5 seconds
  setImmediate(() => handleEvent(event));
});

app.listen(3000);

Python

Python 3.10 or later:

import hashlib
import hmac

def is_valid_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if header is None:
        return False
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header, expected)

Pass the raw request body bytes, the X-Flowstate-Signature header and your secret.

Delivery

PropertyBehaviour
RequestPOST with a JSON body
Timeout5 seconds for your endpoint to respond
SuccessAny 2xx status
RetriesNone. Each event is sent to each endpoint once. A non-2xx status, timeout or connection error isn’t retried.
OrderingNot guaranteed. Events can arrive out of order.
DuplicatesRare. Deduplicate on id.
ScenariosNothing is sent until a scenario is merged.
Configuration changesCan take up to a minute to reach deliveries.

Recommendations

  • Verify every signature, and reject requests without one.
  • Respond with a 2xx straight away and process the event afterwards.
  • Deduplicate on id.
  • Don’t rely on order. When the current state matters, read the record from the REST API.
  • Failed deliveries aren’t retried, so reconcile regularly against the REST API.
  • Subscribe only to the entity and event types you use.
  • Rotate the signing secret on your normal credential schedule.