Skip to main content
The Pensions API manages everything Flow Payroll needs to run workplace pensions: the schemes your organisation uses, the contribution groups employees belong to, each employee’s enrolment and postponement, the auto-enrolment (AE) assessments produced by pay runs, and the per-period calculation files you send to your provider. Use it to set up pensions before your first pay run, keep enrolments in sync with your HR system, and collect contribution data after each period. The service lives at /v2/pension relative to the gateway base URLs on the API reference introduction — for example, https://api.sandbox.flowpayroll.ai/v2/pension in sandbox.

Key concepts

  • Pension provider — the pension company itself. A provider has a name, a URL-friendly slug, a flag indicating whether schemes with it require a payment source, and exportSupported, which says whether a completed pay run produces an export file for its schemes. The Manual provider is the one that does not: choose it for a scheme you file with your provider yourself. Providers are set up in the product (go to Settings and open the Pension tab); the API exposes them read-only.
  • Pension scheme — your organisation’s arrangement with a provider. A scheme records its payment source, duty start date, last re-enrolment date, and an optional external reference. Scheme endpoints use the /pension-schema path.
  • Pension group — a set of contribution rules within a scheme: the pension type (relief at source, salary sacrifice, or net pay arrangement), the contribution basis, employee and employer factors, a National Insurance savings factor, and an optional external reference. One group can be marked as the organisation’s default.
  • Employee pension association — the link between an employee and a pension group, with an enrolment date and an option to send enrolment letters.
  • Additional voluntary contribution: an extra employee contribution on top of the scheme contribution, for one pay run only. Held per employee, tax year, and period.
  • Contribution override: replaces the group’s contribution settings for one pay run only, for example an unpaid-leave month. Any additional voluntary contribution still applies on top. Held per employee, tax year, and period.
  • Membership end: the record of a membership ending: why it ended, when, and what each side is owed back. It survives the association being removed, which is what keeps a refund retrievable afterwards.
  • Auto-enrolment assessment — the AE result a pay run produces for each employee: the assessment outcome, worker age, earnings, and effective assessment date.
  • Postponement — a deferral of auto-enrolment, defined as a start-date definition and an eligibility-date definition (a value plus a frequency). Defaults are set at organisation level and can be overridden per employee.
  • Pension calculation — an employee’s contribution amounts for a specific period, retrievable per month or per provider and exportable as a CSV or provider-format file.
  • Itemised contributions: how contributions reach a payslip: the pension service pushes one payroll line per contribution, so the payslip shows a pension row per contribution, and additional voluntary contributions, per-period overrides and opt-out refunds through payroll all become possible.
  • Provider submission: what was sent to a scheme’s connected pension provider for one pay run: the enrolments, the contribution schedule and the payment approval, each with its status, the provider’s reference and the reason if it did not land. A scheme becomes sendable by being linked to a connection in the Integrations API; NEST is the first provider.

API structure

Pension providers

Providers are read-only through the API: list them with GET /pension-provider, retrieve one with GET /pension-provider/{id}, or look one up by its slug with GET /pension-provider/slug/{slug}. Create and manage providers in the product — go to Settings and open the Pension tab.

Pension schemes

Create, update, retrieve, and delete schemes at /pension-schema and /pension-schema/{id}. GET /pension-schema/organisation returns every scheme in your organisation, each with its provider and groups embedded. Creating a scheme requires a pensionProviderId — find it with GET /pension-provider.

Pension groups

Manage contribution groups at /pension-group and /pension-group/{id}. Before creating a group, call GET /pension-group/allowed-bases to see the contribution bases the service accepts. Set externalReferenceId on a group to record the identifier your provider uses for it — a worker group or category code, for example. It is optional, and it is returned on every group, including the groups embedded in GET /pension-schema/organisation, so you can map our groups onto yours without a second lookup.
A group’s externalReferenceId is stored and returned for your own reconciliation. It is the scheme-level externalReferenceId that provider export files use as the employer reference.

Employee enrolment

POST /employee-pension associates an employee with a pension group, and GET /employee-pension/{employeeId} reads the association. GET /employee-pension/batch fetches several employees at once via repeated employeeId query parameters. POST /employee-pension/{employeeId}/enrolment-letter triggers an enrolment letter for an employee.

Ending a membership

