Create an employment agreement for a person. THIS IS THE STEP THAT MAKES SOMEBODY ACTIVE: people_create only makes the record, and a person with no agreement reports isActive false forever — a bulk import therefore lands 100% inactive until this is run for each of them. TWO THINGS BOTH HAVE TO BE TRUE or they stay inactive with no error: the `type` must be a CALCULABLE one (built-ins "agreement", "annex", "termination" are; "list-of-intent" and "work-experience" are not), and the dateFrom/dateTo window must cover today (pass dateTo null for an ongoing contract rather than a far-future date). `employee` is an IRI — /people/<id> from people_list. Types are org-extendable, so run agreements_list on somebody already active to see the codes this org really uses. Requires ROLE_AGREEMENTS_MANAGER. Write.
employee string | null- IRI of the person this agreement employs, e.g. /people/<id> from people_list. Nullable in the schema, but an agreement with no employee attaches to nobody and moves no one to active - pass it.
type string- The CODE of an agreement_type row, e.g. "agreement". NOT a fixed list: an org can add its own types, which is why this is not an enum. WHICH TYPE MATTERS: only a CALCULABLE type makes the employee count as active. Built-in codes are "agreement", "annex" and "termination" (calculable) and "list-of-intent" and "work-experience" (NOT calculable - an employee whose only agreement is one of these still reports isActive false). Run agreements_list on someone already active to see the codes this org really uses.
variant string- Contract form. Built-in values are "uop" (umowa o prace), "uz" (umowa zlecenie) and "uod" (umowa o dzielo).
amount numberminutesPerWeek integerjobSize integerhasFixedWorkingTime boolean- Whether this contract has an agreed weekly working time.
amountType string- Whether `amount` is net or gross: "netto" or "brutto". Defaults to netto.
billingType string- How `amount` is expressed: "per-hour", "per-day", "per-week" or "per-month". Defaults to per-month.
positionName string | nullposition string | nulldateFrom string- First day this agreement covers. HALF OF WHAT MAKES THE EMPLOYEE ACTIVE: a dateFrom in the future leaves them inactive until it arrives, which is correct but is the usual surprise after a bulk load.
dateTo string | null- Last day this agreement covers, or null for open-ended. The other half: a dateTo in the past leaves the employee inactive. For an ongoing contract pass null rather than a far-future date.
currency stringcost string | nullhoursPerWeek integer
Get one employment agreement by id — type, variant, the dateFrom/dateTo window, hoursPerWeek, and the derived `calculable`, `active` and `status`. agreements_list supplies the id. Requires ROLE_AGREEMENTS_MANAGER or ROLE_MEETING_MANAGER. Read-only.
id* string | integer
List employment agreements — filter by employee (IRI), isActive, type or variant. THE way to answer "why does people_list say this person is inactive": each row carries `calculable` and `active`, and a person is active exactly when they hold one that is both. Also the place to read the agreement `type` codes this org actually uses before calling agreements_create, since an org can add its own. Requires ROLE_AGREEMENTS_MANAGER or ROLE_MEETING_MANAGER. Read-only.
isActive booleantype anyemployee anyvariant anyorder.type stringorder.variant stringorder.dateFrom stringorder.dateTo stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Change an existing employment agreement — the way an agreement is ENDED, because the backend exposes no delete on this resource: set `dateTo` to the last day it covers and the person stops being active from then, with the record and its history intact. That is the correct move for a payroll-adjacent row; there is no way to make one disappear and there should not be. Also the way to correct a wrong `type`, `variant` or `positionName` in place rather than stacking a second agreement on the person — TWO agreements do not cancel out, the calculable one keeps them active, so "add a correct one alongside" silently leaves the wrong one in force. `amount`, `amountType` and `billingType` are accepted but the API never returns them, so you cannot read back what you wrote. Requires ROLE_AGREEMENTS_MANAGER. Write.
type string- The CODE of a row in `agreement_type` — see {@see AgreementType}. Still a
plain string and still the same five values it has always held for an org
that has not added any; widened 16 -> 63 in Version20260730150000 because
an org-entered name slugs into this column and 16 does not hold one (63,
not 64, so MySQL can widen it without rebuilding the table — the
migration's docblock has the byte arithmetic).
variant stringamount numberminutesPerWeek integerjobSize integerhasFixedWorkingTime boolean- Whether this contract has an agreed weekly working time.
amountType stringbillingType stringpositionName string | nullposition string | nulldateFrom stringdateTo string | nullcurrency stringcost string | nullhoursPerWeek integerid* string | integer
agreementTypes_create
write
Add a contract type to THIS org's list, so an agreement can be recorded against something the five built-ins do not cover — "Umowa zlecenie", "Kontrakt B2B", "Użytkownik funkcyjny". This is configuration, not a code change: the list is a per-tenant table, and a custom type needs no translation entry because its `name` renders verbatim in all seven locales. DO NOT SEND `id`: the code is slugged from the name server-side with diacritics folded ("Użytkownik funkcyjny" becomes "uzytkownik-funkcyjny"), and passing an id is refused with 422 "Update is not allowed for this operation". Post the name and read the assigned code back off the response. `calculable` DEFAULTS TO FALSE AND IS SILENT: it decides who counts as employed — the resourcing bench, holiday accrual, the cost and budget base — so a type meant for people who should NOT accrue leave or occupy an FTE is correct at false, and a type meant for real employment MUST set it true or everyone on it reports inactive with no error. Nothing will tell you which you got. `position` orders the dropdown; `isActive` defaults true. There is no update or delete over MCP on purpose — `agreement.type` stores this row's id as a bare string with no foreign key, so renaming or removing a type orphans every agreement pointing at it. Requires ROLE_AGREEMENTS_MANAGER. Write.
name string | null- The org-entered label. Null for the built-in five, which carry a
translation key instead — see the class docblock.
calculable boolean- Does an agreement of this type mean "this person is employed"?
position integer- Dropdown sort key. Ties are legitimate while a user reorders and are
broken by the code, so there is deliberately no unique constraint.
isActive boolean
Get one contract type by id — its name or translationKey, `calculable`, `isActive`, `position` and `builtIn`. The id IS the code, so this reads back a type by the same string an agreement stores in `type`. Use it to confirm a type persisted after agreementTypes_create, and to check `calculable` before putting anyone on it. Requires ROLE_USER. Read-only.
id* string | integer
List the contract types THIS org can put on an agreement — the values behind `Ludzie > <person> > Umowy > Edytuj umowę`. Read it before agreements_create or agreements_import, because the list is per-tenant: five built-ins ship ("agreement", "annex", "termination", "list-of-intent", "work-experience") and an org can add its own, so a `type` that is valid in one org 422s in another. THE ID IS THE CODE — the `id` on each row is exactly the string `agreements_create` wants in `type`, not a numeric key to look up. `calculable` is the field that decides whether holding this type makes somebody ACTIVE and counts them into the resourcing bench, holiday accrual and the cost base; a non-calculable type leaves them inactive with no error anywhere, which is intended for a type like "list-of-intent" and a silent bug if you picked it by accident. `builtIn` rows carry a translationKey and a null name; custom rows carry a name rendered verbatim and a null translationKey. Requires ROLE_USER. Read-only.
order.position stringorder.name stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
One allocation by id — a single person's booking on a project, with its dates and percentage. allocations_list finds the id; this reads the full record. An allocation with no employee is an OPEN role (unfilled demand), not a booking. Needs the resourcing module. Read-only.
id* string | integer
List resourcing allocations — date-ranged assignments of a position on a project to an employee (or to nobody yet, an open role). No filters; page with cursor. Each item carries employeeId/employeeName and projectId/projectName already resolved (null employeeId means an open role); positionId is bare — resolve its name via positions_list. source distinguishes sheet-imported rows from ones created directly in Flowtly. Use this to reconcile a resourcing sheet import: read back what landed and compare against what was submitted.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
assetBookings_cancel
read
Release an asset — the way an assignment ends, and the closest thing this resource has to a delete (there is no delete operation). Takes the booking id and a `cancelReason` of 3-255 characters; the booking is kept and stamped with `cancelledAt` so the history survives, and the asset becomes free for the next holder. This is the call to make when an employee leaves: assetBookings_list filtered by `employee` finds what they hold, and this releases each one. Requires ROLE_PROPERTY_BOOKINGS_MANAGER. Write.
cancelReason stringid* string | integer
assetBookings_create
write
Assign an asset to a person or a project. `property` is the asset IRI (/assets/{id}) and is required. Name the holder ONE of three ways: `relation` with a single IRI (/people/{id} for a person, /projects/{id} for a project), or `relationName` (employee | project) plus `relationId`, or the `employee` / `project` IRI field directly. Exactly one holder must resolve — naming neither is refused with "Employee or Project must be set." and naming both with "Employee and Project cannot be set at the same time." TWO THINGS THAT ARE NOT IN THE SCHEMA AND WILL 422 YOU: the asset must already be reservable (`bookingAllowed: true` — set it with assets_update), a business rule enforced for EVERY caller including a manager, refused with "This asset is not reservable."; and the asset's own `bookingType` (minutes | days | single-days | permanently) is what makes sense of `duration` / `endDate` — a space dedicated to one person indefinitely is `permanently` with a `startDate` and no end. Concurrent bookings on one asset are serialised server-side, so an overlap is refused rather than double-booked. Requires ROLE_PROPERTY_BOOKINGS_MANAGER to book on someone else's behalf. Write.
employee string | nullproject string | nullrelation string | nullrelationName string | nullrelationId string | nullproperty* string | nullstartDate stringduration integer | nullendDate string | nullbillingAmount string | nullbillingCurrency string | nullconsumptionSharePercent string | null
Get one asset booking by id — the asset, its holder, the dates, and whether it has been cancelled. Read-only.
id* string | integer
List asset bookings — who or what currently holds each asset, which is the assignment the Assets screen shows and the only place an asset-to-person link actually lives. Each row carries the asset, the holder (`relationName` employee | project plus `relationId`), start/end dates and, once released, `cancelReason` and `cancelledAt`. Filter by `property` to see one asset's history, or by `employee` to see everything one person holds — that second one is what to run before someone leaves. Note `employee` here is the NUMERIC id, not the /people IRI that assetBookings_create takes. Add `exists.cancelledAt: false` to see only what is still held; without it the list includes released bookings too. Read-only.
employee integerrelationName anyrelationId anyproperty anystartDate.before stringstartDate.strictly_before stringstartDate.after stringstartDate.strictly_after stringdateTo stringexists.cancelledAt booleancursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
assetBookings_update
write
Update an existing asset booking — its dates, duration, billing amount/currency, or metered-consumption share. `relationName` and `relationId` are required by the payload, so send the holder the booking already has unless you are deliberately moving it. To end an assignment use assetBookings_cancel, not an endDate in the past. Requires ROLE_PROPERTY_BOOKINGS_MANAGER. Write.
relationName stringrelationId stringstartDate stringduration integer | nullbillingAmount string | nullbillingCurrency string | nullconsumptionSharePercent string | null- This booking's share of the asset's metered consumption, 0–100.
endDate string | nullid* string | integer
assetMeterReadings_get
read
Get one meter reading by id — its meter, date and value. Read-only.
id* string | integer
assetMeterReadings_list
read
List meter readings — the dated values recorded against an asset meter, the raw data the metered-billing split reads. Each row carries the meter, date and value. Use it to read a meter's history: a value that never moves across periods (a stuck or shared meter) bills zero, and a meter with no recent rows is one nobody is reading. Read-only.
meter anymeter.property anyreadingType anysource anyisEstimated booleanreadingAt.before stringreadingAt.strictly_before stringreadingAt.after stringreadingAt.strictly_after stringperiodFrom.before stringperiodFrom.strictly_before stringperiodFrom.after stringperiodFrom.strictly_after stringperiodTo.before stringperiodTo.strictly_before stringperiodTo.after stringperiodTo.strictly_after stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Get one asset meter by id — the asset it sits on, utility type, unit and external/QR identifier, with its readings. Read-only.
id* string | integer
List the org's asset meters — the utility/media counters attached to assets (electricity, water, gas, heat). Each carries the asset it sits on, its utility type and unit, and its readings. Filter by `property` (the asset it belongs to) and `utilityType`. Use it to resolve the meter id that readings take, and to spot meters that read zero, are stuck on one value, or sit on a shared/collective meter. Read-only.
property anyutilityType anyreadingMode anyexternalId anyisActive booleancursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update an asset meter — its label, utility type, unit, or active state. Use it to retire a meter from metering (e.g. a utility now billed straight from the invoice) without deleting its reading history. Requires ROLE_PROPERTIES_MANAGER. Write.
property stringname stringutilityType stringunit stringreadingMode stringexternalId string | nullisActive booleannote string | nullid* string | integer
Create an asset (name + status + bookingType required; status = in-stock | damaged | sold, bookingType = minutes | days | single-days | permanently). bookingType is required even when the asset is never booked — pass "permanently" for something that is not lent out, and leave bookingAllowed false. Two fields carry the structure: `parent` nests an asset under another (a unit under a building, a monitor under a desk), and `attributeSet` sets the category the Assets list groups by, which is also where custom attributes such as area or floor live. `assetCode` is a UNIQUE cross-system handle — use it to hold the id this asset has in the system of record it was imported from, so a re-import updates rather than duplicates. Requires ROLE_PROPERTIES_MANAGER. Write.
attributeSet string | nullparent anyname* stringbookingAllowed booleanstatus* stringbookingType* stringtimes integer | nulldescription stringserialNumber string | nullassetCode string | nullboughtAt string | nullwarrantyTo string | nulllocation string | null
Get one asset by id — name, status, category (attributeSet), parent, assetCode, serial number, purchase and warranty dates, location and booking settings. Read-only.
id* string | integer
Load MANY assets in one call, keyed on `assetCode` — the tool for bringing an inventory across from another system, where assets_create would be one round trip per record. Rows reconcile against the org: an unknown assetCode creates, a known one updates in place, a matching row is skipped, so re-running changes nothing and a half-finished run is safe to repeat. `parentAssetCode` nests a row under another BY ITS CODE, resolved against the org and against earlier rows of the same batch; a parent that never resolves fails that row rather than silently orphaning it. THREE FIELDS MAKE THE RECORD READABLE rather than a bare name: `attributeSetName` is the category the UI shows as Typ zasobu and the list groups by, `locationName` is where the thing physically is, and `attributes` is a {name: value} map for area, floor, price and anything else the source carries. All three are resolved BY NAME — the category, the location, the attribute definitions and their bindings are found or created for you, so a caller never handles one of those IRIs, and names are matched case-insensitively so "Mieszkanie" and "mieszkanie " cannot split the list in two. `attributes` needs a category to hang off, and a value that parses as a number creates a number attribute, decided the first time the name appears. An attribute that fails to write does NOT fail its asset. PASS dryRun:true FIRST on a real inventory load — it reports would-create / would-update / would-skip per row and creates nothing at all, categories and locations included. Max 1000 rows. Requires ROLE_PROPERTIES_MANAGER. Write.
assets* array- The batch, in order. Rows are matched to existing assets by assetCode: an unknown code creates, a known one updates in place, and a row identical to what is already stored is skipped. Re-running the same batch therefore changes nothing.
dryRun boolean- Preview without writing. TRUE reports create / update / skip per row and creates nothing — run a first inventory load dry and read the counts before running it for real. Defaults to false, which writes.
List the org's assets — the register of physical things it owns or sells, from laptops and desks to apartments, parking spaces and storage units. Filter by status (in-stock | damaged | sold), attributeSet (the category the Assets list groups by), bookingAllowed, or a partial name or serialNumber; order by name, status, serialNumber, boughtAt or warrantyTo. NOT PAGINATED — the whole set comes back in one response, so a large register is one big payload rather than a first page. Use it to resolve the asset id that asset bookings and asset documents take. Read-only.
status anybookingType anyattributeSet anyname stringserialNumber stringbookingAllowed booleanorder.name stringorder.status stringorder.serialNumber stringorder.boughtAt stringorder.warrantyTo stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update an asset by id — name, status, category, parent, assetCode, serial number, dates, location or booking settings. This is how an asset moves from in-stock to sold. Note the status vocabulary is in-stock | damaged | sold and has NO reserved state, so a hold has to be modelled some other way. Requires ROLE_PROPERTIES_MANAGER. Write.
attributeSet string | nullparent anyname stringbookingAllowed booleanstatus stringbookingType stringtimes integer | nulldescription stringserialNumber string | nullassetCode string | nullboughtAt string | nullwarrantyTo string | nulllocation string | nullid* string | integer
attributeEntityValues_create
write
Set an attribute value on one entity (attribute + value required). `relation` IS AN IRI — "/properties/7", not the word "property": the backend resolves it and derives the relation name from the resource class, so passing a bare name throws. (`relationId` takes a plain id and still works, but it is deprecated in favour of the IRI.) The attribute must already be BOUND to that entity's category or the value is stored and never displayed. Requires ROLE_ATTRIBUTES_MANAGER. Write.
attribute* string | nullrelation string | nullrelationId string | nullvalue* string | nulldateFrom string | nulldateTo string | null
attributeEntityValues_delete
write
Remove an attribute value from an entity. The definition and the binding survive; only this entity's value goes. Requires ROLE_ATTRIBUTES_MANAGER. Write.
id* string | integer
attributeEntityValues_list
read
List attribute VALUES — what a specific asset, project, budget or client actually holds for a bound attribute. Each row carries the attribute, the value, and `relationId` naming the entity it belongs to. Read-only.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
attributeEntityValues_update
write
Change one attribute value in place, by its id. Use this rather than creating a second value for the same (entity, attribute) pair — nothing enforces uniqueness, so a duplicate is accepted and the UI shows one of them. Requires ROLE_ATTRIBUTES_MANAGER. Write.
value stringdateFrom string | nulldateTo string | nullid* string | integer
Create an attribute definition (name + type required; type is number | string | date | state | period). THE TYPE IS THE DECISION: it is shared by every entity carrying this attribute, so a field created as `string` cannot later total or sort as a number without every existing value being rewritten. Decide it against the values you actually have, not the first one you see. A definition on its own does nothing — bind it to a category with attributeSetAttributes_create, or it never appears anywhere. Requires ROLE_ATTRIBUTES_MANAGER. Write.
name* stringtype* stringshift integerrequired booleanmultiple booleandefaultValue string | nullformatPattern string | null
Get one attribute definition by id — name, type, whether it is required or multiple, default value and format pattern. Read-only.
id* string | integer
List attribute DEFINITIONS — the named fields (area, floor, price) that categories bind and assets carry values for. Each has a type: number | string | date | state | period. Read-only.
order.name stringorder.type stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update an attribute definition — name, type, required, multiple, default or format. Changing `type` on a definition that already has values is the risky one: existing values are not converted. Requires ROLE_ATTRIBUTES_MANAGER. Write.
name stringshift integerrequired booleandefaultValue string | nullformatPattern string | nullid* string | integer
attributeSetAttributes_create
write
Bind an attribute definition to a category (attributeSet + attribute, both IRIs). THIS IS WHAT MAKES AN ATTRIBUTE APPEAR: without the binding a value can be written successfully against an entity and will never show in the UI — a failure with no symptom. Requires ROLE_ATTRIBUTES_MANAGER. Write.
attributeSet* stringattribute* stringrequired booleanimplicitUnitQuantity boolean
attributeSetAttributes_delete
write
Unbind an attribute from a category. The definition and any values survive; they simply stop being shown for that category, which makes this look like data loss when it is not. Requires ROLE_ATTRIBUTES_MANAGER. Write.
id* string | integer
attributeSetAttributes_list
read
List the bindings between categories and attribute definitions — which fields appear on which category. Read-only.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
attributeSets_create
write
Create a category (name + relationName required; relationName is one of property | project | budget | client, and for an asset category it is the plain string "property" — NOT an IRI). Optional icon from a fixed list (room, parking, building, office, local, desk, monitor and so on) which the UI shows beside the category. LIST FIRST: names are not unique, so a second "Mieszkanie" is accepted and quietly splits the Assets list in two. Requires ROLE_ATTRIBUTES_MANAGER. Write.
name* stringrelationName* stringicon string | null
Get one attribute set by id — its name, relationName, icon, and the attributes bound to it. Read-only.
id* string | integer
List the org's attribute sets — the CATEGORIES an asset, project, budget or client is filed under. Filter by relationName: "property" for asset categories (what the UI calls Typ zasobu and the Assets list groups by), plus "project", "budget" and "client". Reach for this before creating one: a category duplicated by spelling or casing silently splits the list it groups, and nothing in the UI explains why. Read-only.
relationName anyorder.name stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
attributeSets_update
write
Rename a category, change its icon, or move it to another relationName. This is how a category created with a typo gets fixed rather than duplicated. Requires ROLE_ATTRIBUTES_MANAGER. Write.
name stringicon string | nullid* string | integer
Create a bank — the institution a bank account belongs to, not the account itself (that is bankAccounts_create). Write.
name stringswift* string
Get one bank by id — the institution, not an account held with it. Use bankAccounts_get for the account.
id* string | integer
List the banks the org's accounts are held with. Hidden banks are INCLUDED by default — pass hidden=false for the pickers' view, or hidden=true to find the retired ones. Use it to resolve the bank id that bankAccounts_list filters on and that bankAccounts_create needs.
hidden anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update a bank by id. This is also how a bank is hidden and un-hidden: set `hidden` true to retire one from the pickers without deleting it, false to bring it back. There is no separate archive tool because the API has no archive action for a bank — the flag is the mechanism. Write.
name stringswift stringhidden boolean- ARCHIVED, NOT DELETED (#4371).
id* string | integer
P&L per employee for a budget — what each person's time earned against what they cost. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
Get one budget by id — its period, scope and settings. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
List the org's budgets — the periods against which income and cost are planned and compared. Use it to resolve the budget id that every pnl tool takes. Needs ROLE_BUDGETS_VIEWER. Read-only.
properties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
name stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
P&L for a budget, broken down BY TAG — income, costsByTag, costsByProject and netByTag over the budget's periods. The tag axis is what makes this readable for a business whose costs are not naturally per-project: tag the documents, and the split follows. Carries displayPricePerSqm when the org has enabled price-per-sqm and named an area attribute, which is what turns this into a per-square-metre view for a property developer. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
budgets_pnlByTagsDrilldown
read
The documents behind one cell of budgets_pnlByTags. Reach for it when a tag total looks wrong — it names the transactions making up the number instead of leaving you to guess. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
clientContacts_create
write
Create a contact person for a client (client, type, name, email required). Write.
client stringtype* stringposition integeremail* stringname* stringtitle string | nullphone string | null
Create a new client record (name, country, currency, status, tinType required). Write.
country* stringcurrency* stringdefaultCost string | nullstatus* stringattributeSet string | nullname* stringdueDays integertinType* stringtinCountry string | nulltin string | nullexternalPaymentCustomerId string | nullinvoiceComment string | nulldefaultTaxRate string | nulldocumentsLanguage stringaddressPhoneNumber string | nulladdressCity string | nulladdressPostCode string | nulladdressStreetLine string | nulladdressBuildingNumber string | nulladdressLocaleNumber string | nulladdressCountry string | nullclientContacts arrayallowDuplicate boolean | null- Not persisted (no ORM\Column): an escape hatch for POST /clients only,
mirroring CounterpartyCreateByTypeInput::allowDuplicate. When true,
ClientCreateProcessor::refuseIfDuplicate() skips the duplicate-party
guard, matching the human-facing PATCH-and-attach pattern in
CounterpartyCreateByTypeProcessor.
vatNumber string | null
Get one client by id — name, country, currency, tax id and status.
id* string | integer
Load MANY clients in one call, keyed on `externalRef` — the tool for bringing a customer or buyer list across from another system, where clients_create would be one round trip per person. Rows reconcile against the org: an unknown externalRef creates, a known one updates in place, a matching row is skipped, so re-running changes nothing. The ref is stored as `externalPaymentCustomerId`, the only external-reference column a client has, and `clients_list` filters on it. DO NOT match clients by name instead — a buyer list is full of shared surnames and joint purchases. Each result carries `counterpartyId`, which is what contracts_import and contracts_create need. Two traps the schema cannot express: a `tin` is REJECTED without a `tinCountry`, and a contact row needs an e-mail, so a phone number alone cannot create one. PASS dryRun:true FIRST on a real onboarding load. Max 500 rows. Requires ROLE_CLIENTS_MANAGER. Write.
clients* array- The batch, in order. Rows are matched to existing clients by externalRef: an unknown ref creates, a known one updates in place, and a row already matching is skipped. Re-running the same batch changes nothing.
dryRun boolean- Preview without writing. TRUE reports would-create / would-update / would-skip per row and creates nothing. Run a first onboarding load dry and read the counts before running it for real. Defaults to false, which writes.
List clients (the org's customers). Filter by status, or by externalPaymentCustomerId to find the client behind a payment-provider id. Use it to resolve the client id that invoices_list, deals_list, projects_list and contracts_list all filter on.
properties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
externalPaymentCustomerId anystatus anyorder.name stringorder.status stringorder.tin stringorder.tinCountry stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update a client record by id. Write.
country stringcurrency stringdefaultCost string | nullstatus stringattributeSet string | nullname stringdueDays integertinType stringtinCountry string | nulltin string | nullexternalPaymentCustomerId string | nullinvoiceComment string | nulldefaultTaxRate string | nulldocumentsLanguage stringaddressPhoneNumber string | nulladdressCity string | nulladdressPostCode string | nulladdressStreetLine string | nulladdressBuildingNumber string | nulladdressLocaleNumber string | nulladdressCountry string | nullclientContacts arrayvatNumber string | nullid* string | integer
List every organization config key the backend recognises, with its type and allowed values. This is the catalog of what is configurable — read it before configs_get or configs_update rather than guessing a key name. Permission is enforced per key by the backend, so a key appearing here does not guarantee the connected user may write it.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Read one organization config value by id, where the id is a key from configKeys_catalog (e.g. organization-logo-url, organization-icon-url).
id* string | integer
Update an organization config value by id (type + name required; permission is enforced per config key by the backend). Write.
type stringname stringvalue string- Every write path reaches this setter — API Platform's denormalizer for
PATCH and POST, `Config::setValue()`, and the providers that seed a
default — so a boolean is normalised and validated HERE rather than at
any one caller. Fixing `configs_update` alone would have left the next
caller with the same footgun.
id* string | integer
contractAttachments_create
write
Attach a document to a contract — normally the executed PDF, or an annex (DPA, SLA, price annex) filed alongside it. Pass the bytes as base64 with a fileName and the contract id from contracts_list; `contractId` here is a BARE id, unlike the IRIs contracts_update takes for counterparty and project, though a full /contracts/<id> IRI is accepted and stripped. SIZE LIMIT: the bytes travel as base64 inside this call, so the whole document has to fit in one model response — keep it under roughly 150 KB, and for anything larger use contractAttachments_createUploadTicket instead, which is built for exactly this and has no such ceiling. An executed contract with a signature card is usually well past that (673,617 bytes becomes 898,156 base64 characters, several times what one response can carry), and no error comes back when it does not fit, because the call cannot be emitted at all — the request never reaches the server, so check the file size BEFORE starting rather than discovering it by failing. This is what clears the missing-document problem contracts_get reports, so a contract maintained over the API stops sitting in the app's needs-tidying queue. One signed document can back several contract rows (a deal with both a recurring and a one-shot part is two rows, because `cyclic` is per-record) — call this once per contract id with the same bytes. The backend analyses the document asynchronously: `status` comes back "pending" and analysisSummary lands later, as a SUGGESTION to check rather than a fact to trust. Write.
fileBase64* string- The document's bytes, base64-encoded (normally the signed PDF). Practical ceiling is about 150 KB of file: base64 inflates it by 4/3 and the result has to be emitted inside this call, so a bigger document cannot be sent this way at all — no error arrives, the call simply cannot be written. Use contractAttachments_createUploadTicket for those: it hands back a URL you POST the raw bytes to, so they never pass through the model at all.
fileName* string- Filename for the upload, e.g. Umowa-022-2026.pdf.
contractId* string- Contract to attach it to — the id from contracts_list, or the full /contracts/<id> IRI.
mimeType string- MIME type of the bytes, e.g. application/pdf.
kind string- contract = the agreement itself. annex = a document filed alongside it (DPA, SLA, price annex).
contractAttachments_createUploadTicket
read
Mint a short-lived, single-use ticket for attaching a LARGE document to a contract — the executed PDF, or an annex. Use this instead of contractAttachments_create whenever the file is more than a few tens of KB: that tool carries the bytes as base64, which a caller has to emit as text, and a real signed contract (~700 KB, ~900 K base64 characters) is far beyond what fits in one response. Pass the contract id from contracts_list plus a fileName; you get back an uploadUrl and a ready-to-run curl. Then send the file's RAW BYTES to that URL (curl --data-binary @file.pdf) — not base64, not multipart — and the response is the created attachment. The ticket expires in 15 minutes, works once, and can only attach to the one contract it names. This is what clears the missing-document problem contracts_get reports. Write.
contractId* string- Contract the document will be attached to — the id from contracts_list, or the full /contracts/<id> IRI.
fileName* string- Filename to record on the attachment, e.g. Umowa-022-2026.pdf.
mimeType string- MIME type of the file, e.g. application/pdf.
kind string- contract = the agreement itself. annex = a document filed alongside it (DPA, SLA, price annex).
counterpartyBankAccounts_create
write
Attach a bank account to a counterparty (counterparty + accountNumber). Write.
counterparty stringaccountNumber string
Add a note to a lead or a deal (body + exactly one of lead/deal). Author is the connected user. Write.
body* stringlead string | nulldeal string | null
Delete a CRM note by id. Write.
id* string | integer
Get one CRM note by id.
id* string | integer
List notes written on leads and deals. Filter by lead or deal to read the running commentary on one record.
lead anydeal anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update a CRM note’s body by id. Write.
body stringid* string | integer
Get one deal lost-reason by id.
id* string | integer
dealLostReasons_list
read
List the reasons a deal can be marked lost, in order. deals_lose requires a lostReasonId from here.
order.position stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Create a deal/opportunity. Required: title, stage (from stages_list), and an ANCHOR — at least one of client or lead. A deal with neither is refused with 422 “A deal must reference a client or a lead.”, so anchor a prospect you have no customer record for to its lead (`/leads/<id>` from leads_list) rather than inventing a client; pass client (`/clients/<id>` from clients_list) once there is one. Setting both is allowed. Optional: amountMinor, currency, expectedCloseDate, owner, contact. Creating straight into a won stage additionally requires client — a lead-only deal cannot be won. Write.
title* stringclient string | null- Anchor: a deal must reference a client OR a lead — set at least one; setting both is allowed. IRI of the customer, e.g. /clients/<id> from clients_list. When the prospect has no client record yet, anchor to lead instead. Required on its own only for a won stage: a lead-only deal cannot be won.
contact string | nullstage* stringamountMinor string | nullcurrency string | nullexpectedCloseDate string | nullowner string | nulllead string | null- The other half of the anchor: a deal must reference a client OR a lead — set at least one; setting both is allowed. IRI of the prospect, e.g. /leads/<id> from leads_list. Use it for an opportunity with no customer record yet, then run leads_convert (or set client) before winning the deal.
Delete a deal by id (soft delete). Write.
id* string | integer
Get one deal by id — title, client, stage, amount, owner, contact, expected and actual close dates.
id* string | integer
List deals/opportunities — the sales pipeline. Filter by status (open / won / lost), stage, owner, client, lead, or by expectedCloseDate / closedAt ranges. Amounts are minor units with an explicit currency; do not assume the org's default.
status anystage anyowner anyclient anylead anytitle stringclosedAt.before stringclosedAt.strictly_before stringclosedAt.after stringclosedAt.strictly_after stringexpectedCloseDate.before stringexpectedCloseDate.strictly_before stringexpectedCloseDate.after stringexpectedCloseDate.strictly_after stringcreatedAt.before stringcreatedAt.strictly_before stringcreatedAt.after stringcreatedAt.strictly_after stringorder.createdAt stringorder.expectedCloseDate stringorder.amountMinor stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Mark a deal lost — requires lostReasonId (from dealLostReasons_list); optional lostReasonNote. BACKFILLING A HISTORICAL LOSS: pass optional closedAt (ISO-8601) to record the date it ACTUALLY closed, exactly as deals_win does. Omit it and the server stamps now. It may not be in the future (422), and may be earlier than the deal’s createdAt. Write.
lostReasonId* string- REQUIRED. A deal-lost-reason id or IRI, from the picklist at GET /deal-lost-reasons. Both forms are accepted.
lostReasonNote string- Optional free-text note kept alongside the picklist reason -- the detail a fixed list cannot carry.
closedAt string- Optional. When the deal ACTUALLY closed, ISO-8601. Defaults to now; may not be in the future (422).
id* string | integer
Reopen a won/lost deal back to open. Write.
id* string | integer
Update a deal by id (title, stage, amountMinor, currency, expectedCloseDate, owner, contact, client, lead). Moving the stage is logged automatically. The anchor rule from deals_create still applies to the result, so you cannot clear the only client or lead a deal has — swap one in first. Moving a deal into a won stage requires client: attach the customer here (or run leads_convert) before winning a lead-only deal. Write.
title stringclient string | null- Anchor: the deal must still reference a client OR a lead after this patch, so null is refused when lead is also empty. IRI of the customer, e.g. /clients/<id> from clients_list. Set it before moving the deal into a won stage — a lead-only deal cannot be won.
contact string | nullstage stringamountMinor string | nullcurrency string | nullexpectedCloseDate string | nullowner string | nulllead string | null- The other half of the anchor: the deal must still reference a client OR a lead after this patch, so null is refused when client is also empty. IRI of the prospect, e.g. /leads/<id> from leads_list.
id* string | integer
Mark a deal won — moves it to a won stage and stamps it closed; optional contractId links an existing contract. BACKFILLING A HISTORICAL WIN: pass optional closedAt (ISO-8601, e.g. “2026-05-07” or a full timestamp) to record the date it ACTUALLY closed. Omit it and the server stamps now, which puts an old deal in this month’s “won this month” figure — so set it whenever you are entering a deal that closed before today. It may not be in the future (422), and it MAY be earlier than the deal’s own createdAt: a deal created today and closed in May is the normal shape of a correct backfill, not an error. The deal must ALREADY reference a client: winning a lead-only deal is refused with 422 “Attach a customer before marking this deal Won.”, because there is no customer to bill. Turn the lead into one with leads_convert, or set client with deals_update, then win. Write.
contractId string- Optional id of an existing contract to link to the won deal. Not found -> 422.
closedAt string- Optional. When the deal ACTUALLY closed, ISO-8601 ("2026-05-07", or a full timestamp). Omit it and the server stamps now, which files a historical deal under this month. It may not be in the future (422), and it MAY precede the deal's own createdAt -- that is the normal shape of a backfill, not an error.
id* string | integer
dealStageHistories_get
read
Get one deal stage-change record by id.
id* string | integer
dealStageHistories_list
read
List a deal's stage transitions, newest first. Filter by deal. Every deals_update that moves the stage is logged here automatically, so this is how you reconstruct how long a deal sat in each stage — the deal itself only carries its current one.
deal anyorder.changedAt stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Add a department, so people can be filed under it. `name` is required (up to 128 characters) and is UNIQUE across the org; `code` is optional (up to 64) and is ALSO unique — the short form an org already uses in its own spreadsheets (CEO, TECH, PROC). `manager` is an optional employee IRI from people_list. LIST FIRST AND EXPECT COLLISIONS: because both name and code are unique, re-posting a department that already exists FAILS rather than being idempotent, so an import that assumes create-per-row will stall the first time it meets a department the org already carries — typically one left over from a trial. Reconcile that row with departments_update instead of creating around it. THERE IS NO DELETE: the backend exposes no delete on a department, so a wrong name or code is corrected in place with departments_update and never removed. Requires ROLE_EMPLOYEES_MANAGER. Write.
name* stringcode string | nullmanager string | null
The org's departments, with the numeric id each one is referenced by. READ THIS BEFORE people_create or people_update: both accept a `department` IRI and there is no other way to discover a valid one. The collection is unpaginated and ordered by name, so a single call returns every department the org has. Filter by `name` (partial match) or `code` (exact). Rows carry id, name and code; `manager` is a relation and is not included in list rows — read it with people_list from the other side if you need it. Requires ROLE_EMPLOYEES_VIEWER. Read-only.
order.name stringid anyname stringcode anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Rename a department, give it a code, or set its manager. This is the tool that makes a department import possible rather than merely convenient: `name` and `code` are both unique, so a department the org already has — the single "HR" row a proof-of-concept tends to leave behind — cannot be created again, and the real list is reached by CORRECTING that row rather than colliding with it. Only the fields you send change, so passing `code` alone leaves the name intact. `id` is the numeric id from departments_list; `manager` is an employee IRI from people_list. THERE IS NO DELETE, which makes this the whole repair story: a department created with a typo is fixed here, and one that should not exist can only be renamed, not removed. Requires ROLE_EMPLOYEES_MANAGER. Write.
name stringcode string | nullmanager string | nullid* string | integer
holidayDaysLimits_create
write
Give a person an allowance of one leave type, effective from a date. `seconds`, NOT days (#3763): an 8h day is 28800, so 21 days is 604800 and an overtime balance of 2h30 is 9000 — a figure that had nowhere to go while this was stored in whole days. `employee` and `holidayType` are IRIs (people_list and holidayTypes_list supply them); `variant` is the contract type the allowance belongs to (uop, b2b, uz, uod). To CORRECT an existing balance, add a row with a later dateFrom rather than editing the old one — the row in force is the latest one whose dateFrom has arrived, so history stays intact and a correction can be entered before it takes effect. (employee, holidayType, variant, dateFrom) is unique, so re-posting the same day replaces nothing and fails. Requires ROLE_HOLIDAYS_MANAGER. Write.
employee* stringseconds* integer- Leave allowance in SECONDS, not whole days (#3763).
holidayType string | nullvariant* stringdateFrom stringnotes string | null
holidayDaysLimits_get
read
One entitlement row by id — the amount, the type, the contract variant and the date it takes effect. holidayDaysLimits_list finds the id. Amounts are in SECONDS (#3763). Read-only.
id* string | integer
holidayDaysLimits_list
read
How much leave each person is ENTITLED to, per type — not how much they have taken, which is holidays_list. Filter by employee. A person can hold several rows for one type over time, because a balance gets topped up or corrected: the row IN FORCE is the one with the latest dateFrom that has already arrived, and rows dated ahead are deliberately ignored until then. Amounts are in SECONDS (#3763) — an 8h leave day is 28800. Requires ROLE_HOLIDAYS_MANAGER. Read-only.
employee anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
holidayDaysLimits_update
write
Fix a row that was entered wrongly — a typo in the amount, the wrong variant. Amounts are in SECONDS (#3763). This is NOT how you record a balance CHANGING over time: for that, holidayDaysLimits_create a new row with a later dateFrom, which preserves what the previous balance was and when. Editing in place rewrites history and makes the old figure unrecoverable. holidayDaysLimits_list finds the id. Requires ROLE_HOLIDAYS_MANAGER. Write.
seconds integer- Leave allowance in SECONDS, not whole days (#3763).
holidayType string | nullvariant stringdateFrom stringnotes string | nullid* string | integer
holidayRequests_cancel
read
Cancel a leave request — use it to clear a request that should never be acted on, such as a row left behind by a trial, a test, or someone who has left. TWO THINGS THAT SURPRISE PEOPLE. (1) IT DOES NOT DELETE THE ROW: the backend sets status to `canceled` rather than removing the row. BUT A CANCELLED REQUEST DISAPPEARS FROM holidayRequests_list — verified on production: afterwards neither the unfiltered list nor status=canceled returns it. So you cannot read back what you cancelled and there is no undo through the MCP; be sure of the id before calling. (2) IT IS NOT THE SAME AS REJECTING. Rejecting records a decision — it writes an approval-log entry naming you and MAILS THE EMPLOYEE that their leave was refused — whereas cancelling notifies only HR, and only when `notify-hr-managers-of-leave-activity` is on for the org. For a row that was never a genuine application, cancel is the honest and quieter one. ONLY WORKS ON A PENDING (`requested`) REQUEST when you are not its owner: an accepted request has already produced a Holiday that this does not remove, so cancelling one would leave a booked absence behind a request reading `canceled`. Requires ROLE_HOLIDAYS_MANAGER for someone else's request; the requester can always cancel their own. holidayRequests_list supplies the id. Write.
id* string | integer
holidayRequests_list
read
Leave REQUESTS and where they stand — pending, approved, rejected. Distinct from holidays_list, which is booked leave: a request still awaiting a decision is not yet an absence, so plan against holidays_list and use this one to see what is waiting on someone. Supplies the holidayRequestId that holidays_approve and holidays_bulkApprove take. Read-only.
status anyemployee anytype anyorder.dateFrom stringorder.dateTo stringorder.type stringorder.status stringproject string- Filter holidays by project (accepts project id or IRI).
search string- Free-text search over the leave request's employee name, type and description.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Who is off RIGHT NOW — every currently-running leave, org-wide, for everyone. This is the tool for 'who is out today', and the one to cross-check before treating resourcingBench_get's freePercent as availability, because the bench does not subtract leave. Unlike holidays_list it applies no project scoping and needs no permission beyond being signed in, so its answer covers the whole organisation. Returns each absence with its type and dates. Read-only.
employee anytype anydateFrom.before stringdateFrom.strictly_before stringdateFrom.after stringdateFrom.strictly_after stringdateTo.before stringdateTo.strictly_before stringdateTo.after stringdateTo.strictly_after stringorder.dateFrom stringorder.dateTo stringorder.type stringproject string- Filter holidays by project (accepts project id or IRI).
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Record leave that a person is actually taking — the booked absence itself, not the entitlement (holidayDaysLimits_create) and not a pending application (holiday requests, which still need approving). What this writes is already-agreed time off, so it appears in holidays_list straight away and needs no approval step. `employee` is an IRI from people_list; `type` is a holidayTypes_list id. `dateFrom`/`dateTo` inclusive, and one call covers a whole range rather than a row per day. Two things bite: a type whose `descriptionRequired` is true (read holidayTypes_list first — `vacations` commonly is) REJECTS a create with no `description`; and `pick-up-day` is time already owed, so it does NOT consume the annual allowance the way `vacations` does — filing a day given back for a Saturday public holiday as `vacations` silently eats a day of someone's entitlement. Check holidays_list for the same person and dates before creating: this endpoint will happily record the same absence twice. EVERY CREATE MAILS THE EMPLOYEE, at their own company address, to say the absence was added — so loading a year of history someone already lived through arrives in their inbox row by row, and for staff who have not been invited yet it is the first they hear of Flowtly at all. Pass `notify: false` for a BACKFILL of absences that already happened; leave it alone when logging something new, because then the mail is the point. It suppresses the message only — the row, its `createdAt` and its payroll fact are written either way. Requires ROLE_HOLIDAYS_MANAGER. Write.
type* string- #4548. `Holiday` is writable directly (`holiday:create`), so it carries the
same unbounded-free-text defect the request did. `max` tracks `length`.
employee* stringdescription string | null- #4548, and it is reachable two ways: written directly through
`holiday:create`, and copied from the accepted request by
`HolidayRequestDecider:47`. Both columns are VARCHAR(255), so bounding the
request at 255 keeps that copy safe — this bounds the direct path.
dateFrom stringdateTo stringrequestedSeconds integer | nullnotify boolean- Should this absence tell the employee it was created? (#4705)
Remove a booked absence outright — the row is deleted, unlike holidayRequests_cancel which only flips a request's status. Use it to clear absences that should never have counted: demo or test rows left by a trial, or ones orphaned when their employee was deleted (people_delete detaches absences rather than removing them, so they survive with an empty employee name). THIS MOVES REAL NUMBERS: a booked absence is `payrollEligible` and consumes the person's entitlement, so deleting one changes their leave balance — the point when clearing test data, and a data-loss bug when the row was genuine. No undo, no notification. Read holidays_list first and be certain the row is not real history: a description in the org's own language, or dates matching an actual absence, usually means it is. Requires ROLE_HOLIDAYS_MANAGER. Write.
id* string | integer
One leave record by id, with its type, dates and duration. Get the id from holidays_list or holidays_active. Read-only.
id* string | integer
Booked leave over a period — the planning view, where holidays_active answers only about today. Filter by employee, by date range, or by project. WHAT YOU SEE DEPENDS ON YOUR PERMISSIONS, and a short list is not proof nobody is off: a holidays manager or accountancy viewer gets the organisation, while a project lead or viewer MUST pass a project filter (or ask about themselves) and is refused outright without one — that refusal is a permission boundary, not an empty calendar. Read-only.
employee anytype anydateFrom.before stringdateFrom.strictly_before stringdateFrom.after stringdateFrom.strictly_after stringdateTo.before stringdateTo.strictly_before stringdateTo.after stringdateTo.strictly_after stringorder.dateFrom stringorder.dateTo stringorder.type stringproject string- Filter holidays by project (accepts project id or IRI).
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
holidayTypes_create
write
Add a leave type the org does not yet offer — a sabbatical, unpaid childcare, a training day — so absences can be booked against it with holidays_create and an allowance granted with holidayDaysLimits_create. `name` (3–64 characters) is what people pick from when booking; `color` and `icon` are how it reads in the calendar; `reducesWorkingTime` false marks time off that does NOT lower the month's expected hours; and `descriptionRequired` true makes the type demand a reason, which holidays_create then enforces — see that tool for what it rejects. `status` defaults to `active`, so a type created without thinking about it is offered to everyone immediately. READ holidayTypes_list FIRST: types are org-wide, and THERE IS NO DELETE — a duplicate or a misspelt name can only be hidden again by setting status to inactive with holidayTypes_update, and it keeps every absence booked against it in the meantime. Requires ROLE_HOLIDAYS_MANAGER. Write.
status stringname* stringcolor string- THE API HAS TO ACCEPT ITS OWN OUTPUT (#4407).
descriptionRequired booleanicon string | nullreducesWorkingTime boolean- Does an absence of this type reduce the month's expected working hours?
The leave types this org uses, with the id each one is referenced by. Read it before holidayDaysLimits_create/update, which need a holidayType IRI and will otherwise be guessed at. The one that is not a holiday in the ordinary sense is `pick-up-day` — time off owed for overtime already worked (Polish *odbior nadgodzin*), which is a GRANTED balance rather than an annual entitlement. Read-only.
order.name stringorder.status stringorder.descriptionRequired stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
holidayTypes_update
write
Change a leave type, and above all TURN ONE BACK ON. `status` flips between `active` and `inactive`, and an inactive type is refused by holidays_create — so recording historic leave against a type the org has since retired starts here, and this is what unblocks a leave-history import rather than sending someone into the app UI. DEACTIVATING IS NOT DELETING, and there is no delete: absences already booked keep an inactive type and still read with it in holidays_list, so inactive means only 'not offered for new bookings'. THE TRAP THAT FOLLOWS FROM THAT: reactivate `vacations` to load last year's absences, forget to set it back to `inactive`, and you have not merely finished an import — you have changed what the organisation offers today, because every employee booking leave now sees that type on the list again. Set it back in the same session you imported. `descriptionRequired` also reaches into holidays_create, which refuses a booking with no description once it is on; turning it on leaves absences already recorded alone. `id` is the string id from holidayTypes_list (`vacations`, `not-paid`), and only the fields you send change. Requires ROLE_HOLIDAYS_MANAGER. Write.
status stringname stringcolor string- THE API HAS TO ACCEPT ITS OWN OUTPUT (#4407).
descriptionRequired booleanicon string | nullreducesWorkingTime boolean- Does an absence of this type reduce the month's expected working hours?
id* string | integer
incomingInvoices_acceptAllSuggestions
read
Accept every pending suggestion on an incoming invoice in one call — what a human does with the app's "accept all" button. The server applies, rebuilds, and applies again until nothing new appears: the transaction match does NOT exist until the supplier and amount are applied, so a single pass would leave the document unattached. Returns a report (what was applied, what was refused and why, and the transaction it ended up filed against). Pass dryRun to preview without writing. Never accepts supplier_create or a duplicate warning. Write.
dryRun boolean | null- Preview only: report what WOULD be applied, write nothing.
id* string | integer
incomingInvoices_applySuggestion
read
Accept one of Flowtly's own suggestions on an incoming invoice — the same proposals a human sees in the app (supplier match, cost group, matching bank transaction, duplicate warning). Read them first with incomingInvoices_suggestions, then apply one by its id. Prefer this over guessing: Flowtly's matcher, not the agent, decides what is plausible. Write.
suggestionId string | nullaction string | nulloverrides array | nullid* string | integer
incomingInvoices_checkEInvoices
read
Pull any new KSeF e-invoices into the org — what the app's "Sprawdź e-faktury" button does. Call this before concluding that a supplier's invoice is missing: without it you cannot tell "the supplier never sent it" from "our sync has not run yet". Returns once the fetch is queued; re-read incomingInvoices_list afterwards to see what arrived. Write.
No parameters.
incomingInvoices_create
write
File an incoming (supplier) invoice or supporting document into accountancy — pass the bytes as base64 with a fileName and receivedAt. Flowtly OCRs it and suggests a supplier and a matching bank transaction. The file is fingerprinted as externalId 'upload_sha256:<sha256 of the bytes>': to avoid a duplicate, hash the bytes and check incomingInvoices_list for that externalId BEFORE uploading. Write.
fileBase64* string- The document's bytes, base64-encoded (a PDF, image, or XML invoice).
fileName* string- Filename for the upload, e.g. Invoice-2026-07-0007.pdf.
receivedAt* string- When the document reached the org (ISO-8601) — e.g. the date of the email that carried it.
mimeType string- MIME type of the bytes, e.g. application/pdf.
type string- invoice = the cost document itself. supporting_document = a payment receipt or annex filed alongside one.
contractor string | integer- Supplier this invoice is from — an id or IRI from suppliers_list. Optional: omit it and Flowtly's OCR suggests a supplier for a human to confirm.
transaction string | integer- Bank transaction this document settles — an id or IRI from transactions_list. Optional: omit it and Flowtly suggests a match.
currency string- Currency IRI, e.g. /currencies/1. Optional.
relatedMonth string- Accounting month this cost belongs to (ISO-8601 date). Optional.
dueDate string- Payment due date (ISO-8601). Optional.
note string- Free-text note. Optional.
incomingInvoices_get
read
Get one incoming (supplier) invoice or supporting document by id, with its OCR'd fields and current match state.
id* string | integer
incomingInvoices_list
read
List incoming (supplier) invoices and supporting documents — the accountancy inbox. An incoming invoice IS a document attached to a bank transaction, so exists.transaction=false is how you find documents that are not yet matched to a payment. Filter also by status, relatedMonth, counterparty, project, tags, or hasDetectedProblems. Each document is fingerprinted as externalId 'upload_sha256:<sha256 of the bytes>' — hash a file and look for that externalId here BEFORE incomingInvoices_create, or you will file a duplicate.
exists.transaction booleanhasDetectedProblems booleanproperties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
status string- Filter transaction attachment by status based on custom logic
relatedMonth stringrelatedMonth.before stringrelatedMonth.strictly_before stringrelatedMonth.after stringrelatedMonth.strictly_after stringreceivedAt.before stringreceivedAt.strictly_before stringreceivedAt.after stringreceivedAt.strictly_after stringtags stringorder.receivedAt stringorder.name stringorder.amount stringname stringinvoiceNumber stringcounterparty anyproject stringbudget stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
incomingInvoices_matchCandidates
read
List the bank transactions that could be the payment for this incoming invoice, ranked by the backend's own matcher. Reach for it when a document has no transaction attached and you need to choose one; prefer these candidates over guessing from amounts yourself.
exists.transaction booleanhasDetectedProblems booleanproperties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
status string- Filter transaction attachment by status based on custom logic
relatedMonth stringrelatedMonth.before stringrelatedMonth.strictly_before stringrelatedMonth.after stringrelatedMonth.strictly_after stringreceivedAt.before stringreceivedAt.strictly_before stringreceivedAt.after stringreceivedAt.strictly_after stringtags stringorder.receivedAt stringorder.name stringorder.amount stringname stringinvoiceNumber stringcounterparty anyproject stringbudget stringid* string | integercursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
incomingInvoices_suggestions
read
Read Flowtly's own proposals for an incoming invoice — supplier match, cost group, matching bank transaction, duplicate warning. These are exactly the proposals a human sees in the app. Read them first, then apply one by id with incomingInvoices_applySuggestion, or take them all with acceptAllSuggestions. Pass refresh to recompute rather than serve the cached set.
exists.transaction booleanhasDetectedProblems booleanproperties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
status string- Filter transaction attachment by status based on custom logic
relatedMonth stringrelatedMonth.before stringrelatedMonth.strictly_before stringrelatedMonth.after stringrelatedMonth.strictly_after stringreceivedAt.before stringreceivedAt.strictly_before stringreceivedAt.after stringreceivedAt.strictly_after stringtags stringorder.receivedAt stringorder.name stringorder.amount stringname stringinvoiceNumber stringcounterparty anyproject stringbudget stringrefresh boolean- Force a rebuild instead of returning the cached suggestions. Suggestions are cached; this is the only way to regenerate ones that are stale or wrong. The transaction-match rebuild runs synchronously; the OCR/LLM rebuild is dispatched async.
id* string | integercursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
incomingInvoices_suggestionsDebug
read
Explain WHY an incoming invoice's suggestions came out as they did — the matcher's scoring, for diagnosing a missing or wrong suggestion. Diagnostic only; use incomingInvoices_suggestions for normal work.
id* string | integer
initialBudgetItems_list
read
List initial budget line items — the planned amounts, by tag, that contractComparison comes back against. Needs ROLE_BUDGETS_VIEWER. Read-only.
initialBudget anytagDefinition anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
initialBudgets_contractComparison
read
PLANNED versus CONTRACTED, per tag — the initial budget's planned amounts against the sum of contract values actually signed for that project. This is the question 'have we committed more than we budgeted, and where', and it reads directly off the contracts already in the org, so importing contracts makes it answerable without any further work. Amounts are grosze; a mixed-currency project produces a notice rather than a silently wrong total. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
Get one initial budget by id, with its items. Needs ROLE_BUDGETS_VIEWER. Read-only.
id* string | integer
List initial budgets — the ORIGINAL plan for a project or investment, as opposed to the live budget it is measured against. Needs ROLE_BUDGETS_VIEWER. Read-only.
project anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
invoiceTransactions_create
write
Record a payment against an outgoing (sales) invoice. `invoice` is an invoice IRI from invoices_list; `date` is when the payment is treated as made. `transaction` is optional — omit it to record settlement with no bank line, which is what you want for historical invoices whose bank statement was never imported. `amount` is optional and defaults to the invoice's outstanding amount. Recording a payment is what stops an issued, past-due invoice being treated as unpaid, so it is also what stops payment reminders being queued for it. Nothing prevents recording two payments against one invoice, so read invoices_get first if you are unsure whether one is already settled. Write.
invoice* stringtransaction string | nullamount number- MINOR units (grosze/cents) — 2 606,74 is 260674, NOT 2606.74. This differs from every neighbouring money field on this API: Invoice.amount, Invoice.amountNet and Transaction.amount are all MAJOR units, so a caller copying a transaction amount straight into this field stores 100x too little and the invoice renders as 1% paid.
date* string
invoiceTransactions_delete
write
Delete a payment record from an invoice by id — read the ids from invoices_get's invoiceTransactions. This removes the RECORD THAT AN INVOICE WAS PAID, not a bank transaction: use it when an invoice carries a payment that should never have existed, the usual case being the same payment booked twice — once by hand and once by the statement import that later matched it. Check invoices_get first and delete the record whose `transaction` is the wrong one (keep the one pointing at the real imported bank line); deleting the last remaining payment makes the invoice unpaid again, which re-arms payment reminders for it. Requires ROLE_INVOICES_MANAGER. Irreversible, high-impact. Write.
id* string | integer
invoiceTransactions_update
write
Update an existing invoice-payment record by id (from invoices_get's invoiceTransactions, or by paging invoiceTransactions). The most common use: point a payment recorded with no bank line at a transaction you just imported via transactions_importStatement, by setting `transaction` to a transaction IRI/id from transactions_list. THE FOOTGUN: this is a PATCH, but the backend still requires `invoice` and `date` on every call — it does NOT merge in the existing values for you. Read the record first (or already have it from the create call) and resend its `invoice` and `date` unchanged alongside whatever you actually mean to change, or the update is rejected. `transaction` accepts null to unlink a payment from a bank line. `amount` is optional. Write.
invoice stringtransaction string | nullamount number- MINOR units (grosze/cents) — 2 606,74 is 260674, NOT 2606.74. This differs from every neighbouring money field on this API: Invoice.amount, Invoice.amountNet and Transaction.amount are all MAJOR units, so a caller copying a transaction amount straight into this field stores 100x too little and the invoice renders as 1% paid.
date stringid* string | integer
leadActivities_bulkImport
read
Record a whole outbound wave — every message you actually sent — in ONE call, instead of one leadActivities_create per message. Pass an array; each row names its lead (leadCompanyName, matched against an EXISTING lead, or a lead IRI) plus type and occurredAt. Give every row an externalId — the stable per-message id, e.g. the Gmail message id — and the import is idempotent: re-running it, or re-running a wave that was only partly imported, reports duplicates instead of creating them. Rows without an externalId dedupe on (lead, type, occurredAt, contact), the same natural key leads_bulkImport uses, so a wave that first landed through that tool is not duplicated here. Every row gets its own outcome (created | duplicate | error), so one malformed row does not discard the rest of the batch. Does NOT create leads — use leads_bulkImport for that. ≤ 1000 rows/call. Write.
activities* array
leadActivities_byList
read
Every lead activity on a CAMPAIGN (a lead list), in one call — pass the list's id, IRI, or exact name. leadActivities_list filters by a single lead, so campaign-level reporting otherwise costs one call per member (302 for a list like PZFD); this resolves the list's members and reads their activities in bounded batches instead. Combine with type and occurredAt.after/.before to get the counts people actually ask for: reply rate (type=reply_received), bounce rate (type=bounced), send coverage (type=message_sent). Returns listId, listName, leadCount, and the merged activities sorted by occurredAt. An unknown list is an ERROR, not an empty result — so a mistyped name cannot read as "this campaign had no activity". Ids come from leadLists_list. Read-only.
list* string- Lead list id, IRI, or exact name. An unknown list is an error, not an empty result.
membershipStatus any- Restrict to members with this outreach status (pending, contacted, replied, bounced, do_not_contact).
type any- Activity type, e.g. message_sent, reply_received, bounced, follow_up.
channel anycontact stringthreadId stringexternalId stringoccurredAt.after stringoccurredAt.before stringoccurredAt.strictly_after stringoccurredAt.strictly_before stringorder.occurredAt string
leadActivities_create
write
Log ONE outreach touch on a lead — an invite sent, an invite accepted, a message, a reply, a call, a follow-up (lead + type + occurredAt required; channel, contact, body optional). THIS is where a prospect's outreach history belongs: a crmNote is freeform commentary, an activity is the structured, filterable touch-log the prospecting queue timeline renders. Do NOT narrate touches into a note. type: invite_sent | invite_accepted | message_sent | reply_received | call | meeting | follow_up | …; channel: linkedin | email | phone | …. Write.
lead* stringcontact string | nullwave string | nulltype* stringchannel string | nulloccurredAt* stringbody string | nullexternalId string | nullthreadId string | nullfromMailbox string | null
leadActivities_delete
write
Delete a logged outreach activity by id. Write.
id* string | integer
Get one lead activity (outreach touch) by id.
id* string | integer
List a lead's outreach touches — its activity timeline (invite sent, replies, calls, follow-ups). Filter by lead to read one prospect's history. This is the structured counterpart to crmNotes_list: activities are the typed, dated touch-log; notes are freeform commentary.
lead anycontact anytype anychannel anyexternalId anythreadId anywave anylead.memberships.list anyoccurredAt.before stringoccurredAt.strictly_before stringoccurredAt.after stringoccurredAt.strictly_after stringorder.occurredAt stringorder.type stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
leadActivities_update
write
Update a logged outreach activity by id (type, channel, occurredAt, body). Write.
contact string | nulltype stringchannel string | nulloccurredAt stringbody string | nullexternalId string | nullthreadId string | nullfromMailbox string | nullid* string | integer
leadContacts_create
write
Add a contact person to a lead (lead + name required; email, phone, role, linkedinUrl, isPrimary optional). A contact's LinkedIn URL belongs in linkedinUrl, NOT in a crmNote. Write.
lead stringname* stringemail string | nullphone string | nulllinkedinUrl string | nullrole string | nullisPrimary boolean
leadContacts_delete
write
Delete a lead contact by id. Write.
id* string | integer
Get one lead contact by id.
id* string | integer
List the contact people attached to leads. Filter by lead to read one prospect's contacts, or by email to find which lead a message came from.
lead anyemail anylinkedinUrl anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
leadContacts_update
write
Update a lead contact by id — e.g. set linkedinUrl / email / phone once you find them. Write.
lead stringname stringemail string | nullphone string | nulllinkedinUrl string | nullrole string | nullisPrimary booleanid* string | integer
leadListMemberships_create
write
Add a lead to an outbound list (list + lead required; status optional). Any lastContactedAt you pass is a snapshot that nothing will advance afterwards — log the touch as a lead activity as well, or it stays unqueryable. Write.
list* stringlead* stringstatus stringlastContactedAt string | null
leadListMemberships_delete
write
Remove a lead from an outbound list. Write.
id* string | integer
leadListMemberships_get
read
Get one lead-to-list membership by id. Its status and lastContactedAt are a caller-written snapshot, not live state — see leadListMemberships_list.
id* string | integer
leadListMemberships_list
read
List which leads sit on which outbound prospecting lists. Filter by list, lead or status. CAUTION: status and lastContactedAt are a SNAPSHOT written by whoever last imported or updated the membership. They are not derived, and nothing advances them when an activity is recorded — logging a wave of 529 follow-ups moves neither field — so they can be arbitrarily far behind. To answer “when did we last touch this prospect”, read the activity log instead: leadActivities_list for one lead, leadActivities_byList for a whole campaign. leadListMemberships_syncFromActivities reports the gap and can close it.
list anylead anystatus anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
leadListMemberships_update
write
Update a lead’s membership in a list — e.g. set outreach status (contacted/replied/bounced). status and lastContactedAt are caller-maintained: what you write stands until someone writes again, and recording lead activities does NOT update them. Write.
list stringlead stringstatus stringlastContactedAt string | nullid* string | integer
Create an outbound prospecting list (name required). Write.
name* stringdescription string | nullowner string | nullchannel string | nullstatus stringsegment string | null
Delete an outbound list by id. Write.
id* string | integer
Get one outbound prospecting list by id.
id* string | integer
List outbound prospecting lists. Use it to resolve the list id that leadListMemberships_create takes.
status anychannel anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update an outbound list by id. Write.
name stringdescription string | nullowner string | nullchannel string | nullstatus stringsegment string | nullid* string | integer
Get one lead lost-reason by id.
id* string | integer
leadLostReasons_list
read
List the reasons a lead can be marked lost, in order.
order.position stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Import many leads in ONE call, each with its contacts, list membership and outreach activities nested — the server creates the lead then threads its id into the children, so you never juggle intermediate IRIs. Idempotent by natural keys (companyName / email / (list,lead) / (type,occurredAt,contact)): safe to re-run and to chunk (≤100 leads/call). This is the bulk path a campaign import should use instead of N leads_create calls. Write.
list objectleads* array
Convert a qualified lead into a Client + one contact per lead-contact + an open Deal. Requires an existing client (the lead’s client or a clientId in the body). Write.
companyName string | nullwebsite string | nullsource string | nullowner string | nullclient string | nullindustry string | nulllinkedinUrl string | nulldoNotContact boolean- getDoNotContact(), NOT isDoNotContact() — see the property's comment. An
is*() accessor is invisible to API Platform's readability check and the flag
would be silently dropped from every payload.
companySize string | nullstage string | nullid* string | integerclientId string- Optional id of an EXISTING client to link the lead to. Omit it and the lead's own client is used, or a new Client is created from the lead.
title string- Optional title for the Deal this creates. Defaults to the lead's companyName.
Create a lead (outbound/inbound prospect target; companyName, source, owner, linked client optional). A new lead is always status=open — status is not settable here, and moves only through leads_convert, leads_lose and leads_reopen. Write.
companyName string | nullwebsite string | nullsource string | nullowner string | nullclient string | nullindustry string | nulllinkedinUrl string | nulldoNotContact boolean- getDoNotContact(), NOT isDoNotContact() — see the property's comment. An
is*() accessor is invisible to API Platform's readability check and the flag
would be silently dropped from every payload.
companySize string | nullstage string | null
Check whether a prospect is already in the CRM, using the same filters as leads_list (companyName, source, owner, …). Call this BEFORE leads_create: a duplicate lead splits the outreach history across two records, and nothing downstream will merge them for you.
status anysource anyowner anyclient anycompanyName stringlinkedinUrl anydoNotContact booleancreatedAt.before stringcreatedAt.strictly_before stringcreatedAt.after stringcreatedAt.strictly_after stringclosedAt.before stringclosedAt.strictly_before stringclosedAt.after stringclosedAt.strictly_after stringorder.createdAt stringorder.companyName stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Delete a lead by id (soft delete). Write.
id* string | integer
Get one lead by id — company, website, source, status, owner and the client it converted to, if any.
id* string | integer
List leads — prospect targets, before qualification. Filter by status, source, owner, client, companyName, or createdAt/closedAt ranges. A qualified lead becomes a Client plus an open Deal via leads_convert; until then it lives only here, not in clients_list.
status anysource anyowner anyclient anycompanyName stringlinkedinUrl anydoNotContact booleancreatedAt.before stringcreatedAt.strictly_before stringcreatedAt.after stringcreatedAt.strictly_after stringclosedAt.before stringclosedAt.strictly_before stringclosedAt.after stringclosedAt.strictly_after stringorder.createdAt stringorder.companyName stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Close a lead as LOST — sets status=lost and stamps closedAt. REQUIRES lostReasonId, the `id` of a leadLostReasons entry (run leadLostReasons_list first; it is a picklist, so free text is refused with 422). This is the ONLY way to record a lead as lost: leads_update ignores status, and doNotContact means "never contact again", which is a different and much stronger statement than "we did not win this one". It does NOT move the lead's stage — LeadStage has no terminal flag, so the lead keeps its funnel position and leads_reopen can restore it exactly. Write.
companyName string | nullwebsite string | nullsource string | nullowner string | nullclient string | nullindustry string | nulllinkedinUrl string | nulldoNotContact boolean- getDoNotContact(), NOT isDoNotContact() — see the property's comment. An
is*() accessor is invisible to API Platform's readability check and the flag
would be silently dropped from every payload.
companySize string | nullstage string | nullid* string | integerlostReasonId* string- REQUIRED. The id of a leadLostReasons entry — run leadLostReasons_list and pass its `id`, e.g. the entry labelled "Chose a competitor". Without it the call is refused with 422 "A lost reason is required."; the reason is a picklist entry, not free text.
Undo leads_lose — sets status back to open and clears closedAt and the lost reason. The stage is untouched, so the lead resumes exactly where it was. Reach for this when a lead was closed against the wrong record or the prospect came back. Write.
companyName string | nullwebsite string | nullsource string | nullowner string | nullclient string | nullindustry string | nulllinkedinUrl string | nulldoNotContact boolean- getDoNotContact(), NOT isDoNotContact() — see the property's comment. An
is*() accessor is invisible to API Platform's readability check and the flag
would be silently dropped from every payload.
companySize string | nullstage string | nullid* string | integer
Update a lead by id (company, website, source, owner, linked client, stage, doNotContact). NOT status or lostReason: those are refused by the entity and silently ignored by this endpoint, so closing a lead needs leads_lose (with a lostReasonId) and undoing that needs leads_reopen. Moving `stage` walks the funnel; it does not close the lead. Write.
companyName string | nullwebsite string | nullsource string | nullowner string | nullclient string | nullindustry string | nulllinkedinUrl string | nulldoNotContact boolean- getDoNotContact(), NOT isDoNotContact() — see the property's comment. An
is*() accessor is invisible to API Platform's readability check and the flag
would be silently dropped from every payload.
companySize string | nullstage string | nullid* string | integer
Get one lead stage by id.
id* string | integer
List the stages a lead moves through, in order. Leads have their own stage set — deals use stages_list, which is a different thing.
order.position stringorder.label stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Create a location (name required; optional officeOpenHour/officeCloseHour as seconds past midnight). Use the real address rather than a project or investment name — this is what someone standing in front of the asset needs, and the project name is already carried elsewhere. Requires ROLE_LOCATIONS_MANAGER. Write.
name* stringofficeOpenHour integer | nullofficeCloseHour integer | null
Get one location by id — its name and office hours. Read-only.
id* string | integer
List the org's locations — the physical places assets sit, shown in the UI as Lokalizacja. Needs ROLE_LOCATIONS_MANAGER, which unusually gates the READ as well as the write. Read-only.
order.name stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Rename a location or change its office hours. Requires ROLE_LOCATIONS_MANAGER. Write.
name stringofficeOpenHour integer | nullofficeCloseHour integer | nullid* string | integer
Return the organization this MCP connection is bound to — { orgId, name, slug, userId }. Call it to confirm WHICH tenant you are about to write into before any create/update: the connection is pinned to exactly one org by the token, and writing prospects/records into the wrong org is a real incident. Read-only.
No parameters.
organizationAddresses_get
read
Get one subscription address record by id — name, street, city, postCode, country, and the tax fields. `street` carries the building number when it was entered by hand, and does not when it came from the NIP/GUS lookup. Read-only.
id* string | integer
organizationAddresses_list
read
List the organization's subscription address records — the address attached to the Flowtly subscription, and the source the mail-footer {{organizationAddress}} renders from. Normally exactly one row. This is NOT the invoice seller address, which lives in the organization-billing-* config keys (configs_get) and is what invoices and KSeF read; the two are maintained separately and routinely disagree. Read both before concluding which one a customer actually edited. Read-only.
organization anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
organizationAddresses_update
write
Update the organization's SUBSCRIPTION address record (id required; send only the fields you are changing). THIS IS THE RECORD THE MAIL FOOTER RENDERS FROM: the footer's {{organizationAddress}} is composed as "street, postCode city" from here, NOT from the organization-billing-* config keys that invoices and KSeF use as the seller address. The two stores drift, and the footer reading this one is a known defect — so when a signature shows an address the customer swears they corrected, they corrected the billing keys and this is the record still holding the old value. `street` is a single free-text column that must carry the building number too: the NIP/GUS lookup fills only the street name and silently discards the building and flat number, which is why addresses here read "ul. Example" with no number. Write the full "ul. Example 8/12" to repair it. READ FIRST with organizationAddresses_list and compare against configs_get on organization-billing-street before writing, so you copy the customer's own maintained value rather than inventing one. Requires ROLE_BILLINGS_MANAGER. Write.
type stringname stringstreet stringcity stringpostCode stringcountry stringemail string | nulltin string | nulltinCountry string | nullid* string | integer
organizationIcon_upload
read
Upload/replace the organization's icon/favicon (base64 image + contentType + filename). Read the current one via configs_get organization-icon-url. Write.
bytesBase64* string- The image file bytes, base64-encoded.
contentType* string- The image MIME type, e.g. image/png, image/jpeg, image/svg+xml.
filename* string- A filename for the upload, e.g. logo.png.
organizationLogo_upload
read
Upload/replace the organization's logo (base64 image + contentType + filename). Read the current one via configs_get organization-logo-url. Write.
bytesBase64* string- The image file bytes, base64-encoded.
contentType* string- The image MIME type, e.g. image/png, image/jpeg, image/svg+xml.
filename* string- A filename for the upload, e.g. logo.png.
Get an organization by id. WARNING — this does NOT tell you which organization you are connected to. An OAuth connection is pinned to exactly one org (token-bound), but this endpoint returns any org the connected USER is a member of, so a successful read here reads like confirmation you are working in that org when you may not be. To verify the tenant you are actually operating on, read tenant-scoped data instead — people_list or clients_list — and never start a bulk write on the strength of this call alone.
id* string | integer
paymentScheduleLines_create
write
Add one instalment to a contract's payment schedule — the plan of what is expected to be invoiced or paid, and when. Pass the contract IRI, a date and an amount. This is what clears the missing-payment-schedule problem contracts_get reports on a non-cyclic contract: a one-shot fee still has a schedule, it is simply a single line for the whole amount on the day it falls due. Cyclic contracts are not checked for one, because the system does not auto-generate lines from a cadence. AMOUNT IS IN MINOR UNITS — grosze, not złote: 5 300,00 is "530000", and "5300" silently books a 53,00 line. The API returns them the same way, so read one back with contracts_paymentScheduleLines if you are unsure of the scale. Read the result back with contracts_paymentScheduleLines. Write.
contract string- IRI of the contract this instalment belongs to, e.g. `/contracts/0f7d9c1e-...`. A bare id is NOT promoted on a request body — send the full path. The backend requires it (the column is not nullable) even though the spec declares no required list.
date string- When the instalment falls due. Date-time; the date part is what matters.
amount* string- MINOR UNITS as a decimal string — grosze, not złote. 5 300,00 is "530000"; writing "5300" creates a 53,00 line. Carries no currency of its own — the contract's currency applies.
note string | null- Free-text label for the instalment, e.g. the milestone it hangs off. Optional.
paymentScheduleLines_delete
write
Remove one payment schedule line by id. Deletes the PLAN, not the money: an invoice or transaction already matched to the line is not affected, but it stops being reconciled against anything. Prefer paymentScheduleLines_update for an instalment that moved. Write.
id* string | integer
paymentScheduleLines_import
read
Load a contract's whole instalment plan in one call, instead of one round trip per line. Built for developer contracts, which are paid in construction tranches — a single sale is six to twelve instalments, and a register of them is hundreds. Each row names its contract BY NAME (for an imported developer contract, its agreement number), a due date, and an amount in MINOR UNITS — grosze, so 5 300,00 is "530000" and "5300" silently books 53,00. Rows reconcile against the lines already there on contract+date+amount+note, so an unknown line is created, an identical one is skipped, and re-running the same batch changes nothing; PaymentScheduleLine has no external-reference column, so that natural key is the reconcile key. A row whose contract name matches nothing, or matches MORE than one contract, is reported failed rather than attached to a guess — putting an instalment on the wrong contract misstates two cashflows at once. Pass dryRun:true first on a real load. Max 1000 rows. Write.
lines* array- The instalments, in any order. Each is matched to its contract BY NAME and reconciled against the lines already there, so an unknown line is created, an identical one is skipped, and re-running the same batch changes nothing.
dryRun boolean- Preview without writing. Reports would-create / skipped / failed per row and creates nothing.
paymentScheduleLines_update
write
Change one payment schedule line by id — its date, amount or note. Use it when an instalment slips or is renegotiated, rather than deleting and recreating, so the line keeps any invoice already matched to it. AMOUNT IS IN MINOR UNITS — grosze, not złote: 5 300,00 is "530000", and "5300" silently books a 53,00 line. The API returns them the same way, so read one back with contracts_paymentScheduleLines if you are unsure of the scale. Write.
date string- When the instalment falls due.
amount string- MINOR UNITS as a decimal string — grosze, not złote. 5 300,00 is "530000".
note string | null- Free-text label for the instalment. Optional.
id* string | integer
Create a person/employee record (firstname + lastname required; optional companyEmail, contactEmail, contactPhone). Write.
companyEmail string | nullcontactEmail string | nullcontactPhone string | nullbirthday string | nullfirstname* string | nulllastname* string | nullphoneTextAllowed booleantracksTime boolean- Whether this employee is expected to log working time. When false, the
time-tracking dashboard widgets (project timer + monthly working-hours)
are hidden for them. Defaults to true so existing employees keep their
current behaviour.
excludedFromProjects boolean- Whether this employee is excluded from project assignment. When true they
are not assignable to projects: the Resourcing bench stops offering them
and the time-logging assignee picker can filter them out. Defaults to
false — included — so every existing employee keeps their current
behaviour. This narrows FUTURE assignability only; it never touches an
excluded person's existing allocations, which stay real facts.
department string | nullreportsTo anydefaultProject string | null- Project this employee's working time is assigned to by default when not
logged against a specific project. Stored only for now — no automatic
assignment is wired up yet.
Delete an employee/person record by id (e.g. to remove a placeholder/dummy employee). Requires ROLE_EMPLOYEES_MANAGER; the backend runs a delete processor that also detaches related records. High-impact, irreversible. Write.
id* string | integer
Get one person/employee record by id — names, emails, phone, manager, and whether they are active.
id* string | integer
people_getPermissions
read
What a person can actually do, resolved: their permission groups (each with the roles it grants), their per-person overrides, and the effectiveRoles the two combine into. THE way to check whether an access change landed — people_list shows a roles field, but this is the one that explains WHY it holds those roles and which lever to pull to change it. Reach for it before every people_setRoleOverrides call, because that tool replaces the override lists wholesale and this is where you read the current ones. staleOverrides are removed-overrides that no longer match any group-granted role, so they currently do nothing. people_list supplies the id. Requires ROLE_ROLES_MANAGER to view anyone but yourself. Read-only.
id* string | integer- Person/employee id (or IRI). people and employees share the same id.
Give an existing person a LOGIN: creates a pending organization invitation and emails it to them, in the org's configured UI language. This is the step people_create and people_setPermissionGroups do NOT do — a person with permission groups still cannot sign in until they are invited and accept. Requires the person's email; fails if they already have a login. Onboarding order: people_create (record) -> people_invite (login) -> people_setPermissionGroups (rights). Write.
id* string | integer- Person/employee id (or IRI) to give a login to. people and employees share the same id.
email* string- Address the invitation is sent to, and the address the person will sign in with. Usually the person's companyEmail — read it off people_get rather than guessing.
firstname string- Optional; shown in the invitation mail. Defaults to the person's own firstname when omitted.
lastname string- Optional; shown in the invitation mail. Defaults to the person's own lastname when omitted.
phone string- Optional phone number recorded on the invited user.
List people/employees. Filter by isActive, reportsTo (a manager's id), projectMembers.project, or search; page with cursor. People and employees share the same id, so this is how you resolve the employee id that work time, responsibilities, project membership and permission tools all expect.
isActive booleanid anyfirstname stringlastname stringreportsTo anyprojectMembers.project anyexists.userId booleanexists.reportsTo booleanexists.companyEmail booleanexcludedFromProjects booleansearch string- Free-text search over employee name and company email.
properties array- Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: properties[]={propertyName}&properties[]={anotherPropertyName}&properties[{nestedPropertyParent}][]={nestedProperty}
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
people_setPermissionGroups
read
Set (replace) a person's WHOLE permission-group set by numeric group ids (see permissionGroups_list — e.g. the "Business Owner" group grants ROLE_ADMIN): pass every group they should end up with, and [] removes all of them. Grants access; does NOT create a login or email the person — that is people_invite. THE TRAP: giving someone their FIRST group moves them onto the computed model, where roles come from groups and per-person overrides, and a role granted to them by hand outside that model disappears in the very same call — a ROLE_ADMIN handed to one person is exactly the kind this removes. It cuts the other way too: clearing their last group moves them back off it and makes those older roles reappear. The overridesAdded/overridesRemoved lists say nothing about any of this; they describe overrides and stay empty while effective access changes. So the response reports the difference between the roles the person held before this call and after it, as rolesLost and rolesGained — that is the pair to read once the call returns. rolesLost null (not []) means the snapshot taken before the write could not be read and the delta is UNKNOWN, with the reason in roleDeltaUnavailable: the group change still happened, so a null is not a clean bill of health — re-check with people_getPermissions. To give back a role that should have survived, grant it with people_setRoleOverrides. Requires ROLE_ROLES_MANAGER. Write.
id* string | integer- Person/employee id (or IRI). people and employees share the same id.
groupIds* array- Numeric permission-group ids the person should have (see permissionGroups_list). REPLACES their current set — pass every group they should keep. Empty array removes all groups.
people_setRoleOverrides
read
Set (replace) the roles ONE person gets on top of — or has taken away from — their permission groups. Reach for a group first (people_setPermissionGroups): groups are the intended abstraction and scale to more than one person, so use an override only where a single individual genuinely differs from every group. REPLACES both lists wholesale, so read people_getPermissions first and pass back every override they should keep; omitting a list clears it. Roles are ROLE_ constants — permissionGroups_list shows the ones this org already uses. A role in both added and removed is refused rather than guessed at. Returns the same resolved snapshot as people_getPermissions, so you can confirm the result without a second call. Does NOT create a login — see people_invite. Requires ROLE_ROLES_MANAGER. Write.
id* string | integer- Person/employee id (or IRI). people and employees share the same id.
added array- Roles this person gets ON TOP OF their groups. REPLACES the whole added-set — read people_getPermissions first and pass back every override they should keep. Omit or pass [] to clear.
removed array- Roles to take away from this person even though a group grants them. REPLACES the whole removed-set. Omit or pass [] to clear.
Update a person/employee record by id (name, companyEmail, contactEmail, contactPhone, etc.). Write.
companyEmail string | nullcontactEmail string | nullcontactPhone string | nullbirthday string | nullfirstname string | nulllastname string | nullphoneTextAllowed booleantracksTime boolean- Whether this employee is expected to log working time. When false, the
time-tracking dashboard widgets (project timer + monthly working-hours)
are hidden for them. Defaults to true so existing employees keep their
current behaviour.
excludedFromProjects boolean- Whether this employee is excluded from project assignment. When true they
are not assignable to projects: the Resourcing bench stops offering them
and the time-logging assignee picker can filter them out. Defaults to
false — included — so every existing employee keeps their current
behaviour. This narrows FUTURE assignability only; it never touches an
excluded person's existing allocations, which stay real facts.
department string | nullreportsTo anydefaultProject string | null- Project this employee's working time is assigned to by default when not
logged against a specific project. Stored only for now — no automatic
assignment is wired up yet.
id* string | integer
permissionGroups_create
write
Create a permission group (name required; roles = list of ROLE_* strings it grants). Write.
code string | null- Stable code for default seed groups; null for org-created custom groups.
name* stringdescription string | nullroles array- ROLE_* string values
permissionGroups_get
read
Get one permission group by id, including the ROLE_* strings it grants.
id* string | integer
permissionGroups_list
read
List the org's permission groups and the roles each one grants — e.g. the "Business Owner" group grants ROLE_ADMIN. Read this before people_setPermissionGroups: the roles in the response are the authority on what a group actually permits, so you never have to guess from its name.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
permissionGroups_update
write
Update a permission group's name, description, or granted roles by id. Write.
name stringdescription string | nullroles array- ROLE_* string values
id* string | integer
Get one sales pipeline by id.
id* string | integer
List sales pipelines. A pipeline owns an ordered set of stages — read them with stages_list filtered by pipeline.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
List positions — the named roles (e.g. "Backend Engineer") that a project allocation fills. No filters; Position has pagination disabled, so this always returns the org's full role catalog in one call. Each item is {id, name, roles}. Use it to resolve the position name behind an allocations_list row's positionId, and to find the position id a resourcing import must match against.
order.name stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
projectMembers_create
write
Put a person ON a project (employee + project IRIs required, e.g. "/people/204" and "/projects/243"; optional position = employee|tech-lead|account-manager|viewer, default employee). THIS IS THE ACCESS CONTROL, not a label: a person who is not a member does not see the project at all — it is missing from their Projects list and they cannot log time against it — so this is the tool that restores someone locked out of a project. POSITION IS NOT COSMETIC: a user holding a project-scoped role sees only the projects where their membership position matches it — ROLE_PROJECTS_LEAD matches tech-lead, ROLE_PROJECTS_VIEWER matches viewer — so giving a project lead an `employee` row leaves them just as blind as no row at all. Membership does NOT cascade: putting someone on a parent folder gives them nothing on the projects underneath it, so a folder tree needs one call per project. The unique key is (employee, project, position), which means positions stack rather than replace — a person can hold employee AND tech-lead on the same project as two separate rows, and adding tech-lead to someone who is already an employee there does not remove or upgrade the employee row (use projectMembers_update to change a position in place). Read the current rows with projectMembers_list?project=/projects/{id} first, or projects_get, whose projectMembers array carries each row's id. NOT SILENT: adding a person who is not yet on the project dispatches a project-assigned notification to them, so a 17-project backfill sends 17 notifications. Requires ROLE_PROJECTS_MANAGER. Write.
employee* stringproject* stringposition stringpositionName stringnotify boolean- Whether to send this person the "project assigned" notification (#3955).
projectMembers_delete
write
Take a person OFF a project by membership id — find it with projectMembers_list or in projects_get's projectMembers array. This REVOKES ACCESS: once the last membership row for that person on that project is gone, the project disappears from their view and they can no longer log time against it, which is exactly how a project silently vanishes for someone. Hours already logged are NOT deleted and stay on the project; the person simply can no longer see or add to them. Deleting one position leaves any other position the same person holds on the same project intact. Requires ROLE_PROJECTS_MANAGER. Irreversible (recreating makes a new row and re-notifies), high-impact. Write.
id* string | integer
Get one project membership by id — its employee, project and position. Ids come from projectMembers_list or the projectMembers array on projects_get.
id* string | integer
List project memberships — WHO CAN SEE WHICH PROJECT. Filter by project (`/projects/{id}`) to read one project's roster, or by employee to read every project one person can reach; each row carries its own id, the employee, the project and the position (employee|tech-lead|account-manager|viewer). Reach for this first when someone reports a project missing from their Projects list or cannot log time against it: an empty roster, or a roster without them in it, IS the explanation — visibility is membership. It is also the id source for projectMembers_update and projectMembers_delete. Note the same person can appear several times on one project, once per position.
project anyemployee anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
projectMembers_update
write
Change an existing membership's position by id (employee|tech-lead|account-manager|viewer) — get the id from projectMembers_list or the projectMembers array on projects_get. Use this to promote or demote IN PLACE; use projectMembers_create to add a second, additional position alongside the one already held. Changing a position can REVOKE sight of the project for someone whose role is project-scoped (a ROLE_PROJECTS_LEAD demoted from tech-lead to employee stops seeing it). Cannot move a membership to another person or project — delete and recreate for that. Requires ROLE_PROJECTS_MANAGER. Write.
position stringpositionName stringid* string | integer
projectTemplates_create
write
Create a reusable project blueprint from a structure document (version, project, phases, and their lists/tasks). Offsets inside it are RELATIVE — startOffsetDays and durationDays are counted in days from the startDate given at instantiate time, so one template serves every future start. The project.name in the structure is a placeholder; override it per customer when instantiating. The structure is validated server-side against the schema for its declared version, and a violation names the offending JSON pointer. Write.
name* stringstructure object- The blueprint document. Server-validated against the project-template JSON Schema for its declared version. Offsets are in days and resolve against the startDate passed to projectTemplates_instantiate.
projectTemplates_delete
write
Delete a project template by id. Soft delete, and it does NOT touch projects already created from the template — those are ordinary projects and live on. Write.
id* string | integer
projectTemplates_get
read
Get one project template by id, including its full structure document. projectTemplates_list finds the id. Read this before projectTemplates_update — the structure is written WHOLE, so an update must send the complete document, not a fragment. Read-only.
id* string | integer
projectTemplates_instantiate
read
Build a real project from a template — the project, its phases, its task lists and every task, in ONE atomic call. startDate is required and is the anchor every startOffsetDays in the template resolves against. Pass name to override the template's placeholder project name, and client to attach the new project to a customer: instantiating twice against the SAME client is how one customer ends up holding several engagements, each its own project. Returns the created project. Write.
startDate* string | nullname string | nullclient string | nullid* string | integer
projectTemplates_list
read
List the org's project templates — reusable blueprints of a project, its phases, its task lists and its tasks. Reach for this BEFORE projects_create when the same shape of project is set up repeatedly (an engagement type, an audit, an onboarding): instantiating a template builds the whole tree in one call, where projects_create makes an empty project you then have to fill by hand. The row flagged isDefault is the org's built-in template, applied to a project created with no template chosen. Read-only.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
projectTemplates_update
write
Update a project template by id. The structure column is stored and replaced WHOLE, never merged — send the complete document or the parts you omit are gone. Read the current one with projectTemplates_get first. Changing a template does NOT touch projects already instantiated from it; there is no back-propagation. Write.
name stringstructure object- The blueprint document. Server-validated against the project-template JSON Schema for its declared version. Offsets are in days and resolve against the startDate passed to projectTemplates_instantiate.
id* string | integer
resourceRequestCandidates_get
read
One recruitment candidate by id. The id comes from resourceRequestCandidates_list. Needs ROLE_HR_MANAGER. Read-only.
id* string | integer
resourceRequestCandidates_list
read
The candidates put forward against hiring requests — people in a recruitment pipeline, not employees available for allocation. Filter by the request id from resourceRequests_list. Needs ROLE_HR_MANAGER. Read-only.
resourceRequest anycandidate anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
resourceRequests_get
read
One hiring request by id, with its position and status. Get the id from resourceRequests_list. HR/recruitment, not resourcing allocation. Needs ROLE_HR_MANAGER. Read-only.
id* string | integer
resourceRequests_list
read
Open hiring requests — a request to recruit for a position, in the HR domain. Despite the name this is NOT resourcing allocation demand: it is recruitment. Returns the collection; resourceRequests_get reads one, and resourceRequestCandidates_list gives the people put forward for it. Needs ROLE_HR_MANAGER. Read-only.
status anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
resourcing_importTimeline
read
Import a resourcing allocation timeline sheet (fetch it via the Drive MCP, pass its CSV verbatim). This is a FULL-REPLACE mirror of the org's Allocation rows for `year`: rows in the sheet are created/updated, and any existing row for that year absent from the sheet is DELETED — not a merge. DRY-RUN BY DEFAULT: an omitted dryRun previews and writes nothing; pass dryRun:false to apply. The report gives `created` / `replaced` plus `unmatchedPeople` / `unmatchedProjects`. TWO THINGS ARE EASY TO MISS: a sheet row whose project does not resolve is SKIPPED while the call still reports success, so a green result can hide a partial import; and a role code the position catalogue does not already hold is CREATED as a new position rather than rejected — see `createdPositions`. Both are called out in `warnings` when they happen; surface that to the user rather than reporting only `created`. A sheet that parses to zero rows is refused (it looks exactly like a bad read about to wipe the whole timeline) unless you pass force:true. Read allocations_list afterwards to see what landed. High-impact. Write.
csv* string- The allocation-timeline sheet as CSV (fetch it via the Drive MCP). A role code the position catalogue does not already hold is CREATED as a new position rather than rejected — check `createdPositions` and `warnings` in the report.
dryRun boolean- Preview only; report what WOULD change and write nothing. Default true.
force boolean- Allow an apply whose parse produced zero rows (would otherwise refuse to wipe).
year number- Planning year the day columns fall in.
resourcingActuals_get
read
Reported hours vs the plan, per person per week, over a from/to window — the 'is the team actually on plan?' question, which NO other resourcing tool answers: allocations tell you what was PLANNED, this tells you what was DELIVERED. Returns week columns plus one row per person (planned %, reported %, variance, totals, and a per-project breakdown). reportedPercent null means 'no contract that week' and 0 means 'a contract existed and nothing was reported' — do NOT collapse the two. Pass financials for revenue/cost/margin, which are omitted otherwise. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only.
from* string- Window start, YYYY-MM-DD. Required.
to* string- Window end, YYYY-MM-DD, INCLUSIVE. Required. Must be on or after `from` (the backend 422s otherwise).
financials boolean- Include revenue / cost / margin per person. Omitted, those fields come back null and no financial pass runs — ask for it only when the question is about money. Asking does not guarantee an answer: the figures also require ROLE_RESOURCING_FINANCIALS_VIEWER, which ROLE_RESOURCING_MANAGER alone does not carry (per-person cost is derived from pay). Without it the call still succeeds — the money fields come back null exactly as if unasked, so treat null as withheld-or-unrated, never as zero.
Who is NOT staffed over a from/to window — the bench. Reach for it when asked who to put on a new project or where capacity is going unused; resourcingActuals_get tells you how loaded people are, this tells you who has no load at all. IT DOES NOT KNOW ABOUT LEAVE: freePercent is 100 minus confirmed allocations, nothing else, so someone on three weeks' approved holiday reads 100% free and no field on the response says otherwise. Answering 'who is available' from this alone will put people on projects while they are away — cross-check holidays_active or holidays_list. Needs the resourcing module. Read-only.
from* string- Window start, YYYY-MM-DD. Required.
to* string- Window end, YYYY-MM-DD, INCLUSIVE. Required. Must be on or after `from` (the backend 422s otherwise).
resourcingRequests_list
read
Open resourcing requests — someone asking for a person to be allocated to a project, which is the demand side of resourcing. This is the flow the Resourcing UI's Requests view renders. Do NOT confuse it with resourceRequests_list: that one is HR RECRUITMENT (hiring for a position). Pair it with resourcingRequestsHistory_list for what has already been decided, and resourcingBench_get for who could satisfy a request. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
resourcingRequestsHistory_list
read
What has already happened to resourcing requests — the decision trail (confirmed, declined, changed) behind the open requests in resourcingRequests_list. Reach for it to answer 'was this already asked for and turned down?' before proposing the same allocation again. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
resourcingSchedule_get
read
The planned resourcing schedule over a from/to window — the allocation timeline as the planner shows it. Use it for what is BOOKED going forward; use resourcingActuals_get for what was actually reported against it. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only.
from* string- Window start, YYYY-MM-DD. Required.
to* string- Window end, YYYY-MM-DD, INCLUSIVE. Required. Must be on or after `from` (the backend 422s otherwise).
responsibilities_create
write
Create a responsibility inside a group (responsibilityGroup = group id or IRI, + name, required; optional description; optional parent = another responsibility IRI for nesting). Assign people to it via responsibilityEmployees_create. Write.
parent anyresponsibilityGroup* stringname* stringdescription stringchildren array
responsibilities_get
read
Get one responsibility by id.
id* string | integer
responsibilities_list
read
List responsibilities inside a RACI group. Filter by responsibilityGroup. Responsibilities can nest via parent; people are assigned to them through responsibilityEmployees, not directly.
responsibilityGroup anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
responsibilities_update
write
Update a responsibility by id (name, description, parent, responsibilityGroup = group id or IRI). Write.
parent anyresponsibilityGroup stringname stringdescription stringchildren arrayid* string | integer
responsibilityEmployees_create
write
Assign an employee to a responsibility (responsibility = responsibility id or IRI, employee = employee id or IRI, percentage 0-100, all required; optional targets and description). Write.
responsibility* string | nullemployee* string | nullpercentage* integertargets stringdescription string
responsibilityEmployees_delete
write
Remove an employee's assignment from a responsibility by id. Write.
id* string | integer
responsibilityEmployees_get
read
Get one responsibility assignment by id.
id* string | integer
responsibilityEmployees_list
read
List who is assigned to which responsibility, and at what percentage. Filter by employee to read one person's entire RACI load across every group.
employee anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
responsibilityEmployees_update
write
Update a responsibility assignment by id (percentage, targets, description). Write.
responsibility string | nullemployee string | nullpercentage integertargets stringdescription stringid* string | integer
responsibilityGroups_create
write
Create a responsibility group / RACI area (name required; optional description and responsibleEmployee = the accountable person, given as a plain employee id like 6 (from people_list) or the /people/6 IRI). This is the top-level 'Odpowiedzialności' item. Add individual responsibilities under it via responsibilities_create. Write.
responsibleEmployee* string | nullname* stringdescription stringresponsibilities array
responsibilityGroups_get
read
Get one responsibility group by id.
id* string | integer
responsibilityGroups_list
read
List responsibility groups / RACI areas — the top-level "Odpowiedzialności" items, each with an accountable person. Individual responsibilities hang underneath them.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
responsibilityGroups_update
write
Update a responsibility group by id (name, description, responsibleEmployee = employee id or IRI). Write.
responsibleEmployee string | nullname stringdescription stringresponsibilities arrayid* string | integer
scheduleEmployees_get
read
One schedule-to-employee assignment by id. The id comes from scheduleEmployees_list. Needs ROLE_SCHEDULES_MANAGER. Read-only.
id* string | integer
scheduleEmployees_list
read
Which employees are assigned to which working-time schedules. Use it to go from a schedule (schedules_list) to its people, or to find the schedule a given employee follows. Needs ROLE_SCHEDULES_MANAGER. Read-only.
employee anydateFrom.before stringdateFrom.strictly_before stringdateFrom.after stringdateFrom.strictly_after stringdateTo.before stringdateTo.strictly_before stringdateTo.after stringdateTo.strictly_after stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
The schedules in force on ONE given date — pass the date in the path. Reach for it to answer 'who is working today / on this date' without reading every schedule and resolving its ranges yourself. Unlike the other schedule reads this only needs ROLE_USER, so it is the one available to an ordinary employee. Read-only.
current boolean- Filter only newest Schedules, without a successor
date* string | integercursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
One schedule time range by id. The id comes from scheduleRanges_list. Needs ROLE_SCHEDULES_MANAGER. Read-only.
id* string | integer
The time ranges that make up working-time schedules — the actual hours a schedule covers. Read the parent with schedules_get first; this expands its ranges. Needs ROLE_SCHEDULES_MANAGER. Read-only.
schedule anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
One working-time schedule by id, with its ranges and assigned employees. The id comes from schedules_list; scheduleRanges_list and scheduleEmployees_list read its parts. Needs ROLE_SCHEDULES_MANAGER. Read-only.
id* string | integer
Working-time schedules — the shift/working patterns an org defines, NOT project allocation. Use resourcingSchedule_get for who is booked on what; use this for the working patterns themselves. schedules_get reads one by id. Needs ROLE_SCHEDULES_MANAGER. Read-only.
current boolean- Filter only newest Schedules, without a successor
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Get one deal stage by id.
id* string | integer
List deal stages, in order. Filter by pipeline. deals_create requires a stage id from here, and moving a deal between stages is what dealStageHistories records.
pipeline anyorder.position stringcursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
storage_createUploadTicket
read
Mint a short-lived, single-use ticket for attaching a LARGE file to any record — the way asset PICTURES actually get in, since an image is always past the base64 ceiling. On property/location/invoices/transaction-attachments the newest upload becomes the record's visible image, replacing the previous one; on clients/agreements/candidates uploads accumulate. Use this instead of storage_upload whenever the file is more than a few tens of KB: that tool carries the bytes as base64, which a caller has to emit as text, and a 400 KB JPEG becomes ~533 K base64 characters, far beyond what fits in one response. Pass relationName + relationId plus a filename; you get back an uploadUrl and a ready-to-run curl. Then send the file's RAW BYTES to that URL (curl --data-binary @photo.jpg) — not base64, not multipart — and the response carries the created Storage record. The ticket expires in 15 minutes, works once, and can only file against the one record it names. Write.
relationName* string- Which kind of record the file is filed against. property = an asset (assets_list; the backend calls them properties, hence the singular name) — and this is what sets the asset's PICTURE; projects = a project (projects_list); project-tasks = a task (tasks_list); clients = a client (clients_list); contractors = a cost/contractor record; contracts = a contract — but prefer contractAttachments_create/createUploadTicket, which also clears contracts.problem_missing_document; location = a location (locations.*); employees / candidates / candidate-cvs / candidate-cover-letters = HR records; invoices / agreements / bank-account-statements / transaction-attachments / transaction-attachment-documents = finance documents.
relationId* string | number- The id of that record — as returned by its list tool. A full IRI (/assets/7, /projects/12) is accepted and reduced to the bare id.
filename* string- Filename to record on the upload, e.g. elewacja-frontowa.jpg.
contentType string- MIME type of the file. If omitted, the content-type header sent with the bytes is used — and it must still land inside this allowlist. image/webp and image/svg+xml are NOT accepted (the org logo route takes them, this one does not); convert to JPEG or PNG first. A refusal arrives only AFTER the bytes are sent, as a 422, with the ticket already consumed — so mint a fresh ticket to retry.
Attach a file to any record Flowtly's generic storage accepts — an ASSET (relationName "property"), a project, a task, a client, a location, a contractor, an invoice, an HR record. This is the only route to an asset PICTURE: uploading with relationName "property" sets the image the app shows for that asset (served as `file` on the asset payload). Property has no image column -- the picture is derived from this table at read time, which is why nothing on the entity hints it exists. It is ONE slot and the newest upload wins, so a second image replaces the first rather than adding to a gallery; both rows stay listed under /assets/{id}/documents. The same holds for location, invoices and transaction-attachments; clients, agreements and candidates instead accumulate every upload under `files`; the rest surface only on their /documents route. `file` is omitted from LIST responses unless the request passes ?include=file, so read one record back to confirm the picture landed. Pass relationName + relationId (the id from that record's list tool; a /assets/7 IRI is accepted and reduced) plus the bytes as base64 with a contentType and filename. SIZE LIMIT: the bytes travel as base64 inside this call, so keep it under roughly 150 KB — photographs are almost always past that, and for those use storage_createUploadTicket, which has no ceiling. Permissions are whatever editing the OWNING record needs: the backend resolves relationName to that entity and asks its own voter, so filing against an asset needs the assets permission, against a client the clients one. For a contract, prefer contractAttachments_create instead — it also clears contracts.problem_missing_document, which this does not. Write.
relationName* string- Which kind of record the file is filed against. property = an asset (assets_list; the backend calls them properties, hence the singular name) — and this is what sets the asset's PICTURE; projects = a project (projects_list); project-tasks = a task (tasks_list); clients = a client (clients_list); contractors = a cost/contractor record; contracts = a contract — but prefer contractAttachments_create/createUploadTicket, which also clears contracts.problem_missing_document; location = a location (locations.*); employees / candidates / candidate-cvs / candidate-cover-letters = HR records; invoices / agreements / bank-account-statements / transaction-attachments / transaction-attachment-documents = finance documents.
relationId* string | number- The id of that record — as returned by its list tool. A full IRI (/assets/7, /projects/12) is accepted and reduced to the bare id.
bytesBase64* string- The file's bytes, base64-encoded. Practical ceiling is about 150 KB of file: base64 inflates it by 4/3 and the result has to be emitted inside this call, so a larger file cannot be sent this way at all — no error arrives, the call simply cannot be written. Use storage_createUploadTicket for those, which sends the bytes straight from disk and has no such ceiling. Photographs are almost always past it.
contentType* string- MIME type of the bytes. The backend allowlist is exactly these — notably image/webp and image/svg+xml are NOT accepted here (the org logo route takes them, this one does not), and a refusal is a bare 422 "file: Please upload a valid file" that names no type. Convert a webp to JPEG or PNG before uploading.
filename* string- Filename to record on the upload, e.g. elewacja-frontowa.jpg.
tagDefinitions_create
write
Create a tag definition (name, level, tagGroup required) within a tag group. When the group's allowedRelations contains "project", each definition here IS a project folder — this is the tool that creates one. Write.
name* stringlevel* integerlabel stringtagGroup* string
List tag definitions — the tags that can be attached to records, each inside a tag group. tags_create takes a tagDefinition id from here plus the record to attach it to.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Create a tag group (name required) to organize related tag definitions. Also how you create a PROJECT FOLDER container: pass allowedRelations: ["project"] and the group's definitions become folders in the projects list. A group with empty allowedRelations is universal and is NOT treated as a folder. Write.
name* stringallowedRelations array | null
List tag groups — the containers that organize tag definitions.
cursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Attach a tag definition to a record (tagDefinition + relationName + relationId; e.g. relationName "counterparty" to tag a supplier/vendor). Use relationName "project" to PUT A PROJECT IN A FOLDER, where tagDefinition is the folder. Only root projects (no parent) are grouped into folders — phases follow their parent, so file the root and the tree follows. Write.
tagDefinition* stringrelationName stringrelationId* stringweight string
Create a tax rule. Write.
taxGroup* stringcountry* stringtaxAmount* string
List tax rules — the rates and the periods they apply to. Filter by taxGroup.
taxGroup anycursor string- Opaque pagination token from a previous response's nextCursor. Omit for the first page. When present, other filters are ignored — the cursor already encodes them.
itemsPerPage integer- Page size (max 100). Ignored when cursor is set.
Update a tax rule by id. Write.
taxGroup stringcountry stringtaxAmount stringid* string | integer