AdministrationBrandingNew endpoints
White label: going back to the Flow brand, email history and profile photos
More control over your brand and your emails, following the white-label release of 9 October.What’s new- Branding in the Administration API:
DELETE /customer/brandinggoes back to the Flow brand. Your revisions stay, so publishing one again brings your brand back.DELETE /customer/branding/draftempties the draft so you can start again. What is live does not change.canChangeon the draft says whether you can change the brand. One-organisation administrators can view only.emailButtonShapeandemailHeaderStyleset how the buttons and the top of each email look.GET /organisations/brandingreturns the company logos of up to 100 organisations in one call.
- Profile photos: each person sets, reads and removes their own photo under
/users/me/photo. PNG, JPEG, GIF, WebP or SVG, up to 2 MB. Location and camera data are removed. - The Email Wording API:
- Each email shows its
standardwording, who receives it (audience), what it always includes (locked) and itsplaceholderswith sample values. POST .../rendered-previewsreturns the email exactly as it would be sent, as HTML and plain text.- Every save is kept as a version. List them with
GET .../revisionsand put one back withPOST .../revisions/{revisionId}/restorations. enabled: falseswitches an email off. Only the emails to administrators can be switched off.- The sign-in emails (invitation, email verification, password reset and security alert) are worded for your whole account.
- Each email shows its
New APIAdministrationBranding
White label: your brand, and the wording of your emails
Accounts with white label switched on can show their own product instead of Flow Payroll: in the app, in emails and on payslips. The brand and the wording of each email are now set over the API, the same way the administration screens set them.What’s new- Branding in the Administration API:
- A brand draft (
GETandPATCH /customer/branding/draft) that nobody else sees, changed with a JSON Merge Patch and guarded byversion. POST /customer/branding/revisionspublishes it, or publishes an older revision again withfromRevision.GET /customer/brandingreturns what is live andGET /customer/branding/revisionslists the history.- What it holds: product name, colours, font, logo, icon, help links and legal footer, whether the product name shows beside the logo in each place, and what the app shows while it loads (
loadingStyle:Spinner,IconorAnimation), with optional dark-mode versions of the logo, icon and animation. - File uploads in two steps:
POST /customer/branding/assets, thenGET /customer/branding/assets/{uploadId}until it isReady. - A company logo per organisation, through
/organisations/{organisationId}/branding, andGET /organisations/{organisationId}/resolved-brandingto see whose brand the app shows for an organisation.
- A brand draft (
- The Email Wording API: the subject, heading, paragraphs and button label of each email, for your account or for one organisation, with previews and test sends.
POST /payslip/samplesin the Pay API: a made-up payslip as a PDF in your brand draft, with unsaved changes if you send them. Nothing is stored or sent.
PATCH /customer/branding/draft and POST /customer/branding/revisions.Court ordersBehaviour changeDeprecations
Scottish and Northern Ireland court orders, and court or DWP variations
The Court orders API now takes four more order types, andPUT /orders/{orderId} can record a variation from the court or DWP through each type’s own fields.What’s new- Four new values of
orderTypeonPOST /orders, every response and thecourt_order.*webhook events:Cma, a current maintenance arrestment (CMA), Scotland. SenddailyRatein pounds per day.Cao, a conjoined arrestment order (CAO), Scotland. SendordinaryDebtAmount,maintenanceDailyRate, or both; at least one must be more than 0.NiFinesAeo, an attachment of earnings order (Northern Ireland) for fines. SendtotalAmountOwed, andagreedHigherRatePercentageif the employee agreed to pay more than the table.NiEjoAeo, an attachment of earnings order (Northern Ireland) from the Enforcement of Judgments Office. SendnormalDeductionRate,protectedEarningsRate,ratePeriod(Weekly,MonthlyorDaily) andtotalAmountOwed.
- Read-only pay days in the order’s
details:lastDeductionPayDateon aCmaorCao,lastPayDateon aNiEjoAeo, andfirstPayDateon aDea. - Two new
remittanceDeadlinevalues:AsSoonAsReasonablyPracticable(Cma) andAsTheOrderRequires(CaoandNiEjoAeo). - Variations on
PUT /orders/{orderId}:rateType(DEA),dailyRate(CMA),ordinaryDebtAmountandmaintenanceDailyRate(CAO),agreedHigherRatePercentage(Northern Ireland fines),normalDeductionRate,protectedEarningsRateandratePeriod(Northern Ireland EJO), anddeductionBasis(AEOs). Only the fields you send change. A field that belongs to another order type is refused with a400. - Reference data:
GET /orders/constants/{taxYear}addscma.protectedEarningsPerDay, the Northern Ireland fines tables inniFinesAeo, and a dailytableCfor AEOs and earnings arrestments.
- An AEO for maintenance or a civil debt is created with the court’s rates: the normal deduction rate (NDR) and protected earnings rate (PER). On
POST /orderswithorderTypeAeoNonPriority, orAeoPrioritywithaeoCategoryPriorityMaintenance, senddeductionBasisFixedAmountwith the normal deduction rate infixedAmount, andprotectedEarningsSettingCourtPerAmountwith the court’s protected earnings rate.TableorPercentageis now refused with a400(Attachment of Earnings Act 1971 s6(5)). Fines orders keepTable. An order created before this change keeps its method until an update changes it. - Orders that depend on the employee are checked. Only one
Cmacan run for an employee at a time, and aCaoneeds the employee’sEaandCmaorders ended first. Each is refused with a400. instructionAmountis not accepted for aCma,Cao,NiFinesAeoorNiEjoAeo, which always use their own rates.- Orders end on their end date. An order whose
endDatehas passed with no pay period left before it is nowDischargedautomatically, and thecourt_order.status_changedevent gives the reasonend_date_reached. - Payslip line names. A court order deduction on the payslip, and its admin fee line, is now named with the order’s abbreviation and reference, such as
DEO - AB/123456,AEO priority - AB/123456orCMA - AB/123456. Lines from earlier pay runs keep their text, so don’t parse the order type or reference from a description; read the line’scourtOrderIdandcourtOrderTypeinstead.
percentageRate stops being accepted as input on 4 January 2027percentageRateon a deduction from earnings order (CmsDeo) request, onPOST /ordersandPUT /orders/{orderId}, is an alias ofpercentage. It is read only whenpercentageis not sent. It stops being accepted as input on 4 January 2027.details.percentageRateon aCmsDeoresponse keeps being returned, equal topercentage.
percentage instead of percentageRate before 4 January 2027. If you create AEOs for maintenance or a civil debt, send the court’s normal deduction rate and protected earnings rate as described above. A client that treats an unknown orderType or remittanceDeadline as an error should accept the new values. See Deprecations.PensionsBehaviour change
Bring your own pension provider
A pension scheme can now be run on your own terms. Flow Payroll still assesses, enrols and calculates every contribution, and your integration decides how those contributions reach the provider: in a file you build, through the provider’s own API, or by whatever route it gives you. Nothing in the pay run waits on a file from us.What’s new- A
Manualprovider inGET /pension-providerin the Pensions API. Create a scheme against it for a provider you file with yourself. The scheme is assessed, enrolled and calculated like any other, every contribution is on the payslip, and no export file is produced for it. exportSupportedon every provider fromGET /pension-provider,GET /pension-provider/{id}andGET /pension-provider/slug/{slug}, and on the provider embedded in each scheme fromGET /pension-schema/organisation. It isfalseforManualandtruefor every other provider, so your integration can tell the schemes it handles from the ones Flow Payroll files for you.- Contributions to build your own file from. Once a pay run completes, read each employee’s contributions with
GET /pension-calculation/pension-provider/{pensionProviderId}/by-year/{year}/by-month/{month}, or every provider’s at once withGET /pension-calculation/by-year/{year}/by-month/{month}. Thepayroll_run.completedwebhook tells you when a period’s figures are final.
Manual, do not wait on GET /export-file-retrieval/pension-calculation/... or GET /pension-calculation/export/...: they never list a file for it. Read the calculation endpoints instead. A client that treats a missing exportSupported as true keeps working against an environment that has not yet picked the field up.Recurring line items
Retry creating a recurring deduction without creating it twice
If a request to set up a recurring deduction or payment times out, you can now send it again safely. Add anIdempotency-Key header and a retry gives you back the first answer instead of setting up a second schedule for the same employee.What’s newIdempotency-Keyheader onPOST /in the Recurring line items API: a unique value you choose, such as a UUID, up to 255 characters.- Sending the same key with the same body within 24 hours returns the first response, and nothing is created again.
- The same key with a different body, or from a different user, is refused with a
409and the tokenidempotency_key_reused. A retry that arrives while the first request is still running gets a409withidempotent_request_in_progress; wait a moment and send it again.
HMRCPay runsDeprecations
Tell HMRC why an FPS is late
An FPS that reaches HMRC after the employees’ payment date must give a late reporting reason, or HMRC may send a penalty warning. You can now send that reason with the FPS through the Pay API, for a submission or a preview.What’s newlateReasonon the body ofPOST /rti/fps/{payrollConfigId}/{taxYear}/{period}. Send one of HMRC’s current reasons by name:NotionalPaymentToExpartByThirdPartyOrOverseasEmployer(A),NotionalPaymentEmploymentRelatedSecurity(B),NotionalPaymentOther(C),PaymentSubjectToClass1NicButP11dP9dForTax(D),NoRequirementToMaintainADeductionsWorkingSheetOrImpossibleToReportWorkDoneOnTheDay(F),ReasonableExcuse(G) orCorrectionToEarlierSubmission(H). HMRC’s single code letter works too. The reason is reported against every employee in the submission. Leave it out when you are reporting on time.- Code E, which HMRC withdrew on 6 April 2016, and any other value are refused with a
400and the tokenhmrcRtiFpsInvalidLateReason. lateReasononhmrcDetails.fullPaymentSubmissionReportDetailsof a pay run shows the reason the FPS was sent with, or the one a ready preview was built with. It isnullwhen none was given.
- The FPS request body is now an object with two optional fields,
finalSubmissionDetailsandlateReason. The final submission details, needed for the last period of the tax year, go insidefinalSubmissionDetails.
- Sending the final submission details object as the whole body (
{ "forYear": true, ... }) still works, and is read asfinalSubmissionDetailswith no late reason. It stops being accepted as input on 15 January 2027. Nothing in the response changes.
finalSubmissionDetails. Add lateReason whenever an FPS goes after the payment date. See Deprecations.Webhooks
Bulk P45 and P60 emails now tell you who was missed, and why
When you email P45s or P60s to a group of employees, the event that says the send has finished now names the employees who did not get their form and the reason. You can fix their records and send again without calling back to look the job up.What’s new onp_form_bulk_send.completed and p_form_bulk_send.failed:failures: up to 100 employees who were not sent their form, ordered by employee id, each with areason.failuresTruncated:truewhen more than 100 employees were missed. Fetch the full list fromjobUrl.failureCountsByReason: every failure in the send, counted by reason, for example{"EmployeeEmailMissing": 12}. This is never cut short.
reason is always one of a fixed set of codes, such as EmployeeEmailMissing, EmployeeBirthDateUnparseable, RecentlySent or ConnectionError, so you can match on it exactly. It never contains employee details or error text. GET /pforms/send-notification/jobs/{jobId} uses the same codes.What you need to doNothing. The fields are added to the existing events; read them when you want to follow up on missed employees.Webhooks
Webhooks for court orders, journals, pensions, recurring deductions, HMRC notices, salary settings, connections, billing and payments
Your integration can now react to much more of the payroll without polling: a court order deduction taken, a journal posted to your accounting system, an employee enrolled into a pension, a new tax code notice from HMRC, a payment file ready for the bank, and more. Add the new event types to an endpoint to start receiving them. Nothing changes for the events you already receive.New families- Court orders (
court_order.*): an order added to an employee, changed, removed or changing status, and each deduction taken or skipped in a pay run. - Journals (
journal.*): a pay run’s journal locked, corrected, posted to your accounting system or refused by it, and the accounting entry for a replaced version reversed. - Pensions (
pension.*): each step of automatic enrolment (assessed, enrolled, opted out, ceased, postponed, exempt), the enrolment letter sent or not, and each submission to the pension provider: contributions, new members and payment approval. - Recurring deductions and payments (
recurring_line_item.*): an ongoing schedule or a loan or advance set up, changed, paused, resumed, stopped, finished or removed, and what each payslip took from it. - HMRC notices (
hmrc_notice.*andhmrc_notice_job.*): a tax code or student loan notice received, matched to an employee, applied, dismissed or reopened, and each check for notices or bulk apply or dismiss finishing, failing or being cancelled. - Salary settings (
salary_organisation_config.*,salary_employee_config.*,salary_organisation_statutory_leave_policy.*andsalary_employee_statutory_leave_policy.*): how salary is worked out for part periods, and how it is handled during statutory leave, for the organisation or for one employee. - Connections (
integration_connection.*): a connection to your accounting system or pension provider ready, disconnected, needing you to sign in again, or removed. - Billing your clients (
invoice.*,credit_note.*andbilling_client.*): for a bureau, the invoices and credit notes it raises to its clients, and the clients themselves. - Payments (
payment_file.*andpayment_settings.*): a pay run’s payment file ready or failed, and the bank details and settings it is made from added, changed or removed.
payslip.delivered,payslip.bouncedandpayslip.complainedtell you whether the recipient’s mail server accepted the email, rejected it, or the recipient marked it as spam. They usually arrive minutes or hours after the email was sent. The same three events exist forp45,p60,pension_enrolment_letterandreport.report.emailedandreport.email_failedtell you whether a scheduled report’s email was sent. For pension enrolment letters, keep usingpension.enrolment_letter_sentandpension.enrolment_letter_failed.
- The Webhooks API spec now lists
nullas an allowed value on fields that accept it, such aspayloadStyleonPOST /endpoints. If you generated a client from the previous spec and it refusesnull, generate it again.
POST /endpoints/{endpointId}/event-types, as described in Event types. A family covers the event types that share its name up to the dot, so hmrc_notice.* does not include hmrc_notice_job.*.BillingDeprecations
Billing responses move to the shared envelope; the old fields are deprecated
The Billing API now answers with the same envelope as every other Flow Payroll service, described in Response format. Nothing has been taken away: every field billing sent before is still sent, with the value it sent before, so an integration that reads the old fields keeps working unchanged.What’s newmessage.tokenon every response, a stable snake_case key for the message (clients_listed,not_found,bad_request), withmessage.tokenArgumentswhere a message has values to fill in. On an unexpected failure that includes therequestIdto quote to support, the same id as theX-Request-Idresponse header.validationIssueson every error, each withfield,reasonandreasonToken.content.metadatawithdateFormatanddateTimeFormat, andpaginationTokenon every paginated list, including the lists read from your accounting software. A list’s rows stay where they are incontent.data.
- A
pageSizeof 0 or less is refused with a400namingpageSize. It used to fail with a500. - Creating a credit note without a
clientIdis refused with a400namingclientId. It used to answer404for a client with an empty id. - The request id on a failure now matches the
X-Request-Idresponse header, so quoting either one to support finds the request.
errorsrepeatsvalidationIssues:reasonTextisreason,reasonIdisreasonToken,reasonParametersisreasonTokenArguments.- The top-level
metadatarepeatscontent.metadata. message.textIdandmessage.textParameterskeep their old values, which do not always matchmessage.tokenandmessage.tokenArguments. Match ontoken.- On a paginated list,
paginationToken,hasMorePagesandpageSizeinsidecontent.datago. Read the cursor fromcontent.metadata.paginationTokenand keep reading until there is none. On the contacts, tax rates and accounts read from your accounting software,nextPageTokengoes the same way; thepageTokenquery parameter is not changing.
ApiResponse_ClientDto becomes ClientDtoResponseBody, and so on). The old names stay in the spec as deprecated aliases of the new ones, so a client generated from the previous spec still compiles when you regenerate it.See Deprecations.PensionsDeprecations
Pension responses move to the shared envelope; the old fields are deprecated
The Pensions API now answers with the same envelope as every other Flow Payroll service, described in Response format. Nothing has been taken away: every field pension sent before is still sent, carrying the same values as its replacement, so an integration that reads the old fields keeps working unchanged.What’s newmessage.tokenon every response, a stable snake_case key for the message (pension_scheme_created,not_found), withmessage.tokenArgumentswhere a message has values to fill in. On a500that includes therequestIdto quote to support.validationIssueson every error, each withfield,reasonandreasonToken.content.metadatawithdateFormatanddateTimeFormat, andpaginationTokenon every paginated list (eight endpoints). It is the same cursor the list already carried insidedata. A list’s rows stay incontent.data.items.
errorsrepeatsvalidationIssues:reasonTextisreason,reasonIdisreasonToken,reasonParametersisreasonTokenArguments. It is now on every error response, including the ones that used to answer without it.- The top-level
metadatarepeatscontent.metadata. message.textIdandmessage.textParametersrepeatmessage.tokenandmessage.tokenArguments.- On a paginated list,
content.data.paginationTokenrepeatscontent.metadata.paginationToken. Pass either back as thepaginationTokenquery parameter until then.
PensionSchemeV2ApiResponse becomes PensionSchemeV2ResponseBody, and so on). The old names stay in the spec as deprecated aliases of the new ones, so a client generated from the previous spec still compiles when you regenerate it.See Deprecations.ReportsWebhooksDeprecations
Report exports announce themselves by webhook; the schedule’s own webhookUrl is deprecated
Every report export now sends a webhook event when it finishes, whether a schedule started it or you queued it with POST /exports. Subscribe an endpoint to the new report_export.* family and you no longer poll GET /exports/{id} or give each schedule its own URL.What’s new- Two event types in a new Reports family:
report_export.completedandreport_export.failed. Each carries therunId,mode(EXPORTorSCHEDULED),definitionId,scheduleId,format,statusandrowCount, and aresourceUrlpointing atGET /exports/{runId}, which answers your authenticated request with a fresh download link. The event never carries the link itself. - Delivered like every other event: signed, retried for about three days, logged, replayable. See Delivery and retries.
delivery.webhookUrlonPOST /report-definitions/{definitionId}/schedules, and thewebhookSecretit returned once at creation, are deprecated. A schedule created with awebhookUrlkeeps receiving the legacyX-Flow-SignaturePOST until 30 December 2026; on that date the field stops being accepted as input and stops being returned, and the legacy POST stops. Subscribe an endpoint toreport_export.completedinstead and route onscheduleId. See Deprecations.
report_export.* to its event types. Email delivery on schedules is unchanged.WebhooksNew endpoints
Webhooks: be told the moment something happens
The new Webhooks API sends an HTTPSPOST to your server when something happens in Flow Payroll, so your integration reacts at once instead of polling. It follows the open Standard Webhooks specification, so official libraries verify every delivery in one line.What’s new- 62 event types across 11 families: pay runs, payslips, pay run line items, employees, leavers, HMRC RTI, P45s and P60s, statutory pay, holiday and leave, payslip and P-form emails, and operational events about your own endpoints. Each has a reference page with its payload. See Event types.
- Endpoints at
/v1/webhooks/endpoints: subscribe to exact types, whole families (payslip.*) or everything (*), and chooseFullorMinimalpayloads, strict ordering, a rate limit and custom headers. See Manage endpoints. - Signed deliveries with
webhook-id,webhook-timestampandwebhook-signature. Rotating a secret keeps the old one signing for 24 hours, so you switch with no missed deliveries. See Verify signatures. - Automatic retries for just over three days (10 attempts over about 75.6 hours) on a fixed schedule,
Retry-Afterhonoured, and a delivery log of every attempt. See Delivery and retries. - Resend and replay: send one delivery again, or replay a window of up to 90 days after an outage. See Replay and recovery.
- Account-wide endpoints for bureaus: one URL for every client organisation, optionally filtered. See Account-wide endpoints.
- Test events on demand, answered while you wait. See Testing webhooks.
Pay runsNew endpoint
Download a payslip as a PDF
The Pay API now serves the payslip document itself, the same PDF an employee receives by email. Until now the only way to get a payslip out of the API was the JSON figures.New endpointGET /payslip/data/{employeeId}/{year}/{period}/filereturnsapplication/pdf. SendAccept: application/pdf; the body is the file, not the usual JSON envelope.Content-Dispositioncarries the file name,Payslip-{year}-{period}-{LastName}-{FirstName}.pdf.- Preview payslips can be downloaded before the run is locked, and a download always reflects the payslip’s current figures.
- The file is not password-protected. Only the copy emailed to the employee is, with their date of birth as
ddMMyyyy. - A period with no payslip answers
404with the tokenpayslipNotFound.
SalaryNew endpoints
Statutory leave on the Salary API
The Salary API now decides what happens to a salaried employee’s pay on the days they are paid statutory pay. It takes those days back with an adjustment line and, if the policy says so, adds a top-up.New endpointsGET,PUTandDELETE /organisation/leaveand the same three on/employees/{employeeId}/leave. The document hassickness,familyLeave, andbyTypefor an exception for one family-leave payment. Each policy is:DeductAbsentDays(the default), with optionaltopUp.tiers; orFullPay, which tops every day of the leave up to 100%.
- Every response carries
effective: the policy each of the seven statutory payments resolves to, with itssource(Employee,OrganisationorDefault). - A tier’s length is a
duration,{ "value": 6, "unit": "Week" }.Weekis the only unit today; months can be added later as anotherunitvalue without changing the shape. The tiers must end within 52 weeks of the start of the leave.
- Adjustment lines have ids starting
internal_salary_adjustment_and are posted againstinternal_salary_adjustment_sicknessorinternal_salary_adjustment_family_leave. - Top-up lines start
internal_salary_topup_and are posted againstinternal_salary_topup_sicknessorinternal_salary_topup_family_leave. - Both are posted with
canEdit: falseand follow the statutory line they respond to.
POST /employees/{employeeId}/calculate takes optional statutoryLines to preview the adjustment and top-up. Each returned line now carries:kind:Salary,StatutoryAbsenceAdjustmentorStatutoryTopUp;payElementId;statutoryTypeon an adjustment or top-up.
PensionNew endpoints
Send pension letters to many members at once
POST /pension/employee-pension/letters/{letterType} queues one kind of letter for a list of members and answers 202 with a job to watch. letterType is Enrolment today; it is a path segment rather than a field because each letter is its own collection, so the notices added later will not change the shape of this call.Nothing has been sent when it responds. The letter still has a queue, the notification service and an email provider to cross, which is why the response is a job rather than a result:GET /pension/employee-pension/letters/jobs/{jobId} is how far it has got: status (Queued, InProgress, Completed, Failed), totalCount, sentCount, failedCount, and failures[] naming each member who was missed with a stable reason (AlreadySending, NotEnrolled, SendFailed, UnsupportedLetterType). A member who is no longer enrolled is reported and skipped; the rest of the batch still goes.sentCount means on its way, never delivered. A member stays on the outstanding list until the provider confirms delivery, and only then does their opt-out period begin.GET /pension/auto-enrolment/outstanding-enrolment-information gains totalCount, the number of members owed a letter across every page, and lastAttemptStatus on each row (Sending, Sent, Failed) so a letter already on its way can be told from one nobody has started.Repeated employee ids in one request are collapsed, so asking twice still posts one letter.ReportsBehaviour change
Synchronous runs are now in GET /runs, and schedules report their last delivery
Every run is recorded. Yesterday’s note said a SYNC run returns its rows and is not recorded in the run history. That is no longer true: POST /reports/run and POST /report-definitions/{id}/run now write a meta_report_run row, and the runId in the response is one you can quote. If you built around synchronous runs being absent, GET /runs will return considerably more than it used to.GET /runs?mode=SYNC|EXPORT|SCHEDULEDnarrows the list to one kind. Worth adding if you poll for exports: synchronous rows are far more numerous and would otherwise push an export you are waiting on off the first page.limitdefaults to 50 and is clamped to 200.requestedByis now populated on synchronous and export runs, carrying the kind of principal with the identity —USER#…for a person,M2M#…for an API client,schedulerfor a scheduled run. It stays null on runs made before this shipped.- Retention:
SYNCrows are kept 90 days.EXPORTandSCHEDULEDrows are kept indefinitely. - A definition that fails validation (
400) or is refused by the concurrency cap (429) is not recorded — neither reached your data. A run that times out (408) is.
GET /report-definitions/{definitionId}/schedules returns four new fields on each schedule: lastDeliveryStatus (DELIVERED or FAILED), lastDeliveryAt, lastDeliveryError, and consecutiveDeliveryFailures, which resets to 0 on the first success. Poll these instead of waiting to hear that a report stopped arriving. A run that never produced a file records a FAILED delivery too, so a schedule cannot show a stale success while its reports have been failing.All new fields are optional and everything else is unchanged, so existing integrations keep working.ReportsBehaviour changeDocumentation
A concurrency limit on synchronous report runs, and two corrections to these docs
429 on synchronous runs. One organisation may now have four synchronous report runs in flight at once. A fifth returns 429 from POST /reports/run and POST /report-definitions/{id}/run. The limit is per organisation rather than global, so a busy tenant cannot slow anyone else’s reads. Queue an export instead of retrying in a loop: exports are not limited this way.Two things these docs had wrong, both worth checking against your integration:- Export download URLs last 12 hours, not 15 minutes. The reference said 15 minutes. The service has issued 12-hour URLs throughout, so anything built to refetch every 15 minutes was refetching far more often than it needed to.
- Synchronous runs are not recorded in
GET /runs. The reference said every run lands in the history.EXPORTandSCHEDULEDruns do; aSYNCrun returns its rows in the response and is not recorded. If you relied on run history to audit interactive queries, it has not been capturing them.
429.PensionsNew endpointsBehaviour change
See who is still owed an enrolment letter, keep opt-out forms, and set your own re-enrolment policy
Three additions and two behaviour changes across automatic enrolment. If you read pension data through the API, the behaviour changes are the ones to look at.New endpointsGET /auto-enrolment/enrolment-information/outstanding— every member who has not been given their enrolment information, worst first:Failed(the send did not arrive, so it was never given),Overdue(the six weeks have run out),Due(still in time). Each row carries the employee id, the date they joined the pension, thedueBydate and, where relevant, what went wrong — join toGET /employeesfor names, as you would for any other list. Paginated like every other list in this API:pageSize(1 to 100, default 50) andpaginationToken, answering{ items, paginationToken }. Follow the token until it is absent rather than treating a short page as the last one. Takes an optionalasOfDateso you can ask the question for a date other than today. An organisation that has setemployerIssuesEnrolmentInformationgets an empty list: the duty is discharged outside Flow, so Flow asserts nothing about it.POST /employee-pension/{employeeId}/opt-out-notices— returns a short-lived URL to upload the signed opt-out form to, plus the date it must be kept until. The document goes straight to storage and does not pass through the API, so a scanned form is not limited by request size. Send the returnedcontentTypeas theContent-Typeof yourPUT, or the URL will refuse it.GET /employee-pension/{employeeId}/opt-out-notices?membershipEndDate=— the forms kept for one membership ending, newest first, each with a short-lived URL to read it. These outlive the membership: deleting a member’s pension record does not remove their opt-out forms, which is the point of keeping them.
-
lastEmailedStatuson an enrolment letter can now beSending. It is published as an enum (Sending,Sent,Failed) rather than a free string, so a generated client gives you the union.Sendingmeans a send was asked for and has not come back yet: treat it as not yet sent. It never means the information was given — onlyenrolmentInformationSentDatemeans that, and the member’s opt-out period counts from that alone. -
Asking to send a letter while one is in flight now answers
409. The member would otherwise receive a statutory letter twice, which cannot be undone. A send that never resolves is treated as lost after 15 minutes and can be retried. -
Enrolling someone without giving a date now uses the next pay reference period.
POST /employee-pensionwith noenrolmentDatepreviously enrolled them from the day the call was made, part way through a period. It now uses the first day of the next pay reference period. If you want a different date — because your payroll has already closed for that period, or you are back-dating — sendenrolmentDateexplicitly and it is used unchanged. - Contributions are no longer produced for a period that ends before the enrolment date. Previously a forward-dated enrolment could still attract a deduction for a period the member was not yet in. If a period ends before the enrolment date no contribution line is created, and one already created is removed.
PUT /auto-enrolment-settingstakesreEnrolRecentOptOuts. At your three-yearly re-enrolment date you may choose whether to re-enrol people who opted out, or left the scheme at their own request, in the preceding 12 months. Flow used to always leave them out, and still does unless you set this totrue— nothing changes for anyone who does not set it. Reading the organisation settings always returns the field, so you can see which way it is set.
PensionsBehaviour change
The enrolment information declaration sent to your provider now reflects what happened
NEST’s enrolment file carries a field confirming the worker has been given accurate information about their enrolment and their new pension rights, which their schema describes as required by law and set out in the NEST employer terms. Flow was filling it in as “yes” on every submission, for every employer, whether or not anything had been sent.What changed- The declaration is now made per member, from evidence: an enrolment letter Flow sent and the email provider accepted, or a standing declaration from you that you issue the information yourself. With neither, the field is left empty, which is what the provider’s own schema expects for “not confirmed” (the value
Nis not permitted and never was). PUT /auto-enrolment-settingstakesemployerIssuesEnrolmentInformation: set it when you give workers the information by post or your own systems rather than relying on Flow’s email. Who set it and when are recorded and returned asenrolmentInformationDeclaredByandenrolmentInformationDeclaredDate; they are stamped by the service and are not accepted from the request.- A letter that bounced is not evidence: the information was not given, so no declaration is made for that member.
employerIssuesEnrolmentInformation to true once. If you rely on Flow’s enrolment letters, turn them on with sendEnrolmentLetterOnAutoEnrolment and no further action is needed. Until one of those is true, submissions will leave the declaration empty rather than assert something we cannot evidence.PensionsBehaviour change
Opt-out refunds are now checked against the statutory opt-out period
A jobholder may opt out within one month, and that period runs from the later of becoming an active member and being given their enrolment information (The Pensions Regulator’s detailed guidance 7, paragraphs 17 to 18). Outside it a defined-contribution scheme owes nothing back: contributions stay invested and the member becomes deferred. Until nowDELETE /employee-pension/{employeeId} refunded whenever processRefund=true was sent, with nothing checking the date.What changedDELETE /employee-pension/{employeeId}now refuses an out-of-period refund with a400unless the newrefundOutsideOptOutPeriodReasonquery parameter says why. The employer may still choose to refund; the parameter records that it was a deliberate decision. Supplying it when the date is inside the period is also refused, so a reason never ends up on a refund that did not need one.GET /employee-pension/{employeeId}/refund-previewand the removal response now returnenrolmentDate,optOutPeriodEndDateandwithinOptOutPeriod, so you can show the decision and its dates before acting.withinOptOutPeriodisnullwhen the period could not be worked out, which is not the same asfalse.GET /pension-information/{employeeId}now carriesenrolmentInformationSentDate,enrolmentInformationSentByand anenrolmentLetterobject (lastEmailedAt,lastEmailedTo,lastEmailedBy,lastEmailedStatus,lastEmailError) so you can tell whether a member has been sent their enrolment letter, and what happened to it. The field names match the payslip and P-form email metadata.- Membership endings keep the evidence.
enrolmentInformationSentDate,optOutPeriodEndDate,withinOptOutPeriodandrefundOutsideOptOutPeriodReasonare recorded on the leaver record, because the membership rows they were assessed from are deleted by the removal. PUT /auto-enrolment-settingstakessendEnrolmentLetterOnAutoEnrolment, which emails the enrolment letter when Flow enrols somebody automatically and records the date it went out. Off by default.
withinOptOutPeriod. If it is false and you still intend to refund, send refundOutsideOptOutPeriodReason. The date is indicative where we have no record of when the enrolment letter was given: it is counted from the enrolment date, which is the earliest the period can close, so it may run later in practice.PensionsNew endpoint
Employee-level auto-enrolment exemptions
Until now the only way to keep one worker out of automatic enrolment was to switch auto-enrolment off for the whole organisation. A new per-employee exemption covers The Pensions Regulator’s exceptions to the employer duties — directors without a contract of employment, office holders, LLP members, workers with HMRC tax protection, and the other discretionary cases — without touching anyone else’s assessment.What’s newPUT /auto-enrolment-exemptionmarks an employee exempt, with areasonfrom a closed set (Director,OfficeHolder,LlpMember,TaxProtection,CeasedMembershipWithinTwelveMonths,WindingUpLumpSum,NoticeOfTermination) and an optional free-textnotefor evidence such as which HMRC protection certificate was seen. One exemption per employee — submitting again replaces it. A reason outside the set is refused with a 400 listing the choices.GET /auto-enrolment-exemption/{employeeId}reads it back, andDELETEon the same path removes it, resuming assessment on the next pay run with the determination history untouched.GET /pension-information/{employeeId}now carries the exemption alongside the enrolment, determination, and postponement, so one read still paints the whole picture.- While exempt, an employee is never assessed or automatically enrolled — including at cyclical re-enrolment — and drops out of
GET /auto-enrolment/not-complete, the listing the pay run’s enrolment alerts are built from. - The exemption removes the employer’s duty, not the worker’s right to join:
POST /employee-pensionkeeps working for an exempt employee, exactly as it does for anyone else.
PUT /auto-enrolment-exemption with the employee, a reason and an optional note. The endpoint reference under Pensions → Auto enrolment documents all three operations and the reason vocabulary.EmployeesDeprecationsNew endpointSalary
Salary becomes a history, and a Salary API pro-rates it onto the pay run
An employee’s salary used to be a single number on their record. It could say what they are paid, but not what they were paid, so a rise part-way through a month was either applied early, applied late, or worked out by hand. Salary is now an effective-dated history on the employee, and a new Salary API turns that history into pay-run lines, pro-rated to the days somebody was actually employed in the period.What’s new- Salary history on the employee, in the Pay API:
GET /employees/{employeeId}/salaryreturns every version oldest first with the one in force today;PUT /employees/{employeeId}/salaryrecords a version from a date;DELETE /employees/{employeeId}/salary/{effectiveFrom}removes one. Sending the sameeffectiveFromagain replaces that version rather than appending another, so retries and corrections are safe. The last remaining version cannot be deleted — set its amount to 0 instead, which states that no salary is paid. salaryHistoryon every employee response, read-only, carrying the same versions and the one in force.salarykeeps its meaning: the annual amount in force today.- Employees whose salary predates effective dating need no migration. They read back as a single version effective from their start date, so nothing has to special-case them.
- A corrected start date carries the starting salary with it.
PUT /employees/{employeeId}/starter-detailsmoves the earliest salary version when it sits on the old start date, alongside the New Starter NI category and initial tax code. A move that would land on or after the next version answers400with tokensalaryVersionBlocksStartDate; tidy the history first. - A Salary API at
/v1/salary:GET,PUTandDELETE /organisationset how the organisation pro-rates salary and whether the service runs at all; the same three verbs on/employees/{employeeId}override the method for one person; andPOST /employees/{employeeId}/calculateis a dry run that returns the lines a period would produce without posting anything.
salary on PUT /employees/{employeeId} and PATCH /employees/{employeeId} is deprecated as input and stops being accepted on those two operations on 20 December 2026. It keeps being returned on every employee response, as the annual salary in force today, so only what you send changes. POST /employees is unaffected: a salary sent at creation is still the starting salary, and becomes the first version effective from the start date. While it is still accepted on PUT and PATCH, a changed value updates the most recent salary version in place rather than adding a new one, so it cannot express a change from a date — that is what PUT /employees/{employeeId}/salary is for. See Deprecations.How the pay run is worked out- A full period pays a fraction of the year: the annual salary divided by the number of periods in the year (12 monthly, 52 weekly, and so on), as one line.
- A part period is paid per day. The qualifying days in the period are multiplied by the annual salary divided by the qualifying days in the tax year containing them, so both sides of the division count the same kind of day. Which days qualify is the calculation method:
CalendarDayscounts every day and divides by 365 or 366;WorkingDayscounts only the days the employee works, taken from their working pattern, and divides by that employee’s working days across the tax year — so a three-day week divides by roughly 156, not 260. - The method resolves employee, then organisation, then a default of calendar days, and every response that resolves it says which level answered.
- A joiner, a leaver or a mid-period salary change is pro-rated automatically. A change part-way through the period produces two lines, the old salary up to the day before it takes effect and the new one from it, each naming its dates in the description.
- Salary lines are posted with
canEdit: false, like statutory payment lines, so a later salary change or a corrected record always reaches the open pay run. Adjustments are separate manual lines. - Payslip events now carry a
featuresattribute with asalarytoken when the employee has a salary in force for the period, so a consumer can filter on it without reading the employee record.
PUT /employees/{employeeId}/salary, then check an awkward period with POST /employees/{employeeId}/calculate before you open the pay run. The product walkthrough is Salary. Salary lines are posted only for organisations where the module is enabled; it is being turned on organisation by organisation.IntegrationsBillingPayroll journals
Accounting joins the Integrations API, and billing drops its own
The Integrations API now covers accounting providers as well as pension ones, and billing’s embedded integration endpoints are gone. If you were driving billing’s own OAuth or connection routes, they have moved.Removed from the Billing APIEvery/integrations route: GET /integrations, GET|DELETE /integrations/{provider}, /connect, /callback, /exchange, /credentials, /setup-info, /tenant, /tenants and /custom-connection/test-and-save. Connections are the Integrations API’s job for every service, so there is one place to hold credentials and one record of what was sent.New in the Integrations API- Connecting a provider that signs in on its own website:
POST /connections/{connectionId}/authorisereturns the URL to send someone to,POST /connections/{connectionId}/exchangefinishes it, andGET /connections/{connectionId}/tenantswithPUT /connections/{connectionId}/tenantchoose which of the provider’s own organisations to write to. - Reads for mapping screens, under
/connections/{connectionId}/accounting/:accounts,tax-rates,tracking-categories,contacts, and per documentdocuments/{documentType}/{externalId}/pdfand/attachment-count.typeandstatuson an account come back normalised —Revenue,Expense,Asset,Liability,EquityandActive,Archived,Deleted— so the same code reads the same way whichever accounting system answered. - The provider catalogue now declares what a provider will hold:
supportsDraftJournals,maxAttachmentsPerDocumentandmaxContactPersons. Read them rather than hardcoding a limit, so a second accounting provider needs no change in your client.
POST /journals/{journalId}/postingsposts a finalised journal through the organisation’s accounting connection and answers202with the operation to follow. Track it onGET /operationsin the Integrations API.
IntegrationsPensions
A new Integrations API, and pay runs you can send to NEST
Flow Payroll can now send a pay run’s pension data straight to the provider instead of producing files for you to upload. Two things arrived to make that work: an Integrations API that holds provider connections and the record of everything sent or read, and a Provider submissions group in the Pensions API that sends a pay run, approves the payment and reads back what the provider reported. NEST is the first and, for now, the only provider.What’s new- Integrations API at
/v1/integrations:GET /providersfor the catalogue;POST /connections,PUT /connections/{connectionId}/credentialsandPOST /connections/{connectionId}/testto connect an employer’s NEST account;PUT /bindings/pension/pension-scheme/{schemeId}to link a scheme, which is the opt-in;GET /operationsto follow every submission and every read withretryandcancel;GETandPOST /connections/{connectionId}/importsto see how fresh the provider’s data is and refresh it; andPOST /connections/{connectionId}/sync-sessionsto import what the provider already holds. - Pensions API, with the pay run addressed in the path as
/provider-sync/{payrollConfigId}/{year}/{frequency}/{period}:/sendablesays which schemes can be sent and why not;/sendsubmits enrolments or the contribution schedule; the collection root follows them per scheme with status, member count, total and the provider’s schedule reference;/approve-paymentapproves collection;/issuesreturns every reason a provider refused a submission;/refresh-membersre-reads named members from their employee records so a refused detail can be corrected without re-running the pay run. Outside a pay run,GET /provider-sync/returnslists what providers sent back,GET /provider-sync/reconciliationcompares a scheme with its provider, and/provider-sync/member-changesis the queue of reported opt-outs and stopped contributions withapply,dismiss,acknowledgeanddelete. contributionDueDateOffsetDayson a pension scheme, for the rare case where a provider has published no schedule to read the due date from./refresh-membersnow rebuilds the pay run’s export files from the corrected details and reportsexportFilesRebuilt. The files are rendered from the stored calculations, so a correction that stopped at the record left the download showing the old values, which matters most to a scheme delivering by file rather than through a connection. False means the details are corrected but the file is still the old render.- Every provider submission record carries
createdByandcreatedDate(who sent it, and when) alongsideupdatedByandupdatedDate(who last changed it). Nothing is sent to a provider on its own, socreatedByis the person who submitted it;updatedByreadsSystemonce the provider’s own answer has landed, which is how you tell a person’s action apart from the provider’s reply. Records written before this shipped reportSystemfor both, because the name was never captured. - A published vocabulary of message tokens (
provider_validation_failed,connection_circuit_open,pension_missing_payment_sourceand the rest), each with default English you can show as-is. Branch on the token, never the text.
- Nothing sends on its own, and nothing a provider reports is applied on its own. Completing a pay run produces the export files and stops. A submission happens only on
/send, payment approval only on/approve-payment, and a reported opt-out changes nobody’s pension until/member-changes/{changeId}/apply. - Retries are automatic and bounded. A temporary failure is retried up to five times over roughly eight hours, then parked as
NeedsAttention, visible and retryable, never dropped. Failures a retry cannot fix (rejected sign-in, locked account, rejected content) park at once. Five failures in a row pause a connection for ten minutes; a provider that has not answered in two days is flagged. - Sending twice is safe. Every submission carries an idempotency key, and the Pensions API derives its key from the pay run’s content, so an unchanged run is deduplicated and a corrected one goes as a new submission.
- Provider data is kept fresh. Opt-outs and stopped contributions are read within six hours of a pay run opening, again at lock, and daily; setup, schedules and refunds weekly and after every submission. Reading never changes payroll: what comes back is queued on
/provider-sync/member-changes, and applying one ends the membership through the same flow asDELETE /employee-pension/{employeeId}, with no payroll refund. - The contribution due date comes from the provider. It is read from the provider’s own published schedules and matched on the earnings period, so sending late does not change it. If it cannot be determined, the submission is refused with
pension_unknown_contribution_due_daterather than sent with a guess the provider would reject. - A refusal names who it refused. Each issue on
/issuescarries the record it relates to, so/refresh-memberscan re-read exactly those people. Amounts on a locked pay run are never touched; only the details a provider validates are rewritten. - The full rules are on the Integrations API overview.
POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/send. The product walkthrough is sending pension data to NEST.PensionsPay runs
A payslip now says why a pension deduction was taken gross
A member with no National Insurance number has the full pension contribution deducted, not the 80% net share, because HMRC needs that number for a relief-at-source scheme to reclaim basic-rate relief. Their deduction is therefore higher than an identical colleague’s, while the same amount reaches their pot. That is correct, but until now nothing in the API said so, and the larger figure read like a mistake.What’s newpensionMetadata.reliefAtSourceAppliedon a payslip summary item:falsewhen the full gross was deducted,truewhen relief was netted off.pensionMetadata.reliefAtSourceWithheldReason, currentlyMissingNationalInsuranceNumber, saying why.
- Both fields are
nullwhere relief at source does not apply (net pay arrangement, salary sacrifice) and on payslips stored before the fields existed, so absence is not “relief was applied”. - Nothing about the calculation changed. These fields explain an amount that was already correct.
- Adding the member’s NI number restores relief on future pay runs. It does not change contributions already sent to the pension provider, and it is not pushed to the provider automatically, so update the member there too if relief is to be reclaimed on earlier contributions.
DeprecationsPensions
Every deprecated field on one page, with removal dates
Deprecated fields used to be visible only on the endpoint page that happened to render them — so a field you send on employee migration, or read back on a pension group, could be marked deprecated without you ever passing that page. Deprecations now lists every deprecated field in the API, what to use instead, and the date it is removed.What’s new- A Deprecations page in the sidebar, covering every service in this reference. It is generated from the same OpenAPI specs the reference is built from, so a field cannot be deprecated in a spec without appearing there.
- A stated policy: a deprecated field keeps working with its existing behaviour — in requests and responses alike — until its removal date, and every field gets at least 90 days between deprecation and removal. If we need to move a date, we move it later, never earlier.
- Each field says what removal takes away. Most of ours are deprecated as input only and keep being returned afterwards, so only what you send has to change — the page marks those Input only — still returned, and marks the fields that stop coming back Responses.
- Removal dates are now published for every deprecated field. The Pay API opening-balance fields deprecated on 9 August — the flat NI totals on
openingBalanceDirectorand the aggregatepensiononopeningBalance— stop being accepted as input on 7 November 2026. SendniBandResultandpensionContributionsByTypeinstead; both fields keep being returned on responses, as the sum across letters and scheme types respectively. - The five pension fields deprecated yesterday are removed on 7 December 2026.
DeprecationsPensions
Five deprecated pension fields
Some of the fields on a pension group and on an employee’s enrolment describe how we store the record rather than what the employee’s pension actually is. Five of them are now deprecated in favour of the fields that answer the question directly —pensionType for the arrangement, pensionGroup for the group in force, and isAutoEnrolled for how the employee got there.What’s newisSalarySacrificeis deprecated onPOST /pension-groupandPUT /pension-group/{id}— sendpensionTypeinstead. The flag is read only whenpensionTypeis omitted, and it cannot expressNetPayArrangement, so it can only ever describe two of the three arrangements.isSalarySacrificeis deprecated on the pension group you read back. It is derived — true only whenpensionTypeisSalarySacrifice— so readpensionType, which also tellsReliefAtSourceandNetPayArrangementapart.organisationIdis deprecated on a pension group. A group always belongs to the organisation in your access token, so the field never told you anything your own request did not already state.autoEnrolmentPensionGroupandmanualEnrolmentPensionGroupare deprecated on an employee’s enrolment, returned byGET /employee-pension/{employeeId},GET /employee-pension/batch, andGET /pension-information/{employeeId}. ReadpensionGroupfor the group in force andisAutoEnrolledfor how the employee came to be enrolled — one field for the answer, one for the route to it, instead of inferring both from which of two fields is populated.- All five keep working, with their existing behaviour, until 7 December 2026. Unlike the Pay API opening-balance deprecations, four of these are response fields that stop being returned on that date — only
isSalarySacrificeon a create or update is an input change. Read Deprecations for which is which.
pensionType on every pension group create and update, and switch your reads to pensionType, pensionGroup, and isAutoEnrolled. The Pensions API overview describes the group and enrolment shapes, and Deprecations lists each field with the endpoints that carry it.PensionsBehaviour changeNew endpoint
Itemised pension contributions on payslips
Pension used to reach a payslip as a single total. It now reaches it as one line per contribution, which is what makes voluntary contributions, single-run overrides and opt-out refunds through payroll possible at all. Nothing was removed: the payslip schema gained four fields, no endpoint changed shape, andforPeriod.pension still carries the same total it always did.There is one thing to check in your integration.What’s newpensionLineTypeon every payslip summary item classifies pension rows asContribution,RecurringAdditionalVoluntaryContribution,AdditionalVoluntaryContributionorOptOutRefund, and isnullon everything else. Branch on it rather than parsing line ids or reading an organisation setting: the presence of a contribution row is what tells you which source to total.pensionEmployerContributiongives the employer contribution attributable to a row. It is already insideforPeriod.employerPension, so it is display metadata and never additive.pensionMetadatarecords the context a row calculated under: the pension group, the contribution basis, the employer share, and acontributionSourcesaying whether the settings came from the group, a standing per-employee override, or an override for that pay run only.pensionRefundsByTypeon the period, year-to-date and employment-to-date totals breaks refunds down per scheme type. A refund is money going back to the member, so it is a payable row with a positive amount, andforPeriod.pensionhas it netted off already.- Seven new endpoints under
/v2/pension:GET,PUTandDELETEfor additional voluntary contributions and for per-period contribution overrides, both keyed by employee, tax year and period;GET /employee-pension/{employeeId}/refund-preview; andGET /employee-pension/{employeeId}/membership-endsplusGET /organisation/pension-membership-endsfor the refunds report. DELETE /employee-pension/{employeeId}gainedprocessRefundandreason, andoptOutDatenow means what it says: supplying it records an opt-out rather than a bare removal, and it is refused if it precedes the enrolment date. A removal with neither is recorded as an administrative correction. Preview the refund first, then passprocessRefund=trueto pay it on the next payslip.
New APIAdministration
Administration API
You can now manage your Flow Payroll account over the API, not just the payroll inside it. The new Administration service covers what used to need a support request or a trip to the admin screens: adding organisations, inviting people and deciding which organisations they reach, and issuing, rotating and revoking the API tokens your integrations authenticate with — plus a feed of who changed what.What’s new- A new Administration API group in the sidebar, served under
/v1/administration. It is scoped to your account rather than to one organisation, so it needs noX-Org-Idheader. GET /customerandPATCH /customerto read and rename your account, andPUT /customer/ownerto transfer its ownership. Each account has one owner, whose access cannot be revoked from inside it.- Organisation endpoints: list with search and headcounts, get one in full, add one with
POST /organisations, or add several withPOST /organisations/bulkusing shared defaults and per-entry overrides. Both creates honour anIdempotency-Keyheader. - People endpoints: list who can sign in, invite one person or many, resend an invitation, remove somebody (owner only), and manage each person’s access — direct grants on named organisations and account-wide access — through
/users/{principalId}/organisationsand/users/{principalId}/customer-access. - Guests: invite somebody from another business into specific organisations with
asGuest: true. Guest people and guest organisations appear in the lists with anaccessTypeofGuest, and theaccessTypequery parameter filters either list to one kind. - API token endpoints: create a credential and receive its secret once, rename, rotate, revoke, and manage its access with the same two sub-resources as people. Self-service credentials replace asking support for a
client_id/client_secretpair. GET /activityfor the last 90 days of administration actions, newest first.- Two kinds of access, both reported on every read:
organisationIdsfor direct grants andcoversWholeCustomerfor account-wide access, on people and tokens alike. Only somebody who holds account-wide access can grant it. - Every refusal carries a stable snake_case reason token in
message.token(user_limit_reached,token_limit_reached,duplicate_token_name,idempotency_key_reused,billing_not_set_up, and so on), and bulk endpoints report the same tokens per entry.
GET /v1/administration/customer with your existing token to confirm which account it belongs to, then browse the Administration API group in the sidebar. The Administration API overview explains the two kinds of access, the limits (500 people and 5 API tokens per account), idempotency and pagination.PensionsNew endpoint
Auto-enrolment assessment preview
The Pensions API can now classify a worker for auto-enrolment from nothing but the details you send it.POST /auto-enrolment/assessments/preview (under /v2/pension) takes a date of birth, earnings with their frequency, and the pay reference period dates, and returns the worker category — EligibleJobholder, NonEligibleJobholder, EntitledWorker, or NotEligible — together with what follows from it. Nothing is read from your employee records, nothing is stored, and no enrolment is triggered.We built this with agents in mind. Auto-enrolment categorisation is deterministic statutory logic, and it belongs in the API rather than in a model’s head: an AI assistant answering “would this new starter be auto-enrolled?” or “why can this employee only opt in?” can call the endpoint and ground its answer in the same rules engine that assesses real pay runs, instead of approximating the thresholds itself.What’s new- A stateless preview endpoint in the Pensions group: send
dateOfBirth,earnings,payReferencePeriodStartDate, andpayReferencePeriodEndDate; get back the worker category and its consequences —autoEnrolled,canOptIn, andemployerPaysIn. - The response also carries the worker’s age at assessment, whether they are under State Pension age, their State Pension age date, and the effective assessment date, so a caller can explain the outcome, not just state it.
- Classification follows The Pensions Regulator’s categorisation exactly: the 16–74 age bounds, the State Pension age to 74 opt-in band, and strict earnings thresholds pro-rated per pay frequency.
- Invalid input — an unknown frequency, negative earnings, a period end before its start, or a non-ISO date — returns a 400 that names the problem, so agents and integrations can self-correct.
EmployeesNew endpoint
Update an employee with PATCH
Integrations that only need to change one thing about an employee no longer have to send the whole record.PATCH /employees/{employeeId} accepts a JSON Merge Patch: send the fields you want to change, and everything you leave out keeps its current value. PUT is unchanged, so existing integrations keep working exactly as they do today.What’s new- Partial updates on the employee record — send
{"salary": 42000}and nothing else moves. - Merge semantics follow RFC 7386: omitted fields keep their current values, an explicit
nullclears an optional field, nested objects merge recursively, and arrays replace wholesale. - One call covers the whole updatable surface — personal details, tax codes, NI details, student loans, bank accounts, director details and more — instead of a sequence of dedicated endpoints.
- Fields with their own guarded endpoints (starter details, leaver details, payroll ID, payroll configuration, opening balances) are rejected with a 400 rather than silently ignored, so a mistyped field can never touch HMRC-relevant state.
- Required fields (name, email, birth gender, birth date, hours worked category) cannot be cleared.
PUT; if you read and write individual fields, PATCH is the call to build on.EmployeesDirectors
Split opening balances for directors
Opening balances now describe a director’s year in the detail HMRC needs, so you can migrate a director to Flow Payroll part-way through a tax year — including the awkward cases, where someone is a director for only part of the year or changes NI category letter mid-year.What’s newopeningBalanceDirector.{year}gainsniBandResult, a map keyed by NI category letter carrying the NI-able gross and contributions accrued while the director basis applied, with optional band splits.- Earnings are now split by where they were assessed:
openingBalancecovers the entire employment,openingBalanceDirectorcovers the director stretch from appointment onwards. Each keeps its own band splits. - The flat totals on
openingBalanceDirector(grossForEmployeeNi,grossForEmployerNi,ni,employerNi) are deprecated as input — they cannot express more than one NI letter in a year. They still work, and we still return them as the sum across letters. - Opening balance updates return specific validation issues (unknown NI letter, negative values, band splits exceeding the letter’s gross) instead of one generic message.
niBandResult on PUT /employees/{employeeId}/opening-balance rather than the flat totals. For a director on the annual basis appointed before the tax year began, leave bandResults out of openingBalance — the annualised calculation derives the band split itself. For the alternative basis, include bandResults inside each openingBalanceDirector letter. See the Pay API overview for the full request shape.New APIReportsBeta
Reports API (Beta)
You can now query your payroll data programmatically. The new Reports service takes a single JSON report definition — dataset, columns, filters, grouping, aggregation, sorting — and runs it synchronously, exports it at scale, or delivers it on a schedule. It ships in beta: build against it, and expect refinements before general availability.What’s new- A new Reports group in the sidebar (badged Beta), served under
/v1/report. - Dataset discovery endpoints: list the queryable datasets and inspect each field catalog — types, filterability, groupability, aggregations, and sensitivity.
POST /reports/runfor synchronous runs of inline definitions (10,000-row cap), plus saved report definitions you can re-run by ID.- Asynchronous exports to CSV or JSONL for up to 5,000,000 rows, downloaded via presigned URLs.
- Cron schedules on saved definitions, delivering exports by HMAC-signed webhook or email.
- A run history endpoint that doubles as an audit trail, recording access to sensitive fields.
GET /v1/report/datasets to see what you can query, then iterate on a definition with POST /v1/report/reports/run. The Reports API overview walks through the definition document, relative-period tokens, and the export and scheduling flows.New APIPayroll journals
Payroll journals API
You can now drive payroll journals entirely over the API. A new Payroll journals service turns every finalised pay run into a balanced double-entry journal — mapped to your chart of accounts, split by department or cost centre, and exportable to Xero or any other accounting system — without touching the Flow Payroll UI.What’s new- A new Payroll journals group in the sidebar, served under
/v1/journal. - Template endpoints to define how journals are built: create, clone, and update templates, manage pay-element and control-account mappings, configure split dimensions, and import mappings from systems such as Sage, Xero, Staffology, and IRIS.
- Preview endpoints to render a template against a pay run before you rely on it, including CSV and PDF preview exports.
- Journal endpoints to list journals, fetch the latest version for a pay run, download exports (Xero Manual Journal CSV, generic CSV, or PDF) via presigned URLs, and rebuild or reissue a journal after corrections.
- Organisation-wide defaults via
GET/PUT /org-journal-settings, plus export-option lookups for presets, date formats, and narration and reference tokens.
POST /v1/journal/templates, then browse the Payroll journals group in the sidebar for the full surface. For how templates, mappings, and splits behave in the product, read the Payroll journals guide.Authentication
One set of credentials, many organisations
Your API credentials are no longer tied to a single organisation. The platform now separates who you are from which organisation you’re acting on: your token (sent asX-Auth-Token) proves your identity exactly as before, and the new X-Org-Id request header states which organisation the request applies to. One client_id/client_secret pair can now be granted access to many organisations — useful for bureaus, accountants, and group companies.What’s new- A new
X-Org-Idrequest header, accepted on every endpoint, naming the organisation the request is for. Switching organisation is just sending a different value on the next request — the same token stays valid across all your organisations, with no re-authentication. - A new Authentication API group in the sidebar. Call
GET /v1/authentication/my-organisationsto list every organisation your credentials can act on — it works before an organisation is selected, so you can use it to build an organisation picker. - Nothing breaks for existing integrations: if your credentials access exactly one organisation, requests without
X-Org-Idresolve to it automatically. With access to more than one, omitting the header falls back to your configured default organisation; with no default set, the request is rejected with403so you choose explicitly.
X-Org-Id explicitly on every request, even while your credentials access only one organisation — your integration then keeps working unchanged the day a second organisation is granted. See Authentication → Choosing an organisation for the resolution rules and error responses. To link additional organisations to your existing credentials, contact hello@flowpayroll.ai.PensionsDeprecationsDocumentation
Pension API v1 retired from the docs
The reference now documents the Pension API v2 only. Every pension page (employee pensions, schemes, groups, providers, auto-enrolment) is on the v2 spec. If you still call v1 paths, move to their v2 equivalents.Pay runsPayslips
More detail on payslips
Payslip base values and payslip summary items carry more fields, so an integration can rebuild a payslip line by line from the API without working out the figures itself. See the payslip endpoints in the Pay API overview.Behaviour changeStatutory payHMRCCourt orders
One response envelope across the services
Statutory pay, HMRC DPS and court orders now return the same response wrapper as the Pay API, so one error handler and one pagination reader work across all of them. See Response format.HMRCNew endpoints
P32, P45 and P60 over the API
The HMRC P-forms API grows from a single P45 read to the year-end and leaver forms.What’s new- P32:
GET /p32/{year},GET /p32/{year}/{month}and a PDF atGET /p32/{year}/{month}/pdf. - P45: a preview, a file download, finalised lists by tax year (optionally by payroll configuration), replacements, and
POST /p45/send-notificationto email them. - P60: by employee and tax year, a file download, preview and finalised lists,
POST /p60/{employeeId}/{taxYear}/regenerate, andPOST /p60/send-notification. …/tax-yearsendpoints on each form list the years that have data.
EmployeesNew endpoints
Leavers in bulk, and cancelling a leaver
PUT /employees/leaver/bulksets leaver details for many employees in one call, andDELETE /employees/leaver/bulkcancels them.DELETE /employees/{employeeId}/leaverclears one employee’s leaver details.GET /organisation/balances/paye-liability/tax-yearslists the tax years with PAYE liability data.- The court orders API gains
GET /orders/constants/{taxYear}andPOST /orders/reconcile, and statutory pay gains a history per payment. - Payment configuration and payment files moved into their own Payment API spec.
AuthenticationDocumentation
Authentication guide
A dedicated Authentication page walks through getting a token with the client credentials flow and sending it on each request.New APIPay runs
Recurring line items API
The Recurring line items API manages pay that repeats across pay runs: create a recurring item, attach employees to it, and read or adjust each employee’s schedule by tax year and period, down to a single occurrence.Billing
Billing: invoice numbers, references and email
POST /invoices/{clientId}/{invoiceId}/emailemails an invoice.- Configurable invoice number formats and invoice references, with the tokens you can use from
GET /finance/invoice-reference-fields. - Filter invoices by invoice number.
- Due date settings, and tag group and name on invoices formatted by tag.
PaymentsNew endpoint
Retry a failed payment file
POST /payment/file/run/{payrollConfigId}/year/{year}/frequency/{frequency}/period/{period}/retryregenerates a payment file that failed, and the file endpoint now returns an error response saying why generation failed.- BACS files carry the bank account name.
- Bank details are validated on input, and fewer bank fields are required.
Pay runsHMRCNew endpoints
Year-end finalisation
POST /payroll/year-end/{taxYear}/{payrollConfigId}/finalisefinalises a payroll configuration’s tax year.GET /payroll/year-end/{taxYear}/{payrollConfigId}reads it back, andGET /payroll/year-end/{taxYear}lists every finalisation in the organisation for that year.
Statutory payNew endpoints
Statutory pay attachments and imports
- Attach documents to a statutory payment with
POST,GETandDELETEon/payments/employees/{employeeId}/{statutoryPayId}/attachments. POST /payments/import/{employeeId}brings in statutory payments that began before the employee moved to Flow Payroll.
HMRCBehaviour change
FPS downloads, and DPS matching
- FPS submissions report more statuses and carry pre-signed
submissionDownloadUrlandpreviewDownloadUrllinks to the XML. - HMRC DPS gains
POST /notifications/retry-matching, which retries matching notifications to employees, andGET /notifications/counts.
EmployeesBehaviour changeNew endpoint
The payroll ID has its own endpoint
PUT /employees/{employeeId}/payroll-id changes an employee’s payroll ID. The payroll ID is still set on create and returned on every read, but it stopped being accepted on PUT /employees/{employeeId} on 14 April 2026,.EmployeesBehaviour change
Validation errors you can translate
Each validation issue now carriesreasonToken, a stable snake_case key, with reasonTokenArguments, beside the English reason. Build your own messages on the token; the wording of reason may change. The NI category field is now an enum of the HMRC-published letters.Payments
Barclays BACS files and payment references
- Payment configuration supports the Barclays BACS CSV format and lists
supportedFileFormatsfor each method. - A payment reference template, with a date format, sets the reference printed on each payment.
EmployeesDirectorsBehaviour change
Opening balances, split three ways
The single set of year-to-date values on an employee became three, so a migration can say where each figure came from:- Previous employment, sent on create and update.
- Opening balance, for earnings already paid in this employment.
- Director opening balance, for the stretch a director has spent on the director basis.
GET.EmployeesNew endpoint
Move an employee between payroll configurations
PUT /employees/{employeeId}/payroll-config changes the payroll configuration an employee is paid on. Marital status and phone number are no longer required on an employee, and the partner details fields were renamed to name and nationalInsuranceNumber.BillingIntegrations
Billing: accounting integrations
The Billing API connects to an accounting provider.What’s new- Connect, list, switch tenant and disconnect under
/integrations/{provider}. - Push clients, invoices and credit notes to the provider (
…/sync), and download the provider’s PDF. - Import contacts, tax rates and account codes from the provider.
- Organisation-wide pay element mappings, finance defaults, and invoice and credit note templates under
/finance.
HolidayStatutory pay
Rolled-up holiday pay and statutory rates
- The holiday configuration supports rolled-up holiday pay.
- Statutory pay gains
GET /statutory-rates/{taxYear},GET /statutory-rates/available-yearsandPOST /payments/reconcile.
EmployeesPay runsNew endpoints
Starter details and opening the next pay run
PUT /employees/{employeeId}/starterupdates starter details on their own, split out of the main employee update.POST /payroll/{payrollConfigId}/nextopens the next pay run for a payroll configuration.
EmployeesBehaviour change
Tax code and NI category are optional when onboarding
Leave them out and Flow Payroll applies the default tax code for the tax year and an NI category based on the employee’s age. The unusedisExemptFromNi flag was removed.New APIHoliday
Holiday API
The Holiday API covers holiday entitlement end to end: an organisation holiday configuration, per-employee configuration, holiday taken, opening balances, the employee’s history, and the 52-week average rate.BillingNew endpoints
Billing: credit notes and finance settings
- Credit notes: draft, update, authorise, void, allocate to an invoice, and list by organisation or client.
- Void an invoice, credit it in full, or act on many at once with
POST /invoices/bulk-action. - Tax rates and account codes under
/finance, and finance settings per client. - Unarchive a client.
New APIPensions
Pension API v2
The Pension API v2 adds auto-enrolment to the pension surface: assessments per pay run, employees whose enrolment is not complete, enrolment letters, postponement, and organisation-level auto-enrolment and postponement defaults.Pay runsBehaviour change
Payslips by pay run page with a token
GET /payslip/payrun/{payrollConfigId}/{year}/{period} now pages with paginationToken rather than a page number, because payslips are now pre-processed. Line items gain PO numbers and attachments, removed with DELETE /payroll/line/{employeeId}/{lineId}/attachments/{attachmentId}.Statutory payNew endpoints
Statutory pay: linking, average earnings and Alabaster
- Link periods of incapacity for work with
POST /calculations/employees/{employeeId}/linking, and read the calculation history. - Average weekly earnings, line items per payment, and a single payment by id.
- Alabaster detection and recalculation for maternity pay where a pay rise lands in the relevant period.
New APIStatutory pay
Statutory pay API
Statutory pay moved out of the Pay API into the Statutory pay API: calculations, a preview, and payments per employee and per organisation that you can update and delete.New APICourt orders
Court orders API
The Court orders API manages attachment of earnings orders: create and update orders, attach the order document, list an employee’s orders, and manage the payees deductions are paid to.Organisation
Class 1 NI-able pay elements
Pay elements say whether they are Class 1 National Insurance liable.BillingDocumentation
Attachments, and a new shape for the reference
- Attach files to a line item with
POST /payroll/line/{employeeId}/{lineId}/attachmentsand to an invoice withPOST /invoices/{invoiceId}/attachments. - The reference is now organised as Organisation setup, Pay API and Bill API, each with an overview page.
New APIBilling
Billing API
The first version of the Billing API: clients, contacts and invoices, including closing an invoice, with invoice summary items that name the employee each charge relates to.EmployeesHMRC
Employee codes and NI band results
employeeCode, an optional identifier of your own, on create, update and read.- NI results carry
grossForEmployeeNi,grossForEmployerNiandbandResults. - EPS requests take the tax month.
EmployeesNew endpointsBehaviour change
Effective-dated tax codes, NI categories and loans
Tax codes and NI categories became histories rather than single values, so a change can take effect from a date.What’s newPOST /employees/{employeeId}/tax-code,GET …/tax-codesandDELETE …/tax-codes/{year}/{effectiveDate}.- The same three for NI categories, and
GET /employees/{employeeId}/effective-tax-infofor what applies on a date. - Start and end student loans and postgraduate loans with their own endpoints.
PUT /employees/{employeeId}/ninoupdates the National Insurance number.- Salary is no longer required on an employee, and the unused
paymentTypewas removed.
New APIHMRC
HMRC DPS API
The HMRC DPS API retrieves the tax code, student loan and other notices HMRC sends, matches them to employees, and applies or ignores them one at a time or in bulk.OrganisationPay runsNew endpoints
Several organisations at once, and paged payslips
GET /organisation/information/multiplereturns several organisations’ details in one call.GET /payslip/payrun/{payrollConfigId}/{year}/{period}takespageSizeandpaginationToken.
EmployeesPensionsNew endpoints
Find an employee by NI number or payroll ID
GET /employees/by-ni/{nationalInsuranceNumber}andGET /employees/by-payroll-id/{payrollId}.GET /pension-group/allowed-baseslists the pensionable pay bases a pension group can use.
Pay runs
Pay periods and payment periods
- Pay runs and payslips carry
payPeriodStartDateandpayPeriodEndDate. - Payroll configurations take a payment period setup that says where each period starts.
Pay runsPensionsNew endpoints
Recurring line items, and pension exports
- Line items can recur: read an employee’s with
GET /payroll/line/{employeeId}/recurrent, and the lines in force for a range withGET /payroll/line/{employeeId}/effective-range. - The pension API gains pension calculations, CSV exports and export file retrieval per pay run.
HMRCNew endpoints
EPS resubmission, and the years that have data
POST /rti/eps/{taxYear}/manual/{submissionId}/resubmitresubmits an EPS.GET /payroll/runs/tax-years-with-pay-runsandGET /rti/eps/tax-years-with-submissionslist the years worth asking about.- EPS submission reads no longer return the raw XML.
HMRCPensions
P45 structure, and pension references
The P45 response took a new structure with week and month numbers, and a pension’s employer id becameexternalReferenceId.HMRCNew endpoints
PAYE liability and payments
GET /organisation/balances/paye-liability/{taxYear}summarises what is owed to HMRC.- Record, update and delete PAYE payments against it under
/organisation/balances/paye-payments/{taxYear}.
AuthenticationBehaviour change
OAuth 2.0 client credentials
API access moved to the OAuth 2.0 client credentials flow: exchange a client id and secret for an access token at the token endpoint, then send the token on each request. See Authentication.New APIHMRC
HMRC P-forms API
The HMRC P-forms API starts withGET /p45/{employeeId}.Pay runsEmployeesNew endpoints
Email payslips, and set leavers
POST /notification/email/payslip/{payrollConfigId}/{year}/{period}emails employees their payslips.PUT /employees/{employeeId}/leaversets leaver details.payrollIdon each employee in the list.
PaymentsPay runsNew endpoints
Payment files and payslip notes
GET /payment/file/run/{payrollConfigId}/year/{year}/frequency/{frequency}/period/{period}returns the payment file for a pay run.POST /payslip/data/{employeeId}/{year}/{period}stores payslip notes and a payment date override.
Pay runsNew endpoints
Pay runs and payslips in bulk
GET /payroll/runs/{year}lists pay runs, andGET /payroll/{payrollConfigId}/{year}/{period}/employeesthe employees on one.GET /payslip/payrun/{payrollConfigId}/{year}/{period}returns every payslip on a pay run, andGET /payslip/data/{employeeId}an employee’s payslips over a date range.forceGenerateregenerates on read.- Pay runs carry an
Openstatus,lockedBy,lockedDateandtotalBalances. - Payslip summary items carry the pay element id, tags and date, and base values include pension.
New APIPensions
Pension API
The first Pension API: pension providers, schemes and groups, associating employees with a pension, and monthly pension calculations.PaymentsPay runs
Create pay runs, and payment configuration
POST /payroll/{payrollConfigId}/{year}/{period}creates a pay run.- A Payments API configures how employees are paid per payroll configuration, with
GET /payment/available/configlisting the options. GET /clientslists your clients.- HMRC settings take a bank account, and payment date configuration takes
whichWeekInPeriodfor two- and four-weekly payrolls.
Employees
List employees
GET /employees lists employees with pagination. Employees gain salary, jobTitle and department.OrganisationHMRCNew endpoints
Pay elements, balances and the EPS
- Pay elements:
GET,POST,PUTandDELETEon/organisation/pay-element. Line items take a pay element id. - Opening balances and running balances by tax year and month under
/organisation/balances. - EPS: submit manually or from monthly balances, and read submissions by tax year under
/rti/eps. - The FPS is submitted and read at
/rti/fps/{payrollConfigId}/{taxYear}/{period}.
MCP server
MCP server
The Flow Payroll MCP server lets AI tools such as Claude and Cursor work with the Flow Payroll API and its documentation.OrganisationNew endpoints
Organisation information and HMRC settings
GETandPOST /organisation/informationfor the organisation’s details.GETandPOST /organisation/hmrc/settings, including the PAYE reference.- Add line items with
POST /payroll/line/{employeeId}, andDELETE /payroll/line/{employeeId}/{lineId}to remove one. - Payslips carry the employer’s details.
EmployeesHMRCNew endpoint
Leavers, partners and working days
PUT /employee/{employeeId}/leaverupdates leaver details.- Employees gain partner details and a working days configuration.
- Seconded details are a single
stayStatusenum, following HMRC’s FPS guidance.
Statutory payNew endpoints
Statutory pay
Calculate statutory pay for an employee or as a preview, and create and read statutory pay records, under/statutory.HMRCNew endpoint
Preview the FPS, and read a submission
POST /payroll/{payrollConfigId}/{year}/{period}/hmrc/fps submits the FPS, or previews it with a query parameter, and GET on the same path returns a submission already made.HMRCPay runs
FPS submission, and director appointment dates
- Submit the FPS for a pay run over the API.
- Director details take an appointment date, so appointments in past years are reported correctly.
Pay runs
Upload line items per employee
Line items are uploaded one employee per request, so each one validates on its own. Uploads takeniType, respond with the payrollDate, and accept up to 100 items per request. Employees gain title and payrollId, and employee and payslip fields follow HMRC’s FPS.