DELETE /employee-pension/{employeeId} is the single removal surface, and what you pass to it decides what is recorded: The reason is a record of what happened, not a switch: any ending stops further automatic assessment until the employer’s next cyclical re-enrolment date, and a worker whose membership ended within 12 months of that date is left out of that cycle by default. Whether an opt-out falls inside the statutory opt-out period is the employer’s assessment, not something the service enforces. Call GET /employee-pension/{employeeId}/refund-preview first. It is side-effect-free and returns exactly what a refund would pay back, per settled period, so an operator can see the figure before confirming. Endings stay readable long after the association is gone: GET /employee-pension/{employeeId}/membership-ends for one employee, and GET /organisation/pension-membership-ends for the organisation’s refunds report over a date range.
A refund covers the member’s contributions only. Employer contributions are repaid by the scheme to the employer directly, so they never appear on a payslip. The membership-end records carry both figures, which is what makes the organisation report reconcilable against what the provider actually returned.

Additional voluntary contributions and per-period overrides

Both are addressed per employee, tax year, and pay period, both are idempotent (a repeated PUT replaces the value), and both apply to open pay runs only. The period’s payslip is re-processed automatically, so the change shows up without you triggering anything.
  • Additional voluntary contributions at /employee-pension/{employeeId}/additional-voluntary-contributions/{taxYear}/{period} (GET, PUT, DELETE), with GET /employee-pension/{employeeId}/additional-voluntary-contributions listing them in pay-period order. An additional voluntary contribution sits on top of the employee’s normal contribution. On a relief-at-source scheme the payslip deduction is 80% of the gross amount you set, because the provider claims the basic-rate relief.
  • Contribution overrides at /employee-pension/{employeeId}/contribution-overrides/{taxYear}/{period} (GET, PUT, DELETE), with GET /employee-pension/{employeeId}/contribution-overrides listing them. An override replaces the group settings for that one period, and any additional voluntary contribution still applies on top. The auto-enrolment minimum binds per pay reference period, so an override below it is refused unless you send acknowledgeBelowAutoEnrolmentMinimum.
Additional voluntary contributions and per-period overrides require an organisation whose pension contributions are itemised, which is the standard setup. If an organisation is not, the write succeeds and the payslip does not change. Contact support if a saved contribution or override is not reflected on the payslip.

Auto-enrolment

GET /auto-enrolment/assessments/payrun/{payrollConfigId}/{year}/{frequency}/{period} returns the assessments a pay run produced, and GET /auto-enrolment/complete/payrun/{payrollConfigId}/{year}/{frequency}/{period} lists employees whose enrolment is complete for that pay run. GET /auto-enrolment/not-complete and GET /auto-enrolment/not-auto-enrolments list employees with outstanding or no auto-enrolment, optionally including employee details. POST /auto-enrolment/assessments/preview runs a stateless assessment preview from the request body alone — supply a date of birth, earnings with their frequency, and the pay reference period dates, and it returns the worker category (EligibleJobholder, NonEligibleJobholder, EntitledWorker, or NotEligible) with its consequences, without reading or storing any employee data.

Enrolment information duty

You have six weeks from someone’s enrolment date to give them their enrolment information (The Pensions Regulator, detailed guidance 5 and 10). Two endpoints report on that duty. They are exact complements: every membership appears on one list or the other, never both and never neither. An empty outstanding list therefore means everyone has been informed. GET /auto-enrolment/enrolment-information/outstanding lists who is still owed theirs, worst first: failed sends, then overdue, then still due. GET /auto-enrolment/enrolment-information/sent lists who has already had theirs, most recently given first, with sentDate and sentBy. sentDate is the day the information was given, not the day an email was dispatched. It is the day the member’s opt-out period runs from (detailed guidance 7, paragraphs 17 and 18). Someone holding both a manual and an automatic membership appears once for each. Each is its own enrolment, with its own date and its own letter. Both endpoints are paginated. Follow content.metadata.paginationToken until it is absent rather than treating a short page as the last. The outstanding list returns nothing for an organisation that has declared it issues the information itself. The sent list still answers, because Flow can always report what it sent.

Postponement

PUT /postponement sets an employee’s postponement, GET /postponement/{employeeId} reads it, and DELETE /postponement/{employeeId} removes it.

Organisation defaults

GET /organisation returns your organisation’s pension defaults: the default pension group, default postponement definitions, and the auto-enrolment mode. Update them with PUT /organisation/pension-default/postponement-settings and PUT /organisation/pension-default/auto-enrolment-settings.

Pension calculations and exports

