Penfold Payroll API
4.0

Add MCP server to your AI tool

Allow AI tools and LLMs to interact with the API documentation portal through MCP.

MCP server URL

https://docs.getpenfold.dev/mcp

Standard setup for AI tools providing an mcp.json file

mcp.json
{
  "Penfold Payroll API v4 MCP server": {
    "url": "https://docs.getpenfold.dev/mcp"
  }
}

Close
Base URL
https://payroll-api.getpenfold.dev/v4

Unified API for onboarding employers and managing employees, contributions, pay periods and file uploads.

Authentication

Penfold provisions your integration as an organisation on this API. There are two organisation types, and the type you are given determines the OAuth2 flow you use, the scope of your access tokens, and which endpoints you may call — see Organisation types and endpoint availability below.

Organisation type OAuth2 flow Token scope
Payroll Authorization Code with PKCE (Amazon Cognito) A single employer, who grants consent through the Penfold Workplace Platform
Partner Client credentials (Azure AD B2C) Every employer owned by your organisation

Whichever flow you use, send API requests with Authorization: Bearer <access_token> over HTTPS, and store access and refresh tokens securely server-side.

Payroll organisations — Authorization Code with PKCE

An employer admin grants your application access through the Penfold Workplace Platform. The resulting access token carries an employer claim, so every request is scoped to that one employer; repeat the flow once per employer you integrate with.

The full flow — environment URLs, PKCE code verifier and challenge, worked examples and token refresh — is in the integration guide. Two points are worth highlighting:

  • The redirect_uri sent to /oauth2/authorize, and repeated unchanged when exchanging the code for tokens, is always Penfold's consent URL (https://platform.getpenfold.dev/auth/oauth2-consent on staging) — not your own callback URL.
  • Your callback URL is carried in the state parameter as redirectUri, and must be pre-registered against your organisation. If it is absent or unregistered, Penfold redirects to the first URL registered for your organisation rather than failing.

Partner organisations — client credentials

Server-to-server only — there is no interactive sign-in and no connection to the Penfold Workplace Platform. The token's application (client) ID is mapped to your Penfold organisation, which scopes all requests to employers owned by your organisation.

When Penfold onboards your organisation, we provide the following via a secure channel:

  • Application (client) ID — your Azure AD B2C app registration
  • Client secret — used with the client ID to obtain access tokens
  • Webhook signing secret(s) — for verifying inbound webhook signatures (see Webhooks below)

Penfold does not issue or deliver access tokens directly. Your servers obtain short-lived tokens from Azure AD B2C using the client ID and client secret, with grant_type=client_credentials and the scopes configured for your application.

Example token request (substitute client_id, client_secret, and scope):

TOKEN_URL='https://login.microsoftonline.com/36d1ec63-a7dc-48ca-a634-5514be9a63dd/oauth2/v2.0/token'
curl --location --request POST "$TOKEN_URL" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=<client_id>' \
--data-urlencode 'scope=https://penfoldstaging.onmicrosoft.com/<client_id>/.default' \
--data-urlencode 'client_secret=<client_secret>'

Organisation types and endpoint availability

Most endpoints are available to both organisation types. The endpoints below are available to Partner organisations only — a Payroll organisation calling one receives 405 Method Not Allowed. Each is also marked in its own description.

Method Path
POST /employers
PATCH /employers/{employer_id}
POST /employers/{employer_id}/agreement-signing-sessions
GET /employers/{employer_id}/agreement-signing-sessions/{signing_session_id}
POST /employers/{employer_id}/direct-debit-sessions
GET /employers/{employer_id}/pay_periods
GET /employers/{employer_id}/pay_periods/{pay_period_id}
GET /employers/{employer_id}/employees/{employee_id}/portfolio_summary
GET /employers/{employer_id}/employees/{employee_id}/pot_value_history
GET /employers/{employer_id}/employees/{employee_id}/transactions
GET /employers/{employer_id}/employees/{employee_id}/beneficiaries
PUT /employers/{employer_id}/employees/{employee_id}/beneficiaries
GET /employers/{employer_id}/employees/{employee_id}/fund_allocation
PUT /employers/{employer_id}/employees/{employee_id}/fund_allocation
GET /fund_allocations
GET /fund_allocations/{fund_allocation_id}

Two further differences apply to endpoints that both organisation types may call:

  • external_id on employee enrolment rows (POST .../employees) and contribution rows (POST .../contributions) is required for Partner organisations that use single sign-on with Penfold, and must be omitted by every other organisation, including Partner organisations that do not use single sign-on.
  • GET /employers returns every employer owned by your organisation for a Partner organisation; for a Payroll organisation it returns only the single employer your access token is scoped to.

Date fields

Date-only fields (format: date, e.g. exit_date, employment_start_date, date_of_birth, pay-period dates) use UTC calendar dates in YYYY-MM-DD form. Values are interpreted and emitted as the UTC calendar day, not the server's local timezone. Timestamps (format: date-time, e.g. created_at) remain ISO 8601 UTC instants.

Submission lifecycle (uploads)

Employee enrolment (POST /employers/{employer_id}/employees) and contribution (POST /employers/{employer_id}/contributions) submissions are asynchronous. Each returns an Upload object representing queued work, not a final result.

