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 Partner API MCP server": {
    "url": "https://docs.getpenfold.dev/mcp"
  }
}

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

Unified API for payroll partners to onboard employers, manage employees, contributions, and file uploads.

Authentication

Partner integrations authenticate with Azure AD B2C using the OAuth2 client credentials grant. These endpoints are server-to-server only — there is no interactive partner sign-in and no connection to the Penfold Workplace Platform.

Onboarding

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.

Calling the API

  1. Request an access token from the Azure AD B2C token endpoint with grant_type=client_credentials, your client_id, client_secret, and the scopes configured for your application.
  2. Send API requests with Authorization: Bearer <access_token>.

The token's application (client) ID is mapped to your Penfold organisation, which scopes all requests to employers owned by your organisation only.

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>'

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 the partner 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 for partners — they may rely on the PUT 200 response instead if they originated the request.

Actions: Changed

Payload: employer_id, employee_id, previous_fund_allocation_id, fund_allocation_id, effective_at

Worker opt-out

There is no partner 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. Partners 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 partners 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. Partners call Microsoft's token endpoint before calling 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

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