Read calculations with GET /pension-calculation/by-year/{year}/by-month/{month}, or narrow to one provider with GET /pension-calculation/pension-provider/{pensionProviderId}/by-year/{year}/by-month/{month}. POST /pension-calculation records a calculation for an employee and period. For a pay period, fetch a CSV via GET /pension-calculation/csv/year/{year}/period/{period}/frequency/{frequency}/payroll-config/{payrollConfigId}, or request provider export files with POST /pension-calculation/export and list the generated files — including their file paths — through GET /pension-calculation/export/year/{year}/period/{period}/frequency/{frequency}/payroll-config/{payrollConfigId} or the equivalent /export-file-retrieval/pension-calculation/... endpoint. A scheme on the Manual provider (exportSupported: false on GET /pension-provider) never has export files: read its contributions through the calculation endpoints instead, and use the payroll_run.completed webhook to know when a period’s figures are final.

Provider submissions

A scheme linked to a provider connection can be sent directly instead of, or as well as, by export file. NEST is the only provider supported today; schemes with any other provider continue through the export files. Completing a pay run produces the export files and stops there: a submission only happens when you ask for one, and approving the payment is a second, separate call. The pay run is addressed in the path, as /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}.
  • GET /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/sendable lists every scheme in the pay run and whether each can be sent right now. A scheme that cannot be sent is listed with sendable: false and a reasonToken rather than hidden: pension_scheme_not_connected for one that delivers by export file, connection_not_callable for one whose connection has to be finished or repaired. An empty list means the pay run holds no pension calculations at all.
  • POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/send submits the run. Pass schemeId to send one scheme and operationType to send one part, which is how a part that did not land is sent again on its own; with both parts, enrolments go first, because a member the provider has never seen cannot appear on a contribution schedule. Naming a schemeId the pay run does not hold returns 404. A 2xx means the request was understood, not that anything was sent. Read schemes to find out what the send decided, scheme by scheme: Submitted, Skipped (not connected, or nothing outstanding), Blocked (the connection needs finishing or repairing) or Failed, each with a reasonToken and a readable reasonMessage. submissions carries the pay run’s submission ledger as it stands, filtered to the scheme you asked for; follow it with the next endpoint. Do not read an empty submissions as success — a scheme that delivers by export file sends nothing and says so in schemes. Sending the same unchanged data twice is deduplicated rather than submitted again, while a corrected run goes as a new submission, which is how the provider expects corrections to an open schedule.
  • GET /provider-sync/{payrollConfigId}/{year}/{frequency}/{period} lists the pay run’s submissions per scheme, one record per operationType: pension.members.enrol, pension.contributions.submit and pension.payment.approve. Each carries status, memberCount, totalAmount, the provider’s providerReference once issued (the schedule reference, for contributions), and errorToken with errorMessage if it did not land.
  • GET /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/issues?schemeId&operationType returns every reason the provider refused one submission, each with the record it relates to, the provider’s own code and its own wording. A submission record carries the first page and the total; use this for the rest.
  • POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/refresh-members re-reads named members from their employee records and writes their details back onto the pay run, so a refusal about a person, such as a National Insurance number added after the run was locked, can be corrected without re-running payroll. Amounts, periods and payment dates are never touched. Name up to 250 members by employee id, as issues reports them.
  • POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/approve-payment tells the provider to collect the submitted schedule. It is accepted only once the contribution schedule has status Succeeded and a providerReference; otherwise it returns 400 with a plain-English reason. It is never called on your behalf.
  • GET /provider-sync/returns?type&pageSize&paginationToken lists what providers sent back, newest first: pension.opt-outs.received, pension.stopped-contributions.received, pension.refunds.received, pension.schedules.received and pension.setup.received, each with the full detail as document. Keep following content.metadata.paginationToken while it is non-null.
  • GET /provider-sync/reconciliation?schemeId compares the scheme with its provider: members enrolled in Flow Payroll, opt-outs and stopped contributions the provider reported and how many were applied or could not be matched, the refunds the provider reported and their total (the provider’s own figure), and when the last return arrived.
  • GET /provider-sync/member-changes?status&pageSize&paginationToken is the queue of what providers reported about members, paged the same way. Nothing here has been applied: reading from a provider never changes payroll on its own, so each waits for POST /provider-sync/member-changes/{changeId}/apply, /dismiss, /acknowledge or /delete. GET /provider-sync/member-changes/summary gives the count for a badge.
