API changelog
Every change to the published contract, newest first. Entries are generated from the OpenAPI document itself rather than written by hand, so this page cannot drift from what the API actually serves.
Nothing here is a surprise: within a version we only ever add. Anything that would break an existing integration ships as a new version instead — see Versioning.
This page is English only. It is produced by a tool whose output is not available in French, and translating a generated diff by hand is exactly the kind of drift the rest of this documentation avoids.
Unreleased
Document
- every endpoint is now
GA: none isBetaany more, and none requires thebeta:accessscope. Each operation'sx-lifecycleextension moves frombetatoga, andbeta:accessleaves itsx-required-scopes. Nothing is removed or tightened: a call that worked still works, and a token that carriesbeta:accessis unaffected - the scope simply stops being needed. TheBetastage and the scope stay defined for endpoints we add later. - added the optional query parameter
establishmentto the eight listings that tookestablishmentId(employees, payslips, payroll closures, leave requests, time entries, variable payroll elements, expense report lines and meal voucher counts), and the optional propertyestablishmentto the body of the export creation. It narrows to ONE establishment of your perimeter by its name, compared without case or accents.establishmentIdstays and works as before; given together, both must designate the same establishment. Like the id, it only ever narrows: a name outside your perimeter, unknown, or carried by several of your establishments returns nothing - useestablishmentIdfor those. Only establishments that subscribe the employee module (GCRH) are found by name, whatever listing is filtered. - added the optional query parameter
typetoGET /employees: one employee type, or several separated by commas (Salaried,External,Interim; alsoSalariedToBe,PotentialSalaried), compared without case. Absent reads every type, as before; an unknown value returns nothing rather than being ignored. - the document now declares its
servers(staging, then production) and a document-level security requirement — thekeycloakOAuth2 scheme — instead of repeating that requirement on each operation. An API client that imports the contract (Postman, Bruno, Insomnia…) therefore sets the OAuth2 flow once on the collection, every request inherits it, and the base address becomes a variable. Nothing changes for a call: every operation that required the partner token still requires exactly the same token and the same scopes. A comparison tool will report the per-operationkeycloakrequirement of thirty-four operations as "removed" — it is now the document's, not the operation's. The one public operation (GET /marketplace/.well-known/jwks.json) declares an empty requirement, i.e. no authentication. - the scopes an operation requires moved from its
securityrequirement to thex-required-scopesextension (same names, same operations), and thekeycloakscheme no longer lists scopes: the full catalogue is the document-levelx-scopesextension. Deliberate — a client that pre-fills the token request with the listed scopes asks for all of them, and the authorization server refuses the whole request as soon as one is no longer granted to your application. Do not request any scope: those granted to your application are carried by your credentials (see Authentication). A generated SDK that read the scope list from the scheme should readx-scopesinstead. - the Marketplace string fields that take a closed set of values now list them in their
description:MarketplaceManagementEntityDto.type,MarketplaceModuleDto.planand thestatusof every Marketplace response.MarketplaceActivationDto.payrollProvidernow says what it carries. Deliberately a description and not anenum, so a value added later does not change the shape a generated client was built from. Documentary: no path, schema shape or status code changed. - API title changed from
DGD.Microservices.External.APItoStaff & Go API. The document previously carried the assembly name, which is an internal identifier; a generated SDK names its client after this title, so regenerate yours if it was derived from the old one. No endpoint, schema or scope is affected. - added a
summaryand adescriptionto the twenty-five operations that are also published as agent tools. The wording is the contract's own, not the one those tools carry: an agent is served a long briefing with no other context, where a reference page is read beside the schema and the examples, so the same text cannot serve both. Purely documentary: no path, parameter, schema, scope or status code changed, and nothing that reads this contract behaves differently. - added the optional property
partialto the eight listing responses, and the schemaPartialPerimeterV1it carries. Present only when the answer covers less of your perimeter than your credential was granted — a listing serves the establishments that hold the module it needs, and until now a short answer was indistinguishable from an empty one. Itsreasonssay why —module_not_subscribedorunspecified— and several can stand at once, so settling one leaves the others; it never names which establishment. Absent on a complete answer, so a response you read today is unchanged. See Discovery. - added the property
companyIdtoLeaveBalanceV1,CollectiveAgreementV1andLeaveTypeV1. A credential may be granted a perimeter of several establishments, and these three reads returned rows with nothing saying which one each came from — unusable for a firm whose establishments apply different collective agreements. The other reads already carried it. It is the same opaque company identifier/companiesserves, so it joins straight onto that read. LeaveTypeV1.companyIdisnullon a standard leave type — the key is always present, likename. Null means nobody defined the type: every establishment inherits the same entry and it is returned once. It is not an unknown establishment.CollectiveAgreementV1entries are no longer merged across your perimeter. Two establishments applying the same agreement to the same professional status now return two entries, each with its owncompanyIdand its ownisActive. They previously arrived as one entry whoseisActivewas true when either establishment still applied it, because nothing in the answer could tell them apart. What identifies an entry is now the triple (companyId,id,professionalStatusHiringCode) — it was the pair (id,professionalStatusHiringCode). A single-establishment credential sees no difference.POST /app/payroll/distribution/triggernow requires thecompanySiretyou name to be one your credential is mandated on. No schema moves — the field was already there and already required — but the behaviour does: a SIRET outside your perimeter answers not found, exactly as an unknown SIRET does. It is deliberately the same answer: to a caller that cannot reach a company, that company does not exist, and a distinct refusal would confirm that it does. Until now the target was resolved from the SIRET alone and no perimeter narrowed it.- the same endpoint now serves a credential mandated on several establishments, where it refused one outright. The SIRET is what says which establishment the distribution is for.
- added the optional query parameter
establishmentIdtoGET /leave-requests, the same parameterGET /employeesalready accepts. It narrows the page to one establishment of your perimeter; omitted, the page covers all of it, so a call you make today is unchanged. It narrows and never widens: an establishment outside your perimeter returns nothing rather than being reached, and so does a value that is not an identifier this API issued. - added the property
companyIdstoCompanyGroupingV1— a list, where the reads above gained a singlecompanyId. A grouping is a set companies belong to rather than something one of them owns, so it has no establishment of origin to name; it now names which establishments of your perimeter belong to it. It lists only the members your mandate covers, so the read still never discloses how large a grouping is: do not read the length of that list as the size of the grouping. - the OAuth2 flow now advertises
employments:read:compensationandleaves:read:reason, the two scopes that release fields — pay on an employment, the reason for an absence — rather than a read. Both were already required to obtain those fields and already grantable; they were simply absent from the list a generated client reads to know what it may ask for. Nothing changed about what a credential reaches: if yours holds them, you were already served those fields, and if it does not, the answer is unchanged. ⚠️ They are listed so you can discover what releases a field, not because you hold them — a field sub-scope is granted only on a stated need. Naming one you were not granted is refused asinvalid_scope, and the refusal takes the whole token request with it, so do not tick every scope offered by the API reference's authorize panel. See Authentication. - the OAuth2 flow now advertises
time-entries:read, and a newTime entriessection groups the read it opens. Advertised, not granted: a credential reaches the read only once the scope is issued to it. - the OAuth2 flow now advertises
payroll:read, and a newPayroll closuressection groups the read it opens. Advertised, not granted — and it is no substitute forpayroll:trigger, which stays the only scope that starts a distribution. - the OAuth2 flow now advertises
payroll:read:amounts, a third scope that releases a field rather than a read: the amount declared on a variable payroll element. Like the two above it was already grantable and already required to obtain that field; it is advertised now that a read serves it. Holding it is not enough, as for the two above: each company also opens the family to its partners, and neither of the two stands in for the other. A newVariable payroll elementssection groups that read. - the OAuth2 flow now advertises
expenses:readandmeal-vouchers:read, and two new sections —Expense reportsandMeal vouchers— group the reads they open. Advertised, not granted: a credential reaches either read only once the scope is issued to it, and neither implies the other. - the OAuth2 flow now advertises
payslips:read, and a newPayslipssection groups the two reads it opens. Advertised, not granted. It is not implied bypayroll:read, and does not imply it: closures say whether a payroll month is settled, payslips name the people in it. - the OAuth2 flow now advertises
exports:writeandexports:read, and a newExportssection groups the three operations they open. Advertised, not granted. They are the FIRST write scope of the read catalogue afterpayroll:trigger, and they are deliberately split:exports:writelets you ASK for a file,exports:readlets you follow it and fetch it. Neither implies the other, and neither releases a single row on its own — an export type inherits the scope of the read it extracts, soemployeesneedsemployees:readbesideexports:writeand is refused at creation without it. payslips:read:amountsis not advertised, and will not be. There is no amount anywhere on the payslip read — seeGET /payslipsbelow — so the scope releases no field. Nothing changes for a credential that already holds it.- corrected the operation descriptions — the text an agent reads before calling a tool, and the one printed
under each endpoint of the API reference. Four statements were wrong and are now right. A credential is
not limited to one establishment:
GET /companiesandGET /companies/{id}still said it was, while a mandate granted on a group unfolds into every establishment it reaches, so the listing pages through all of them and every id of that perimeter resolves. Thepartialblock has no single cause: it carriesreasons—module_not_subscribedorunspecified— and several can stand at once, so settling one leaves the others; the fourteen listings said it named "the reason".GET /payslipsgave three reasons a payslip is never served as though they were all of them: records that are archived or confidential are absent too, exactly as onGET /employees, so a count from that read is not a headcount. AndGET /leave-requestsandGET /employees/{id}/employmentsannounced their rows "oldest first" when the page is keyed on the identifier: the order is the one rows were recorded, not the order of their dates. A row takes that identifier when it is first entered and keeps it, so the order reflects neither the dates it carries nor when the request was submitted. ⚠️ The ordering itself has not changed, only the sentence that described it; sort onstartDateyourself if you need a chronology. Said where it was missing, too: that there is no delta feed and no tombstone behind these listings, and thatleaves:read:reasonreleases the reason onGET /leave-requestsexactly as on the single read. No path, parameter, schema, scope or status code moved. - the guides now state, once, that identifiers are opaque and not unique across types: one encoding serves the whole API and nothing in the string says what it designates, so the same value can resolve as a contract on one read and as a leave line on another. Nothing changed in the contract — this was always true — but an integration that keys identifiers in one shared table can silently cross two records. Key each one with the read it came from. See Discovery.
- three guides added, in French and English, for the reads that had none: Worked time,
Payroll and Expenses and meal vouchers. The endpoints were
already published and already documented endpoint by endpoint in the API reference; what was missing is
what only a guide can say — that
realMinutes: nullmeans a day was not declared rather than worked for zero minutes, that a closure'sreferenceMonthand its covered period are two different windows for a company paying in arrears, that an expense report has no total because its lines may mix currencies, and that the three payroll reads do not share a paging order. No path, parameter, schema, scope or status code is affected. - the
clientTokensecurity scheme now states the three header forms it is read in —BearerClient <token>,Bearer <token>, or the token on its own. It named only the first, which told a caller holding a working credential that it was sending it wrong. Documentary: no published operation requires this scheme, and the header has been read in all three forms since the same change.
GET /api/v1/client/companies
- endpoint added
GET /api/v1/client/companies/
- endpoint added
GET /api/v1/client/employees
- endpoint added
GET /api/v1/client/employees/
- endpoint added
GET /api/v1/client/employees/
- endpoint added
GET /api/v1/client/employees/
- endpoint added
GET /api/v1/client/employments/
- endpoint added
GET /api/v1/client/leave-requests
- endpoint added
GET /api/v1/client/leave-requests/
- endpoint added
GET /api/v1/client/referentials/collective-agreements
- endpoint added
GET /api/v1/client/referentials/company-groupings
- endpoint added
GET /api/v1/client/referentials/contract-types
- endpoint added
GET /api/v1/client/referentials/leave-types
- endpoint added
GET /api/v1/client/referentials/professional-statuses
- endpoint added
GET /api/v1/client/payroll-closures
- endpoint added — the payroll closures of your perimeter, one page at a time, MOST RECENT MONTH FIRST. A closure is the state of one payroll month for one establishment: which month, over which window, and how far it has gone. It carries no personal data at all — no employee, no amount, no comment — so it tells you whether a month is settled, never what is in it.
- ⚠️
referenceMonthand the period are two different windows.referenceMonthis the month the closure designates, normalised to its first day, so2026-04-01is April 2026, and together withcompanyIdit is what identifies the closure.periodFromandperiodToare the dates that closure actually covers, and they are not derivable from it: a company paying in arrears closes April over a window that starts in March. Key your records onreferenceMonth; read the period as the span. isClosedandisSynchronizedare independent. Closed means no more input is accepted; synchronized means the closure has been handed to the payroll software, so that month is definitive. A closed month can still change before it is synchronized — do not infer either flag from the other.fromandtomatch the reference month by containment, never the covered period. The page is ordered by that month, descending. Itscursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null; a short page is not the end.- Beta: requires
payroll:readandbeta:access. This read does not trigger anything:POST /app/payroll/distribution/triggeris a different operation, onpayroll:trigger, and is unchanged.
GET /api/v1/client/time-entries
- endpoint added — the working time of your perimeter, one page of DAYS at a time. Each entry is what was recorded for one date and one employment: minutes worked against minutes planned. Individual clock-in and clock-out events are a different kind of data and are not served here.
- ⚠️
realMinutesandtheoreticMinutesare nullable, and null means the day is NOT DECLARED — it does not mean zero minutes were worked. A consumer that computes pay from a null read as0pays nothing for a day that has simply not been declared yet, and nothing in the answer says so. The two totals go back to null if the period is reopened or refused. - ⚠️ Branch on
isDeclared, never onrealMinutes.isDeclaredis true only when both totals andsubmittedOnare set, and the converse does not hold: a day may carryrealMinutes: 420whilesubmittedOnis still null, and that day is not declared. That is the ordinary case, not an edge one — the totals are written as the day is filled in,submittedOnonly once the period is actually submitted. - the page is ordered by date. Its
cursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null; a short page is not the end. - Beta: requires
time-entries:readandbeta:access.
GET /api/v1/client/variable-payroll-elements
- endpoint added — the variable payroll elements of your perimeter, one page at a time, oldest period first. One entry is what was declared on one employee, for one nature, for one payroll period: a bonus, an overtime count, a meal allowance. It is the input a payroll run consumes, never its result.
- ⚠️
valueis meaningless withoutunitandnatureType. The same12is twelve hours, twelve days or twelve euros depending on the nature, so adding values across natures produces a number that is wrong and looks right. Group bynatureCode, read the unit, then read the number.natureTypesays whether the value is money (Amount) or a count (Quantity);unitnames the exact unit the company configured (H,J,EUR) and is null only when the nature records none. - ⚠️
valuesits behind two locks, and the scope is only the first.payroll:read:amountsis required in addition topayroll:read, which never implies it — and each company decides for itself whether it opens its variable pay to its partners. Granted the sub-scope, you still receivenullfor every row of a company that has not opened it, so one page legitimately serves the amount for some rows and withholds it for others. A withheldvaluecomes backnulland is named in therestrictedblock of that entry, with the reason:requires_scope(ask the client's administrator for the sub-scope),requires_company_opt_in(only that company can open it),requires_plan(your plan does not cover it) — several can stand at once and clearing one leaves the others. The employee, the nature, its payroll code, the period and the unit are served throughout: you learn that a bonus was declared in March, not how much.unitandnatureTypeare never withheld. Anullvaluethat is not named inrestrictedmeans no amount was recorded — a different fact. - the nature travels inline —
natureCode,natureName,natureType. There is no referential of element natures on this API, so map your own payroll headings onnatureCoderather than looking for a nature id to resolve. isPayrollExportablesays whether the nature is meant to reach the payroll software. Rows where it is false are served all the same: what to do with them is your decision rather than ours.employmentIdis the employee's current employment and may be null. An element hangs on the employee and the period, so it is not necessarily the employment in force during that period — and it changes between two calls, the day that employee signs a new contract. The element is a historical fact; that pointer is not. Do not diff on it.fromandtomatch the period by containment. The page is ordered by that period, ascending. Itscursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null; a short page is not the end.- Beta: requires
payroll:readandbeta:access.
GET /api/v1/client/expense-reports
- endpoint added — the expense report lines of your perimeter, one page at a time, oldest expense date first. One entry is one expense: one employee, one day, one nature, one amount.
- ⚠️ There is no report total on this API, and there cannot be one. The amount lives on the line together
with its own
currency, and one report may mix two of them — a taxi in euros and a hotel in pounds on the same claim — so any report-level total would be a sum across currencies: a number that is wrong and looks right. Group onrequestIdto regroup the lines of one report, then total them per currency yourself. - ⚠️
amountis meaningless withoutcurrency, and the two are always served together. Never add amounts without reading the currency, and never convert between currencies on your own — you do not know the rate your client books at. Nothing is withheld on this read: there is norestrictedblock at all, so a missing field would be a bug rather than a refusal. - no free text is served, by design. The report, the line, the payment and each status change all carry a comment in Staff & Go and none of them crosses: they are typed by employees, unreviewed, and may name a third party. No internal identity crosses either — who created the report, who paid it.
functionalNumberis the human-readable number of the report, the one printed on the screens your client reads: quote it back when you talk about a report, and userequestIdwhen you group.- the nature travels inline —
natureCode,natureName. There is no referential of expense natures on this API, so map your own expense accounts onnatureCoderather than looking for a nature id to resolve. expenseDateis the day the expense was incurred;createdOnis when the line was entered, which can be weeks later. Reconcile on the first, poll on the second.statusis the report's, repeated on every line because the lines of one report move together. Unsubmitted drafts are never served: a draft is an employee's unsent intention, so nothing this read returns is a claim that has not been filed.fromandtomatch the expense date by containment. The page is ordered by that date, ascending. Itscursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null; a short page is not the end.- Beta: requires
expenses:readandbeta:access.
GET /api/v1/client/meal-voucher-counts
- endpoint added — the meal voucher counts of your perimeter, one page at a time, oldest period first. One entry is one employee for one calculated period: how many vouchers they are owed, which is the number a voucher issuer orders on.
- ⚠️
statusis the employee's within the calculation, not the batch's. A calculation can be transmitted while one employee was aborted out of it, so order on the rows whose own status says they are settled —ValidatedorTransmitted— rather than on everything a period returns. vouchersCountis a result, not a working: Staff & Go computes it from four coefficients — the allowance, an adjustment, and what leaves and expenses took off — and none of them is served. There is nothing to recompute.periodFromandperiodToare the calculation's window, so every employee of one period carries the same one. They are not per-employee dates. The employee is named byemployeeIdonly — no name, no employee number, no job function.fromandtomatch the period by overlap, like/leave-requestsand unlike/time-entries: the period is a range, and a calculation running from the 26th to the 25th belongs to both months. Itscursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null.- Beta: requires
meal-vouchers:readandbeta:access.
GET /api/v1/client/payslips
- endpoint added — the payslips of your perimeter, one page at a time, most recent period first (like
/payroll-closures, and unlike the leave, expense and time listings which page forwards). One entry is one employee for one payroll period. - ⚠️ There is no amount on this read, and there cannot be one. No gross, no net, no net payable. The figures
exist inside Staff & Go but hang off no key to this record and are reconciled by a best-effort match that is
known to fail — a number served here would sometimes be another employee's. Read the document if you need the
figures. This is why
payslips:read:amountsis not part of the contract. - Nothing about the person crosses beyond
employeeId. The payroll metadata behind each line holds the employee's social security number, name, e-mail, mobile and job function; none of them is served, and none is withheld pending a scope — they are not served at all. There is norestrictedblock on this read. No internal identity crosses either (who distributed, who downloaded), nor the operator-facing error and warning texts of a distribution in progress. statusis one code per row, chosen by precedence, andNoPayslipcomes first: a row for which the payroll software produced no document carries it whatever else happened to it, sohasDocument: falsenever arrives alongside a distribution code. After it come the three that say the payslip reached the employee, narrowest first —PostalDistributed,ClientDistribution,Distributed— thenDownloaded, thenPendingDistribution. Staff & Go composes these from several flags that overlap; this read resolves the overlap once, the same way for everyone.- payslips in error, skipped, or superseded by a corrected one are never served. A missing employee for a period therefore means one of those, not an empty payroll — and in particular you will never be handed the OLD document of a payslip that was reissued.
periodNameis free text for display; branch onperiodFrom/periodTo, which are the payroll run's window and identical for every employee of that run.companyIdis the establishment the payslip was produced under, which is not necessarily where the employee works today.hasDocumentsays whetherGET /payslips/{id}/documentcan serve a file for the row. It is the same condition that route tests, so atrueis a promise it keeps.fromandtomatch the period start by containment: ask for a month and you get that month's run. Itscursoris opaque like every other one on this API — do not parse it, and stop only whennextCursoris null; a short page is not the end.- Beta: requires
payslips:readandbeta:access.
GET /api/v1/client/payslips/
- endpoint added — the payslip PDF itself, served as bytes under
application/pdf, with the file name inContent-Disposition. It is not JSON and there is no base64 envelope: read the response body as a file. There is no ranged or chunked form of this read. {id}is theidof a row fromGET /payslips. An id that does not exist, an id outside your perimeter, and a row with no document all answer 404, indistinguishably: the id designates the subject of the read, so a distinct refusal would confirm that a record you cannot reach exists.- it requires
payslips:read, the same scope as the listing, and no second one. A partner that may know a payslip exists for a named employee and a named month is a partner the client company has already trusted with that payslip. - a document beyond the size this API serves in one piece answers 413 with a Problem Details body rather than failing mid-transfer. A payslip weighs a few hundred kilobytes, so this is a guard against an anomaly and not a quota you can hit in normal use.
- when the payslip exists and is yours but its bytes cannot be fetched, the answer is 503 with a
Retry-Afterheader (in seconds). Retry after that delay: the row is there, and a 404 would have told you to stop asking for a payslip that is coming back. - ⚠️ the 503 does not say why, and cannot. Our document store reports a file being moved between storage tiers, a file that moved unexpectedly and a storage provider that is down with one and the same answer, so this status covers all three. Reserve, stated on purpose: if a stored file is ever lost for good — a data incident, not a normal state — this endpoint will keep answering 503 and your client will keep retrying indefinitely. Cap your own retries and raise a support ticket rather than looping forever. We accepted that trade deliberately: the alternative was to answer "this payslip does not exist" about one that does.
- ⚠️ this is the most sensitive response of this API. Everything the listing withholds — name, social security number, the amounts — is inside these bytes. It is audited, and it is deliberately not offered as an agent tool: an agent result is text it quotes and recopies, and this is a named payslip. Store it as a file and do not paste it into a model prompt.
- Beta: requires
payslips:readandbeta:access.
POST /api/v1/client/exports
- endpoint added — asks for a file of a whole domain, produced asynchronously. It is the answer to "give me the four thousand employees", which the paginated reads deliberately do not give: a client synchronising for the first time should not have to pay eighty calls and hold a cursor to obtain an initial state.
- it answers immediately with an export in status
Queuedand produces nothing yet. PollGET /exports/{id}until it readsReady, then fetchGET /exports/{id}/content. There is no push and no webhook for an export. typeis a closed catalogue, never a query you compose.employeesis the only value today: the rows ofGET /employees, the same fields, the same opaque ids, as CSV.establishmentIdnarrows the extraction to one establishment of your perimeter and, like everywhere else on this API, never widens it.- ⚠️ an export can never hand you more than the listing it extracts. The file is produced by walking that very read page by page — the same perimeter bound, the same excluded employees, the same withheld fields. Nothing is queried a second way, so nothing can diverge.
- a type whose read scope your credential does not hold is refused at creation, with
403, not hours later: you learn it on the call you made rather than from a job that fails overnight. - it requires an
Idempotency-Keyheader, like the payroll trigger. A replay of the same key and body hands back the export it already created instead of queuing a second walk of your perimeter. - ⚠️ it is metered on a bucket of its own, far tighter than your plan rate. Creating is the expensive act; following and downloading stay on the ordinary bucket, so polling is not what will throttle you.
- Beta: requires
exports:writeandbeta:access.
GET /api/v1/client/exports/
- endpoint added — where an export stands. It carries no personal data at all: a type, a state, two dates and two counts.
statusis one ofQueued(accepted, nothing read yet),Running(being extracted),Ready(the file can be downloaded),Failed(nothing will be produced) andExpired(produced, and purged since). ARunningexport interrupted by a restart returns toQueued; no other state ever goes backwards.- a
Failedexport always names its cause infailureReason.PerimeterEmpty— every establishment your mandate designates has closed since you asked, so there is nothing left to extract. You get this rather than an empty file on purpose: an empty file cannot be told from a company with no employees.ModuleNotSubscribed— your perimeter still resolves, but none of its establishments holds the module this type reads.TooLarge— see below.Fault— it failed on our side; ask again. - ⚠️ your perimeter is resolved when the file is PRODUCED, not when you ask. A mandate reduced in between
produces a file containing only what it still covers, and one that has emptied out fails with
PerimeterEmpty. Apartialblock, when present, says the file covers less of your perimeter than your credential was granted — the same block the listings carry, for the same reason. expiresOnis when the file is purged, seven days after it became ready. Past it the export answersExpiredrather than disappearing: "it existed and expired" and "it never existed" are different facts, and you can act on the first.rowCountandcontentLengthare kept after the purge.- an id that does not exist and an id belonging to another credential both answer 404, indistinguishably.
- Beta: requires
exports:readandbeta:access.
GET /api/v1/client/exports/
- endpoint added — the produced file itself, served as bytes under the type's media type (
text/csvforemployees), with the file name inContent-Disposition. It is not JSON and there is no base64 envelope. There is no ranged, chunked or resumable form of this read. - the CSV form, stated so you can parse it without guessing: comma separator,
CRLFline endings, UTF-8 without a byte-order mark, RFC 4180 quoting, one header line naming the fields exactly as the JSON read names them. Booleans aretrue/false, an absent value is an empty field, and the identifiers are the same opaque onesGET /employeesserves — so the two join. - ⚠️ your perimeter is re-checked on every download. A credential that no longer covers what the export was created for gets 404, the same answer as an unknown id — never a 403, which would confirm the export exists. Fetch your files while your mandate stands.
- an export whose content has been purged answers 410, not 404: it was there, and it is permanently not. Ask for a new one. An export that is not ready or that failed answers 409, naming its status.
- ⚠️ the size limit is functional, not a guard rail. An extraction whose file passes the size this API
serves in one response is refused rather than produced: the export comes back
FailedwithfailureReasonTooLarge, and this route then answers 413. The conduct is to ask again one establishment at a time —establishmentIdon the creation — or to page through the listing instead. - ⚠️ this file holds the personal data of everyone in your perimeter. Every call to it is audited, and it is deliberately not offered as an agent tool: an agent result is text it quotes and recopies. Store it as a file and do not paste it into a model prompt.
- Beta: requires
exports:readandbeta:access.
GET /api/v1/client/partner/profile
- removed the required
tenantIdresponse property for the response status200 - added the new required
tenantIdsresponse property for the response status200, an array of establishment identifiers. A partner credential is issued for a perimeter of establishments rather than for a single company, so the field that named one company now lists them. An account covering one establishment returns a single-element array.
GET /api/v2/client/partner/profile
- added the new
planresponse property for the response status200 - added the new
modulesresponse property for the response status200 - added the new optional
unavailableresponse property for the response status200 - removed the required
tenantIdresponse property for the response status200 - added the new required
tenantIdsresponse property for the response status200— same change as on v1 above, and for the same reason
POST /api/v1/client/app/payroll/distribution/trigger
- added the media type
application/jsonfor the response with the status200 - added the non-success response with the status
400
GET /api/v1/client/audit/entries
- endpoint added
- the security scope
audit-entries:readwas added to the endpoint's security schemekeycloak - the security scope
audit:readwas removed from the endpoint's security schemekeycloak
POST /api/v1/client/badge/custom
- added the media type
application/jsonfor the response with the status200 - added the non-success response with the status
400
POST /api/v1/client/badge/ubiqod
- added the media type
application/jsonfor the response with the status200 - added the non-success response with the status
400
POST /api/v2/client/app/payroll/distribution/trigger
- endpoint added
POST /api/v2/client/badge/custom
- endpoint added
POST /api/v2/client/badge/ubiqod
- endpoint added
POST /api/v1/marketplace/activation
- added the new required
headerrequest parameterX-Idempotency-Key. Documentary: a call without it was already refused with400MISSING_IDEMPOTENCY_KEY; the contract now says so.
POST /api/v1/marketplace/distribution/trigger
- added the new required
headerrequest parameterX-Idempotency-Key. Documentary: a call without it was already refused with400MISSING_IDEMPOTENCY_KEY; the contract now says so.
How this file is maintained
(Internal note — kept here so the procedure lives next to the file it governs.)
The CI job contract_external fails when openapi/v1.json changed in a merge request without this file
changing too, and prints the section to paste. To produce it yourself before pushing:
cd DGD.Sol/Microservices/External/DGD.Microservices.External.API
git show origin/staging:./openapi/v1.json > /tmp/spec-base.json
oasdiff changelog /tmp/spec-base.json openapi/v1.json --format markdown
Paste the ### sections under a new ## heading at the top — a version number once one is released,
Unreleased until then. Keep the newest first. Drop the # API Changelog … vs … line the tool emits: this
page already has a title.
The oasdiff version is pinned in .gitlab-ci.yml (OASDIFF_VERSION); use the same one locally so the
wording matches what CI would have produced.