Home Payroll API Authenticating with the Payroll API

Authenticating with the Payroll API

Last updated on Sep 19, 2026

Every request to the Payroll API is authenticated. This article covers the two methods, the conventions every request follows, and the shape of an error. It is generated from the API reference, version 0.5.0.

How do I authenticate?

There are two ways to authenticate, and which one you want depends on what you are building.

API key: for server-to-server integrations. Send both headers on every request:

curl "https://payroll-api.moonworkers.co.uk/api/employer/" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Client-Secret: YOUR_CLIENT_SECRET"
import requests

headers = {
    'X-API-Key': 'YOUR_API_KEY',
    'X-Client-Secret': 'YOUR_CLIENT_SECRET',
}
response = requests.get('https://payroll-api.moonworkers.co.uk/api/employer/', headers=headers)
const response = await fetch('https://payroll-api.moonworkers.co.uk/api/employer/', {
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'X-Client-Secret': 'YOUR_CLIENT_SECRET',
  },
})

Bearer token: used by the Moonworkers web app. POST /api/auth/login/ returns an access_token; send it as a bearer token, and refresh it through POST /api/auth/token/refresh/ when it expires:

curl "https://payroll-api.moonworkers.co.uk/api/employer/" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

The authentication endpoints themselves: login, registration, activation, password reset and the social OAuth flow: exist for the web app. If you are building an integration, use an API key rather than logging in as a user.

A handful of endpoints take a bearer token only, and refuse an API key with TokenAuthRequiredException (403). They act on behalf of the signed-in person rather than on the account: their own profile, password and two-factor settings, the notifications addressed to them, the team roster they administer, and the billing calls that hand a human a Stripe checkout page. An API key names an account, not a person, so on these routes it has no one to act as. Each one says so at the top of its description, and lists only the bearer scheme under its padlock.

Which base URL do I use?

The sandbox is at https://payroll-api-sandbox.moonworkers.co.uk and production at https://payroll-api.moonworkers.co.uk. The examples below use production; swap the host while you build. The sandbox also has a Swagger UI at https://payroll-api-sandbox.moonworkers.co.uk/api/docs/ to try calls in the browser.

How are requests shaped?

Most resources hang off an employer: /api/employer/{employer_pk}/..., and reports are nested the same way: employer-level ones such as the P30 and P32 under /api/employer/{employer_pk}/report/..., and per-employee documents such as the P45, P60 and P11D under /api/employer/{employer_pk}/employee/{employee_pk}/report/....

Every path ends in a trailing slash. Without it a GET redirects and a POST fails.

Updates use PUT and replace the whole record. PATCH is not supported, so send every field you want to keep, not just the ones you are changing.

How do errors come back?

A failed request returns the error class name and a message:

{"errorCode": "PayrunNotEditableException", "errorMessage": "This payrun is finalized."}

Branch on errorCode rather than on the message text: messages may be reworded, codes will not. Authentication failures are the exception: they come from the framework itself and return {"detail": "..."}.

The full list of error codes is in the "Payroll API error codes" articles.

Get started

The full reference is at https://payroll-api.moonworkers.co.uk/api/redoc/. No key yet? See "How do I get a Payroll API sandbox key?".