To consume the result:

  1. Poll GET /uploads/{upload_id} until status is terminal (Processed, Error, Timeout, or PartiallyProcessed).
  2. Fetch created enrolments via GET /uploads/{upload_id}/enrolments and contributions via GET /uploads/{upload_id}/contributions. Member and contribution IDs are not returned in the submission response.
  3. Fetch errors via GET /uploads/{upload_id}/errors. Errors are scoped at File, AllRows or Row level.

Processing is idempotent per pay period: re-submitting the same records (or file) will only process records that have not already been processed successfully. This applies to both contribution and enrolment submissions, and is the recommended way to recover from a PartiallyProcessed upload.

Webhooks

Penfold POSTs a JSON body to your pre-registered webhook URL when a resource reaches a notable state. Every webhook shares a common envelope:

Field Type Description
event_id string Globally unique. Upsert by this value for idempotency.
resource_type string Which resource changed (e.g. EmployerSigningRequest). Determines valid action values and payload shape.
action string What happened (Created, Completed, Cancelled, Failed).
occurred_at string ISO 8601 UTC timestamp when Penfold recorded the event.
payload object Resource-specific data — see schemas below.

Resource types

EmployerSigningRequest

Emitted when an agreement signing session is created or reaches a terminal outcome. Expect one Created event followed by exactly one of Completed, Cancelled, or Failed.

Actions: Created, Completed, Cancelled, Failed

Payload:

Field Type Description
signing_session_id string Same id returned from create-session.
employer_id string Penfold employer id.
employer_external_reference string The employer's external reference.
MemberSchemeStatus

Emitted when a worker's scheme membership status changes (e.g. worker-initiated opt-out, irregular contributor, employer scheme closed).

Actions: Changed

Payload: employer_id, employee_id, previous_status, status, effective_at

Employment

Emitted when a worker's employment ends, whether initiated by you or by Penfold.

Actions: Ended

Payload: employer_id, employee_id, exit_date, initiated_by (Partner or Penfold)

ContributionChangeRequest

Emitted after a successful POST .../contribution_change_requests.

Actions: Created

Payload: employer_id, employee_id, id, employee_contributions_percent, salary_sacrifice_enabled, created_at

FundAllocation

Emitted when a worker's elected fund allocation changes via a successful PUT .../employees/{employee_id}/fund_allocation (not on idempotent no-ops). Optional — you may rely on the PUT 200 response instead if you originated the request.

Actions: Changed

Payload: employer_id, employee_id, previous_fund_allocation_id, fund_allocation_id, effective_at

EmployerAmlVerificationStatus

Emitted when an employer company's AML verification status changes (not on idempotent no-ops). You may also poll the employer endpoints as a fallback.

Actions: Changed

Payload: employer_id, previous_status, status

Worker opt-out

There is no API to opt a worker out. Workers opt out through Penfold worker self-service (for example email). Penfold updates member_scheme_status to OptedOut and emits a MemberSchemeStatus webhook with action Changed. You may also poll GET .../employees/{employee_id} as a fallback.

Signature verification (X-Penfold-Signature)

Penfold signs every webhook with HMAC-SHA256 using a shared secret provisioned during onboarding.

  1. Read the X-Penfold-Signature header. It contains comma-separated key/value pairs: t (Unix timestamp in seconds when Penfold sent the request) and v1 (lowercase hex-encoded HMAC-SHA256).
  2. Construct the signing payload (UTF-8): {t}.{raw_body} — the string value of t, a literal ., then the raw JSON body bytes.
  3. Compute HMAC-SHA256(secret, signing_payload), hex-encode, and compare to v1 in constant time.
  4. Reject if |now_unix - t| > 300 (5-minute replay window).
  5. Upsert by event_id for idempotency.

Delivery semantics

  • Return 2xx within 30 seconds.
  • On non-2xx, timeout, or no response: exponential backoff starting at 60 seconds, doubling each attempt, max interval 3600 seconds, up to 10 attempts within 24 hours.
  • After exhausted retries, events are not retried automatically.

Naming conventions

This API uses one casing scheme for what you send and receive — JSON bodies, URL path and query parameters, pagination fields, and signing redirect parameters. Penfold's internal code and databases are not affected.

Default: Identifiers use snake_case (e.g. employer_id, page_size, signing_session_id). Values that describe state or type (statuses, payment methods, webhook actions, upload purpose, signing redirect result values such as Success, etc.) use PascalCase. Values passed to sort_by are field names and use snake_case (e.g. created_at).

New fields should follow these rules unless listed as an exception below.

Exceptions

Item Casing Reason
Azure AD token request (grant_type, client_id, client_secret, scope) OAuth2 protocol names (typically snake_case) Field names are defined by OAuth2, not this API, and are sent to the identity provider's token endpoint rather than to us.
HTTP header names (Authorization, Content-Type, X-Penfold-Signature) Standard HTTP header casing Not JSON body fields
Webhook signature header subkeys (t, v1) Lowercase Fixed format documented under Webhooks
sort_order query values (asc, desc) Lowercase Accepted case-insensitively
Upload error code and scope values PascalCase Fixed error type labels (e.g. NationalInsuranceNumberInvalid, AllRows), not field names

Support

This is version 4.0 of this API documentation. Last update on Sep 30, 2026.