Submission status values are the provider connection’s own: Pending (queued or waiting for a retry), AwaitingProvider (accepted by the provider, still processing), Succeeded, Failed, NeedsAttention (stopped for a reason a retry alone will not fix) and Cancelled, plus SubmitFailed when the submission could not be built at all, for example pension_missing_payment_source or pension_unsupported_frequency. What happens between those statuses, including the retry schedule, pausing after repeated failures and the two-day result deadline, is described once on the Integrations API overview.
A reported opt-out is applied automatically: the member’s membership ends on the date the provider gives, through the same flow as DELETE /employee-pension/{employeeId}, with reason OptOut and no payroll refund, because the provider refunds the member directly. A reported stopped contribution ends the membership as MembershipCeased. Refund amounts the provider reports are recorded for you to reconcile, never re-applied to payroll.

Employee pension information

GET /pension-information/{employeeId} returns a single employee’s full pension picture in one call: their pension composition, latest auto-enrolment determination, and any postponement.

Typical workflow

  1. Look up your provider (GET /pension-provider/slug/{slug}) — set it up in the product first if it does not exist — then create a scheme with POST /pension-schema.
  2. Check GET /pension-group/allowed-bases, then add one or more groups with POST /pension-group, marking one as the default.
  3. Set your organisation’s postponement and auto-enrolment defaults via the /organisation/pension-default endpoints — see the pension defaults guide.
  4. Enrol employees with POST /employee-pension, sending enrolment letters where required.
  5. Run your pay run, then read its AE results with GET /auto-enrolment/assessments/payrun/... and follow up on anyone listed by GET /auto-enrolment/not-complete.
  6. After the period, download the contribution CSV or request an export with POST /pension-calculation/export and retrieve the generated files for your provider.
  7. If the scheme is linked to a provider connection, send the run instead with POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/send, follow it with GET /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}, and approve the payment with POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/approve-payment once the schedule is accepted.
For the product-side view of assessment outcomes, postponement, and letters, see the auto-enrolment guide.

Notes and best practices

  • Scheme endpoints use the path segment pension-schema (singular), not pension-scheme — copy paths exactly as documented.
  • Assessments, CSVs, and exports are addressed by payroll configuration, tax year, frequency, and period, so keep those identifiers from your pay run to hand.
  • Provider export files are described by records with a file path, provider slug, export group, and file type — generate them with the export POST, then list them with either retrieval endpoint.
  • Postponement definitions are a value plus a frequency at both organisation and employee level; an employee-level definition set via PUT /postponement takes the place of the defaults for that employee.
  • Send pensionType when you create or update a group, and read pensionType and pensionGroup back — isSalarySacrifice, organisationId on a group, and the split autoEnrolmentPensionGroup / manualEnrolmentPensionGroup fields are deprecated and go on 7 December 2026. The four response fields stop being returned on that date, so update your parsing as well as your writes. See Deprecations.
  • Every response uses the shared envelope. Read message.token, validationIssues and content.metadata: the old errors list, the top-level metadata, and message.textId / message.textParameters are copies of them, deprecated and removed on 30 December 2026. On a paginated list, read the cursor from content.metadata.paginationToken; content.data.paginationToken is a deprecated copy. The rows stay in content.data.items. See Deprecations.
  • Reading pension amounts off a payslip has one rule: take them from the payslip’s pension summary items or from forPeriod.pension, never from both. See reading pension on a payslip.
  • Additional voluntary contributions and per-period overrides are keyed by tax year and period, so they are written against a specific pay run and only take effect while that run is open.
  • A provider submission never starts on its own, payment approval is never implied by a send, and a change a provider reports never applies itself. Call GET /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/sendable before offering a send, check the schemes outcomes on the send itself rather than assuming a 2xx sent something, and expect NeedsAttention submissions to need a person: the errorToken says what to fix before sending again. Where the refusal names members rather than the file, POST /provider-sync/{payrollConfigId}/{year}/{frequency}/{period}/refresh-members re-reads them so the run does not have to be re-run.
  • The contribution due date is read from the provider’s own published schedules and matched on the earnings period, so sending late does not change it. A scheme can carry contributionDueDateOffsetDays for a provider that has published no schedule yet; without either, a submission is refused with pension_unknown_contribution_due_date rather than sent with a guess.
  • The export files are still produced for a linked scheme on every pay run, so the file route remains available as a fallback.
  • Removing an enrolment without an optOutDate or reason records an administrative removal. It still stops further automatic assessment until the next cyclical re-enrolment date, so pass the date or the reason when the employee actually opted out — the reason is what the record shows and what you evidence in an audit.

Authentication

Every request needs an X-Auth-Token header, and multi-organisation credentials also send X-Org-Id — see Authentication. Responses use the shared envelope described in Response format.