Documentation Get help

Errors

Failures return an HTTP status and a JSON error object. Branch on the status and code; message is for people and can change.

Error object

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "role is required",
    "details": [
      { "field": "role", "message": "role is required" }
    ],
    "errorId": "err_3f9a1c7e5b2d4a60"
  }
}
FieldPresent
codeAlwaysSee Codes.
messageAlwaysHuman-readable. For validation errors, the first entry in details.
detailsBody and query validation errors{ field, message } per problem. field is a dot path, such as salary.currencyCode or readings.3.periodEnd.
errorId400, 403, 404, 500Quote it to Flowstate support.

401, 409 and 501 bodies have code and message only.

Codes

StatuscodeWhen
400VALIDATION_ERRORA body or query parameter fails validation; an include value isn’t allowed; a unique value (externalId, or an employee email) is already taken; or a teamId, projectId, employeeId, contractorId or vacancyId in an assignment body, or in a nested teamAssignment or projectAssignment, doesn’t exist.
401UNAUTHORIZEDMissing, malformed, unknown, revoked or expired key. See Authentication.
403FORBIDDENThe key lacks the permission, or orgId isn’t the key’s organisation.
404NOT_FOUNDThe record in the path doesn’t exist in your organisation; a managerId or jobRole.externalId in the body doesn’t resolve; or scenarioId isn’t one of your scenarios.
409CONFLICTFilling a vacancy that’s already filled.
429—Rate limited. See Rate limits.
500INTERNAL_ERRORServer error.
501NOT_IMPLEMENTEDA create, update, delete or vacancy fill sent with scenarioId. See Scenarios.

Functional groups use their own codes (INVALID_INPUT, FEATURE_DISABLED, PLAN_NOT_EDITABLE and others) in the same { "error": { "code", "message" } } shape, without errorId.

Rate limits

POST /business-metrics/readings allows 120 requests a minute per key. Over the limit it returns 429, a Retry-After header in seconds, and a different body:

{
  "error": "too_many_requests",
  "error_description": "Rate limit exceeded. Please retry later."
}

Handle 429 the same way on any endpoint. The MCP server’s limits are on MCP server.

Retrying

ResponseRetry
429After Retry-After seconds.
500With exponential backoff and a cap on attempts.
400 401 403 404 409 501No. Fix the request, key or permissions.

Creates aren’t idempotent. Before retrying a POST that may have succeeded, look the record up by its externalId (GET /employees/{externalId}); a duplicate externalId returns 400.

async function request(url, init, maxAttempts = 4) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, init);
    const retryable = res.status === 429 || res.status === 500;
    if (!retryable || attempt === maxAttempts) return res;

    const retryAfter = Number(res.headers.get("Retry-After"));
    const delayMs = res.status === 429 && retryAfter > 0
      ? retryAfter * 1000
      : 2 ** attempt * 500;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}