# Build on PayCaptain > One API for UK payroll data. Send employees, shifts and pay elements in; read employees and payslips out. Every rule is published here, for people and for agents. ## What you can do | You want to | Use | Field rules | | --- | --- | --- | | Create or update an employee | `POST /employee` | [Employee fields](https://developer.paycaptain.com/fields/employee) | | Send approved shifts and absences | `POST /shift` | [Shift fields](https://developer.paycaptain.com/fields/shift) | | Send bonuses, expenses and other pay elements | `POST /payment` | [Payment fields](https://developer.paycaptain.com/fields/payment) | | Read employees back | `GET /employees` | [Employee fields](https://developer.paycaptain.com/fields/employee) | | Read payslips for a pay period | `GET /payslips` | [Payslip fields](https://developer.paycaptain.com/fields/payslip) | New here? The [Quickstart](https://developer.paycaptain.com/quickstart) takes you from API key to first accepted record. ## How it fits together Your application is the system of record for people, attendance and pay elements. PayCaptain is the payroll engine. Records flow one way, through a validation layer you own, and payslips flow back. ```text Your application -> Your validation layer -> PayCaptain employees field rules POST /employee shifts business rules POST /shift pay elements change detection POST /payment payslip viewer <- ------------------------ GET /payslips, GET /employees ``` > **Must** Validate before you send. PayCaptain rejects invalid data, but it is not your validation service. Enforce the [field rules](https://developer.paycaptain.com/fields/employee) and [business rules](https://developer.paycaptain.com/rules) in your own code, and only then call the API. ## Trust, in practice PayCaptain is HMRC-recognised payroll software with a direct connection to UK banking services, certified to ISO 27001 and Cyber Essentials Plus. Payroll only works when everyone can rely on it, so this site is built to be checked rather than believed. - **Every rule is written down.** Each field has its type, format, allowed values and conditions on one page. If the API rejects something for a reason that is not documented here, tell us and we will document it. - **One source for people and agents.** Every page is also plain Markdown, and the field rules ship as JSON Schema and OpenAPI. All of it is generated from the same data, so the copies cannot drift apart. - **Clear responsibilities.** PayCaptain's core is operated by PayCaptain UK personnel. Your integration comes in through one authenticated door, and [Trust and security](https://developer.paycaptain.com/trust) sets out what we do and what you must do. - **No trackers on this site.** No cookies, no analytics scripts, no third-party requests. ## Open to the way you work - **Customers** connect the HR, rota and time systems they already run, or build their own. - **Payroll teams** stay in control. Pay Groups, Pay Codes and approvals are configured in PayCaptain, and the API respects them. - **Partners** build one integration and offer it to every shared customer. - **AI builders and agents** get the same rules in machine-readable form. See [Build with AI](https://developer.paycaptain.com/agents). More endpoints are available on request. Email [developer@paycaptain.com](mailto:developer@paycaptain.com) to discuss what you need, or to get your API key and company code. Source: https://developer.paycaptain.com/ --- # Quickstart > From API key to first accepted record in five steps. ## 1. Get your credentials Email [developer@paycaptain.com](mailto:developer@paycaptain.com) for two things: - an **API key**, sent as the `X-API-Key` header on every request - your **company code**, sent as `company` on every record and query Store the key in a secrets manager and call the API from your server only. [Trust and security](https://developer.paycaptain.com/trust#api-keys) has the full list of requirements. ## 2. Make a first call All requests go to `https://api.paycaptain.com` over HTTPS with JSON bodies. ```bash curl "https://api.paycaptain.com/employees?company=ABC" \ -H "X-API-Key: $PAYCAPTAIN_API_KEY" ``` A `200` returns a page of up to 50 employees. A `204` means there are no more pages. ## 3. Create an employee `POST /employee` creates the employee, or updates them if the `hrEmployeeId` already exists. This is the smallest valid record for a salaried employee: ```json { "company": "ABC", "hrEmployeeId": "9876543210", "payrollCode": "123", "firstName": "Jason", "lastName": "Smith", "dateOfBirth": "2000-01-01", "gender": "Male", "email": "jason@example.com", "mailingStreet": "1 Main Street", "mailingCity": "Green City", "mailingStateProvince": "Happy County", "mailingPostalCode": "SW1A 1AA", "mailingCountry": "United Kingdom", "startDate": "2023-12-01", "payBasis": "Salaried", "salary": 12000, "payEffectiveDate": "2023-12-01", "payGroup": "Monthly" } ``` ```bash curl -X POST "https://api.paycaptain.com/employee" \ -H "X-API-Key: $PAYCAPTAIN_API_KEY" \ -H "Content-Type: application/json" \ -d @employee.json ``` Check every value against the [Employee fields](https://developer.paycaptain.com/fields/employee) before sending. Only send [PAYE employees](https://developer.paycaptain.com/rules#who-to-send). ## 4. Send a shift Shifts must be [approved](https://developer.paycaptain.com/rules#approval) in your system first. ```json { "shiftId": "SH-000481", "company": "ABC", "hrEmployeeId": "9876543210", "payrollCode": "123", "date": "2026-11-02", "type": "Working", "payBasis": "Hourly", "units": 7.5, "rate": 15.25 } ``` Pay elements work the same way through `POST /payment`. See [Shift fields](https://developer.paycaptain.com/fields/shift) and [Payment fields](https://developer.paycaptain.com/fields/payment). ## 5. Read payslips ```bash curl "https://api.paycaptain.com/payslips?company=ABC&payPeriod=2026-11&page=1" \ -H "X-API-Key: $PAYCAPTAIN_API_KEY" ``` `payPeriod` is the Pay Period reference shown in the PayCaptain app. Keep requesting the next page until you receive a `204`. ## Before you go live - Enforce every [field rule](https://developer.paycaptain.com/fields/employee) and [business rule](https://developer.paycaptain.com/rules) in your own validation layer. - Stay inside the [rate limits](https://developer.paycaptain.com/reliability#rate-limits) and retry safely. - Pass the [compliance tests](https://developer.paycaptain.com/testing) in your deployment pipeline. Source: https://developer.paycaptain.com/quickstart --- # Trust and security > What PayCaptain does to protect payroll data, and what your integration must do. ## Shared responsibility Payroll data is personal and financial. An integration is only as trustworthy as its weakest side, so the responsibilities are split plainly. | PayCaptain | Your integration | | --- | --- | | HMRC-recognised payroll software | Sends only PAYE employees, with complete and valid data | | ISO 27001 and Cyber Essentials Plus certified | Keeps the API key secret and server-side | | Core software operated by PayCaptain UK personnel | Controls who can view data and trigger API calls | | HTTPS on every endpoint | Enforces TLS 1.2 or higher and encrypts stored data | | Publishes every rule on this site | Validates against those rules before sending | ## API keys Your API key authenticates your application to PayCaptain. Treat it as a privileged credential. > **Must** The API key is a secret. It must never be visible to end users. - Store the key in a secrets manager or environment variable. Never hard-code it. - Call the API from your server only. Never from a browser or a mobile app. - Keep the key out of client-side code, app bundles and every log. - No end user of your application may view, retrieve or export it. - If you suspect a compromise, rotate the key and tell [developer@paycaptain.com](mailto:developer@paycaptain.com) immediately. - Keep an audit log of every API call: timestamp, endpoint, and the user or process that triggered it. ## Data protection Your application handles employee personal data as a data processor under UK GDPR. These are legal obligations, not recommendations. | Principle | What you must do | Reference | | --- | --- | --- | | Data minimisation | Collect and send only the fields your integration needs. | UK GDPR Art. 5(1)(c) | | Purpose limitation | Use the data for payroll processing only. | UK GDPR Art. 5(1)(b) | | Encryption in transit | Enforce TLS 1.2 or higher on all API traffic. | ISO 27001 A.10 | | Encryption at rest | Encrypt stored employee data with AES-256 or equivalent. | ISO 27001 A.10 | | Access control | Use role-based access. Only authorised people and processes see data or trigger calls. | ISO 27001 A.9 | | Retention | Keep employee data no longer than necessary. | UK GDPR Art. 5(1)(e) | | Breach notification | Tell PayCaptain within 24 hours of any breach affecting data sent to or received from the API. | UK GDPR Art. 33 | | Right to erasure | Be able to delete an individual's personal data on request. | UK GDPR Art. 17 | ## Compliance API calls that do not meet the rules on this site are rejected. Repeated non-compliance may lead to API access being revoked. The [compliance tests](https://developer.paycaptain.com/testing) let you prove your integration meets every rule before it goes live. ## This site This site sets no cookies, runs no analytics and loads nothing from third parties. Fonts, styles and scripts are served from the same origin under a strict Content Security Policy. Source: https://developer.paycaptain.com/trust --- # Employee fields > Every field accepted by POST /employee and returned by GET /employees, with its validation rule. Send one JSON object per employment to `POST /employee`. A record is rejected unless every required field is present and valid, so check each field here and then the cross-field [business rules](https://developer.paycaptain.com/rules). Machine-readable: [JSON Schema](https://developer.paycaptain.com/schemas/employee.schema.json) · [OpenAPI](https://developer.paycaptain.com/openapi.json) ## Identity These three fields decide which record PayCaptain creates or updates. See [Identity and payroll codes](https://developer.paycaptain.com/rules#identity-and-payroll-codes). ### `company` Company code issued by the PayCaptain team. - Type: string - Required - Rule: Must match your PayCaptain company code exactly. - Example: `"ABC"` ### `hrEmployeeId` Your system's identifier for the person. PayCaptain upserts on this value. - Type: string - Required - Rule: Unique per person. Never changes, and is the same across all of that person's employments. - Example: `"9876543210"` ### `payrollCode` Identifier for one employment period. HMRC uses this code. - Type: string - Required - Rule: Unique per employment. Never changed once assigned, never reused. A returning employee gets a new code. - Example: `"123"` ## Personal details ### `firstName` First name. - Type: string - Required - Rule: Must not be empty. - Example: `"Jason"` ### `middleName` Middle name. - Type: string - Optional - Example: `"Z"` ### `lastName` Last name. - Type: string - Required - Rule: Must not be empty. - Example: `"Smith"` ### `dateOfBirth` Date of birth. - Type: string - Required - Rule: A valid date in the past. - Format: YYYY-MM-DD - Example: `"2000-01-01"` ### `gender` Gender as reported to HMRC. - Type: string - Required - Rule: Exactly one of the allowed values. This is an HMRC requirement. - Allowed values: `"Male"`, `"Female"` - Example: `"Male"` ### `email` Home email address. - Type: string - Required - Rule: Valid email format. - Format: Email address - Example: `"jason@example.com"` ### `phone` Phone number. Mobile preferred. - Type: string - Optional - Example: `"08000885262"` ### `photoUrl` URL of a photo of the employee. - Type: string - Optional - Format: URL - Example: `"https://example.com/jason.jpg"` ### `niNumber` National Insurance number. - Type: string - Optional - Rule: A valid UK National Insurance number, no spaces. If unknown, omit the field. Never send a placeholder. - Pattern: `^(?!BG|GB|KN|NK|NT|TN|ZZ)[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z][0-9]{6}[A-D]$` - Example: `"AB123456C"` ## Home address All five fields are required together and must form a complete UK residential address. ### `mailingStreet` Street. - Type: string - Required - Rule: Must not be empty. - Example: `"1 Main Street"` ### `mailingCity` Town or city. - Type: string - Required - Rule: Must not be empty. - Example: `"Green City"` ### `mailingStateProvince` County. - Type: string - Required - Rule: Must not be empty. - Example: `"Happy County"` ### `mailingPostalCode` Postcode. - Type: string - Required - Rule: A valid UK postcode, with the single space between outward and inward parts. Trim before testing. - Pattern: `^(?:[Gg][Ii][Rr] 0[Aa]{2}|(?:[A-Za-z][0-9]{1,2}|[A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2}|[A-Za-z][0-9][A-Za-z]|[A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]) [0-9][A-Za-z]{2})$` - Example: `"SW1A 1AA"` ### `mailingCountry` Country. - Type: string - Required - Rule: Must be exactly "United Kingdom". - Allowed values: `"United Kingdom"` - Example: `"United Kingdom"` ## Employment Date relationships are enforced across fields. See [Date rules](https://developer.paycaptain.com/rules#dates). ### `startDate` Date this employment started. - Type: string - Required - Rule: Employment periods for the same hrEmployeeId must not overlap. - Format: YYYY-MM-DD - Example: `"2023-12-01"` ### `leaveDate` Date this employment ended. Omit or send null while employed. - Type: string - Optional - Rule: If set, must be on or after startDate. - Format: YYYY-MM-DD - Example: `null` ## Pay Any change to payBasis, salary or rateOfPay must move payEffectiveDate to the date of that change. See [Pay changes](https://developer.paycaptain.com/rules#pay-changes). ### `payBasis` How the employee is paid. - Type: string - Required - Rule: Exactly one of the allowed values. Decides whether salary or rateOfPay is required. - Allowed values: `"Hourly"`, `"Salaried"` - Example: `"Salaried"` ### `salary` Annual salary. - Type: number - Required when payBasis is "Salaried" - Rule: Annual figure. - Example: `12000` ### `rateOfPay` Hourly rate of pay. - Type: number - Required when payBasis is "Hourly" - Rule: Rate per hour. - Example: `15.25` ### `payEffectiveDate` Date the current salary or rate took effect. - Type: string - Required - Rule: Between startDate and leaveDate inclusive. Must equal startDate when the employee is first created. - Format: YYYY-MM-DD - Example: `"2023-12-01"` ### `payGroup` Pay Group the employee is paid in. - Type: string - Required - Rule: Must match the name of a Pay Group configured in PayCaptain. - Example: `"Monthly"` ## Job ### `location` Work location. - Type: string - Optional - Example: `"Baker Street"` ### `department` Department. - Type: string - Optional - Example: `"Marketing"` ### `team` Team. - Type: string - Optional - Example: `"Content"` ### `jobGrade` Job grade. - Type: string - Optional - Example: `"A1"` ### `jobTitle` Job title. - Type: string - Optional - Example: `"Content Marketing"` ### `jobEffectiveDate` Date the job title took effect. - Type: string - Optional - Format: YYYY-MM-DD - Example: `"2023-12-01"` ### `probationEndDate` Date the probation period ends. - Type: string - Optional - Format: YYYY-MM-DD - Example: `"2024-02-01"` ### `contractType` Contract type. - Type: string - Optional - Allowed values: `"Full Time"`, `"Part Time"` - Example: `"Full Time"` ## Working pattern ### `normalWeeklyHours` Normal weekly hours. - Type: number - Optional - Rule: Hours as a whole or decimal number (40 or 37.5). If your source system stores hundredths (4000), divide by 100 first. - Range: minimum 0 - Example: `40` ### `daysWorkedPerWeek` Working days per week. - Type: number - Optional - Range: minimum 0, maximum 7 - Example: `5` ### `monday` Works on a Monday. - Type: boolean - Optional - Example: `true` ### `tuesday` Works on a Tuesday. - Type: boolean - Optional - Example: `true` ### `wednesday` Works on a Wednesday. - Type: boolean - Optional - Example: `true` ### `thrursday` Works on a Thursday. - Type: boolean - Optional - Rule: The field name is spelled this way in API v1.0.1. Send it exactly as shown. - Example: `true` ### `friday` Works on a Friday. - Type: boolean - Optional - Example: `true` ### `saturday` Works on a Saturday. - Type: boolean - Optional - Example: `false` ### `sunday` Works on a Sunday. - Type: boolean - Optional - Example: `false` ## Bank account ### `bankName` Name of the bank. - Type: string - Optional - Example: `"Monzo"` ### `bankAccountName` Name on the account. - Type: string - Optional - Example: `"Jason Smith"` ### `bankAccountNumber` UK bank account number. - Type: string - Optional - Rule: Exactly 8 digits, sent as a string so leading zeros survive. - Pattern: `^[0-9]{8}$` - Example: `"87654321"` ### `bankAccountSortCode` UK sort code. - Type: string - Optional - Rule: Exactly 6 digits, no hyphens, sent as a string so leading zeros survive. - Pattern: `^[0-9]{6}$` - Example: `"123456"` ## HMRC starter details ### `starterDeclaration` HMRC starter declaration. - Type: string - Optional - Rule: Exactly one of the allowed values, per the HMRC starter checklist. - Allowed values: `"A"`, `"B"`, `"C"` - Example: `"A"` ### `studentLoanPlanOne` Repays Student Loan Plan 1. - Type: boolean - Optional - Default: `false` - Example: `false` ### `studentLoanPlanTwo` Repays Student Loan Plan 2. - Type: boolean - Optional - Default: `false` - Example: `false` ### `studentLoanPlanFour` Repays Student Loan Plan 4. - Type: boolean - Optional - Default: `false` - Example: `false` ### `studentLoanPostGraduate` Repays a Postgraduate Loan. - Type: boolean - Optional - Default: `false` - Example: `false` ## Minimal valid record ```json { "company": "ABC", "hrEmployeeId": "9876543210", "payrollCode": "123", "firstName": "Jason", "lastName": "Smith", "dateOfBirth": "2000-01-01", "gender": "Male", "email": "jason@example.com", "mailingStreet": "1 Main Street", "mailingCity": "Green City", "mailingStateProvince": "Happy County", "mailingPostalCode": "SW1A 1AA", "mailingCountry": "United Kingdom", "startDate": "2023-12-01", "payBasis": "Salaried", "salary": 12000, "payEffectiveDate": "2023-12-01", "payGroup": "Monthly" } ``` Source: https://developer.paycaptain.com/fields/employee --- # Shift fields > Every field accepted by POST /shift, with its validation rule. Send one JSON object per shift or absence day to `POST /shift`. Only [approved](https://developer.paycaptain.com/rules#approval) records may be sent. To withdraw a shift, [archive it](https://developer.paycaptain.com/rules#archiving). Machine-readable: [JSON Schema](https://developer.paycaptain.com/schemas/shift.schema.json) · [OpenAPI](https://developer.paycaptain.com/openapi.json) ## Identity ### `shiftId` Your identifier for the shift record. - Type: string - Required - Rule: Unique in your system. Resubmitting the same shiftId updates the record. - Example: `"SH-000481"` ### `company` Company code issued by the PayCaptain team. - Type: string - Required - Rule: Must match your PayCaptain company code exactly. - Example: `"ABC"` ### `hrEmployeeId` The person who worked the shift. - Type: string - Required - Rule: Must match an employee already sent to PayCaptain. - Example: `"9876543210"` ### `payrollCode` The employment the shift belongs to. - Type: string - Required - Rule: Must match that person's active employment. - Example: `"123"` ## Shift ### `date` Date of the shift. - Type: string - Required - Format: YYYY-MM-DD - Example: `"2026-11-02"` ### `type` What the record represents. - Type: string - Required - Rule: Exactly one of the allowed values. - Allowed values: `"Working"`, `"Holiday"`, `"Sickness"`, `"Unpaid Leave"` - Example: `"Working"` ### `payBasis` How the shift is paid. - Type: string - Required - Rule: Exactly one of the allowed values. Decides whether units are hours or days. - Allowed values: `"Hourly"`, `"Daily"` - Example: `"Hourly"` ### `units` Quantity worked. - Type: number - Required - Rule: Hours when payBasis is "Hourly", days when "Daily". - Range: minimum 0 - Example: `7.5` ### `rate` Rate of pay for this shift. - Type: number - Required - Rule: Per hour or per day, matching payBasis. - Range: minimum 0 - Example: `15.25` ### `sicknessDayType` Whether a sickness day is a qualifying day. - Type: string - Optional - Rule: Only meaningful when type is "Sickness". "Qualifying", "Q" and "Y" mean a qualifying day; "Non Qualifying" and "N" mean it is not. - Allowed values: `"Qualifying"`, `"Q"`, `"Y"`, `"Non Qualifying"`, `"N"` - Example: `"Qualifying"` ### `reason` Reason for an absence, mainly for sickness records. - Type: string - Optional - Example: `"Flu"` ### `archived` Archive the shift so it is not paid. - Type: boolean - Optional - Rule: Send true with the same shiftId to remove the shift from payroll. Records are archived, never deleted. - Default: `false` - Example: `false` ## Cost allocation ### `location` Location worked. - Type: string - Optional - Example: `"Baker Street"` ### `department` Department worked. - Type: string - Optional - Example: `"Marketing"` ### `team` Team worked. - Type: string - Optional - Example: `"Content"` ### `job` Job worked. - Type: string - Optional - Example: `"Content Marketing"` ## Minimal valid record ```json { "shiftId": "SH-000481", "company": "ABC", "hrEmployeeId": "9876543210", "payrollCode": "123", "date": "2026-11-02", "type": "Working", "payBasis": "Hourly", "units": 7.5, "rate": 15.25 } ``` Source: https://developer.paycaptain.com/fields/shift --- # Payment fields > Every field accepted by POST /payment, with its validation rule. A payment is a pay element such as a bonus, expense or deduction. Send one JSON object per pay element to `POST /payment`. To withdraw a payment, [archive it](https://developer.paycaptain.com/rules#archiving). Machine-readable: [JSON Schema](https://developer.paycaptain.com/schemas/payment.schema.json) · [OpenAPI](https://developer.paycaptain.com/openapi.json) ## Identity ### `paymentId` Your identifier for the payment record. - Type: string - Required - Rule: Unique in your system. Resubmitting the same paymentId updates the record. - Example: `"PM-000912"` ### `company` Company code issued by the PayCaptain team. - Type: string - Required - Rule: Must match your PayCaptain company code exactly. - Example: `"ABC"` ### `hrEmployeeId` The person being paid. - Type: string - Required - Rule: Must match an employee already sent to PayCaptain. - Example: `"9876543210"` ### `payrollCode` The employment the payment belongs to. - Type: string - Required - Rule: Must match that person's active employment. - Example: `"123"` ## Payment ### `payCode` Pay Code for this pay element. - Type: string - Required - Rule: Must match a Pay Code Name configured in PayCaptain. - Example: `"Bonus"` ### `date` Date of the payment. - Type: string - Required - Format: YYYY-MM-DD - Example: `"2026-11-28"` ### `units` Quantity, for example hours. - Type: number - Required - Example: `1` ### `rate` Rate per unit. - Type: number - Required - Example: `250` ### `description` Text shown on the payslip. - Type: string - Optional - Example: `"Quarterly bonus"` ### `information` Extra detail shown behind the help icon on the payslip. - Type: string - Optional - Example: `"Q3 performance bonus"` ### `archived` Archive the payment so it is not paid. - Type: boolean - Optional - Rule: Send true with the same paymentId to remove the payment from payroll. Records are archived, never deleted. - Default: `false` - Example: `false` ## Cost allocation ### `location` Location worked. - Type: string - Optional - Example: `"Baker Street"` ### `department` Department worked. - Type: string - Optional - Example: `"Marketing"` ### `team` Team worked. - Type: string - Optional - Example: `"Content"` ### `job` Job worked. - Type: string - Optional - Example: `"Content Marketing"` ## Minimal valid record ```json { "paymentId": "PM-000912", "company": "ABC", "hrEmployeeId": "9876543210", "payrollCode": "123", "payCode": "Bonus", "date": "2026-11-28", "units": 1, "rate": 250 } ``` Source: https://developer.paycaptain.com/fields/payment --- # Payslip fields > Every field returned by GET /payslips. `GET /payslips` returns an array. Each item holds the objects below. These are read-only values produced by PayCaptain, so there is nothing to validate before sending, but field names must be read exactly as spelled. Machine-readable: [OpenAPI](https://developer.paycaptain.com/openapi.json) ## `payslip` | Field | Type | Description | | --- | --- | --- | | `id` | string | Payslip identifier. | | `payeReference` | string | Employer PAYE reference. | | `companyName` | string | Company name. | | `payPeriod` | string | Pay Period reference. | | `published` | boolean | Whether the payslip is published to the employee. | | `payPeriodStatus` | string | Status of the pay period. | | `payGroup` | string | Pay Group name. | | `taxCode` | string | Tax code applied. | | `w1m1` | boolean | Tax calculated on a week 1 / month 1 basis. | | `niCategory` | string | National Insurance category letter. | | `date` | string | Pay date. | | `periodNumber` | integer | Tax period number. | | `pensionNotice` | string | Pension notice text. | | `message` | string | Message shown on the payslip. | | `taxYear` | string | Tax year. | | `url` | string | Link to the payslip. | ## `employee` | Field | Type | Description | | --- | --- | --- | | `hrEmployee_Id` | string | Your identifier for the person. Note the spelling differs from hrEmployeeId on requests. | | `payrollCode` | string | Payroll code of the employment. | | `firstName` | string | First name. | | `middleName` | string | Middle name. | | `lastName` | string | Last name. | | `email` | string | Email address. | | `mailingStreet` | string | Home address: street. | | `mailingCity` | string | Home address: town or city. | | `mailingState` | string | Home address: county. | | `mailingPostalCode` | string | Home address: postcode. | | `MailingCountry` | string | Home address: country. Note the capital M. | | `phone` | string | Phone number. | | `niNumber` | string | National Insurance number. | ## `totals` | Field | Type | Description | | --- | --- | --- | | `gross` | number | Gross pay this period. | | `taxable` | number | Taxable pay this period. | | `net` | number | Net pay this period. | | `benefits` | number | Benefits this period. | | `tax` | number | Income tax this period. | | `employerNationalInsurance` | number | Employer National Insurance this period. | | `employeeNationalInsurance` | number | Employee National Insurance this period. | | `employeePensoin` | number | Employee pension this period. Spelled this way in API v1.0.1. | | `employerPensoin` | number | Employer pension this period. Spelled this way in API v1.0.1. | ## `ytd` | Field | Type | Description | | --- | --- | --- | | `gross` | number | Gross pay, year to date. | | `taxable` | number | Taxable pay, year to date. | | `net` | number | Net pay, year to date. | | `benefits` | number | Benefits, year to date. | | `tax` | number | Income tax, year to date. | | `employeePension` | number | Employee pension, year to date. | | `employerPension` | number | Employer pension, year to date. | ## `previousEmployment` | Field | Type | Description | | --- | --- | --- | | `taxable` | number | Taxable pay from previous employment this tax year. | | `tax` | number | Tax paid in previous employment this tax year. | ## `thisEmployment` | Field | Type | Description | | --- | --- | --- | | `taxable` | number | Taxable pay in this employment this tax year. | | `tax` | number | Tax paid in this employment this tax year. | ## `payslipLines[]` | Field | Type | Description | | --- | --- | --- | | `payslipLineId` | string | Line identifier. | | `payslipId` | string | Parent payslip identifier. | | `units` | integer | Units. | | `amount` | number | Amount per unit. | | `total` | number | Line total. | | `code` | string | Pay Code. | | `description` | string | Text shown on the payslip. | | `locationCode` | string | Location. | | `departmentCode` | string | Department. | | `teamCode` | string | Team. | | `jobCode` | string | Job. | | `isDeduction` | boolean | Line is a deduction. | | `isBenefit` | boolean | Line is a benefit. | | `excludedfromPayslip` | boolean | Line is hidden on the employee's payslip. | | `information` | string | Extra detail shown behind the help icon. | Source: https://developer.paycaptain.com/fields/payslip --- # Business rules > The rules that span more than one field or more than one record. Enforce them alongside the field rules. Field pages cover what a single value must look like. This page covers everything else. Every rule here is mandatory. ## Who to send PayCaptain processes UK PAYE payroll. Send a person only if they are paid through your PAYE payroll. | Send | Do not send | | --- | --- | | Full-time PAYE employees | Contractors and freelancers | | Part-time PAYE employees | Self-employed individuals | | PAYE casual workers | Agency workers paid by the agency | ## Identity and payroll codes `hrEmployeeId` identifies the **person**. `payrollCode` identifies one **employment**. They are different things. ```text Person hrEmployeeId "P001" never changes Employment 1 payrollCode "EMP-101" 2022-03-01 to 2023-06-30 ended Employment 2 payrollCode "EMP-205" 2024-01-15 to (none) active ``` - One `hrEmployeeId` per person, the same across all of their employments. - Every employment has its own `payrollCode`. No two records share one. - A `payrollCode` never changes once assigned and is never reused. - A person who leaves and returns gets a new `payrollCode`. - Employment periods for the same `hrEmployeeId` must never overlap. ## Dates All dates are ISO 8601 calendar dates, `YYYY-MM-DD`. | Rule | Check | | --- | --- | | Leave date is not before start date | `leaveDate >= startDate`, when `leaveDate` is set | | Pay effective date is inside the employment | `startDate <= payEffectiveDate <= leaveDate` | | New employees start pay on day one | `payEffectiveDate === startDate` on first creation | | Employments do not overlap | No two records with the same `hrEmployeeId` have overlapping date ranges | ```js function validateEmployeeDates({ startDate, leaveDate, payEffectiveDate, isNew }) { // ISO dates compare correctly as strings if (leaveDate && leaveDate < startDate) throw new Error("leaveDate is before startDate"); if (payEffectiveDate < startDate) throw new Error("payEffectiveDate is before startDate"); if (leaveDate && payEffectiveDate > leaveDate) throw new Error("payEffectiveDate is after leaveDate"); if (isNew && payEffectiveDate !== startDate) throw new Error("new employee: payEffectiveDate must equal startDate"); } ``` ## Pay changes `payBasis` decides which pay field is required: `salary` (annual) for `"Salaried"`, `rateOfPay` (hourly) for `"Hourly"`. > **Must** Whenever `payBasis`, `salary` or `rateOfPay` changes, set `payEffectiveDate` to the date of that change. ## Approval > **Must** Only approved shifts may be sent. Your application needs an approval step, and a shift must not reach `POST /shift` until an authorised person has approved it. ## Archiving Records are never deleted. To remove a shift or payment from payroll, resubmit it with the same `shiftId` or `paymentId` and `archived: true`. ## Change detection > **Must** Only send a record when at least one field value has changed since the last successful submission. Keep the last successfully submitted version of each record and compare before every call. ```js function hasChanged(current, lastSent) { if (!lastSent) return true; // never sent return Object.keys(current).some((field) => current[field] !== lastSent[field]); } if (hasChanged(employee, lastSentEmployee)) { await submitToPayCaptain(employee); saveLastSent(employee); } ``` ## If a check fails Do not call the API. Log the failure, tell the user what to correct, and submit only once every field rule and every rule on this page passes. Source: https://developer.paycaptain.com/rules --- # API reference > Five endpoints, one header, JSON in and out. | | | | --- | --- | | Base URL | `https://api.paycaptain.com` | | Authentication | `X-API-Key` header on every request | | Format | JSON request and response bodies | | Transport | HTTPS, TLS 1.2 or higher | | Specification | [OpenAPI 3.0](https://developer.paycaptain.com/openapi.json) | ## POST /employee Create or update an employee. Request body: one [Employee](https://developer.paycaptain.com/fields/employee) object. Responses: `200` Success · `400` Error. Fix the record; do not retry unchanged. ## GET /employees Get employees, 50 per page. Returns: an array of [Employee](https://developer.paycaptain.com/fields/employee) objects. | Query parameter | Type | Required | Description | | --- | --- | --- | --- | | `company` | string | Yes | Your PayCaptain company code. | | `lastModifiedDate` | string (YYYY-MM-DD) | No | Only employees modified after this date (YYYY-MM-DD). | | `includeFormer` | boolean | No | Include former employees. Default: `false`. | | `page` | integer | No | Page number. | Responses: `200` A page of employees · `204` No more pages · `400` Error ## POST /shift Create, update or archive a shift. Request body: one [Shift](https://developer.paycaptain.com/fields/shift) object. Responses: `200` Success · `400` Error. Fix the record; do not retry unchanged. ## POST /payment Create, update or archive a pay element. Request body: one [Payment](https://developer.paycaptain.com/fields/payment) object. Responses: `200` Success · `400` Error. Fix the record; do not retry unchanged. ## GET /payslips Get payslips and their lines for a pay period, 50 per page. Returns: an array of [Payslip](https://developer.paycaptain.com/fields/payslip) objects. | Query parameter | Type | Required | Description | | --- | --- | --- | --- | | `company` | string | Yes | Your PayCaptain company code. | | `payPeriod` | string | Yes | Pay Period reference, as shown in the PayCaptain app. | | `page` | integer | No | Page number. | Responses: `200` A page of payslips · `204` No more pages · `400` Error ## Status codes | Code | Meaning | What to do | | --- | --- | --- | | `200` | Success. Data returned or record accepted. | Continue. | | `204` | No more pages. | Stop paginating. | | `400` | The request was rejected. | Fix the record. Do not retry it unchanged. | | `429` | Rate limited. | [Back off and retry](https://developer.paycaptain.com/reliability#retries). | | `5xx` | Server error. | [Back off and retry](https://developer.paycaptain.com/reliability#retries). | Source: https://developer.paycaptain.com/api --- # Limits and retries > Rate limits, retry behaviour and pagination. ## Rate limits | Limit | Threshold | When you reach it | | --- | --- | --- | | Requests per second | 5 | Queue and retry with backoff | | Requests per minute | 100 | Queue and retry with backoff | | Requests per hour | 2,000 | Queue and resume in the next hour | | Concurrent connections | 3 | Queue additional requests | ## Retries On `429` or any `5xx`, wait and retry with exponential backoff: start at 1 second, double each time, cap at 60 seconds. After 5 consecutive failures, stop and alert someone. Never retry a `400`. ```js async function callWithRetry(apiCall, maxAttempts = 5) { let delay = 1000; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const response = await apiCall(); if (response.status >= 200 && response.status < 300) return response; if (response.status !== 429 && response.status < 500) { throw new Error(`PayCaptain rejected the request: ${response.status}`); // do not retry } await new Promise((resolve) => setTimeout(resolve, delay)); delay = Math.min(delay * 2, 60000); } throw new Error("PayCaptain unavailable after 5 attempts. Halt and alert."); } ``` ## Pagination `GET /employees` and `GET /payslips` return up to 50 records per page. Request pages in order until you receive a `204`. ```js async function fetchAll(path, params) { const records = []; for (let page = 1; ; page++) { const response = await callWithRetry(() => get(path, { ...params, page })); if (response.status === 204) return records; records.push(...(await response.json())); } } ``` Source: https://developer.paycaptain.com/reliability --- # Build with AI > Everything on this site in a form an agent or AI builder tool can read, plus a prompt sequence that produces a compliant integration. ## Machine-readable resources Humans and agents read the same rules. Every resource below is generated from the same source as the pages you are reading. | Resource | URL | Use it for | | --- | --- | --- | | Site index | [/llms.txt](https://developer.paycaptain.com/llms.txt) | Finding the right page | | Whole site, one file | [/llms-full.txt](https://developer.paycaptain.com/llms-full.txt) | Loading every rule into context | | Any page as Markdown | add `.md` to the page URL, for example [/fields/employee.md](https://developer.paycaptain.com/fields/employee.md) | Reading one page cleanly | | OpenAPI 3.0 | [/openapi.json](https://developer.paycaptain.com/openapi.json) | Generating clients | | Employee schema | [/schemas/employee.schema.json](https://developer.paycaptain.com/schemas/employee.schema.json) | Validating a record in code | | Shift schema | [/schemas/shift.schema.json](https://developer.paycaptain.com/schemas/shift.schema.json) | Validating a record in code | | Payment schema | [/schemas/payment.schema.json](https://developer.paycaptain.com/schemas/payment.schema.json) | Validating a record in code | | Field data | [/fields.json](https://developer.paycaptain.com/fields.json) | Building forms and validators | Requests that send `Accept: text/markdown` receive the Markdown version of any page. JSON Schema covers single-field rules and conditional required fields. Rules that compare dates or compare records with each other are on [Business rules](https://developer.paycaptain.com/rules) and must be implemented in code. ## Instructions for agents If you are an AI agent generating an integration, these are not optional: - Implement every field rule and every business rule exactly as written. There is no discretion to skip or relax a rule. - Keep the API key server-side, in a secret store. Never expose it to a client. - Validate every record before calling the API, and send only records that have changed. - Send shifts only after approval. Send PAYE employees only. - Generate and pass the [compliance tests](https://developer.paycaptain.com/testing). ## Prompt sequence A good integration is built in steps, one concern per prompt, foundation first. Review the output after each step before moving on. | Step | Build | Why it comes here | | --- | --- | --- | | 1 | Project setup: environment config, server-side API client for `https://api.paycaptain.com`, structured logging, rate limiting | Secure connectivity exists before any feature does | | 2 | Authentication: secure login, MFA, role-based access (Admin, HR Manager, Team Manager, Viewer), 30-minute session timeout, login audit log | Access to payroll data is controlled and auditable from the start | | 3 | Validation engine: one module enforcing every field and business rule, with unit tests | Every screen that sends data uses the same checked path | | 4 | Employees: view, add, edit, re-hire, set leave dates | Shifts and payments both depend on correct employees | | 5 | Shifts: create, approve, submit, archive | Approval is mandatory before submission | | 6 | Pay elements: create, select a valid Pay Code, submit, archive | Invalid Pay Codes are rejected | | 7 | Payslip viewer (optional): read-only, access-controlled, logged | Payslips are sensitive even when read-only | | 8 | Security hardening: input sanitisation, CSRF, Content Security Policy, error handling, retention | The pre-launch pass | | 9 | Compliance test suite wired into CI as a deployment gate | Proof that every rule is met | ## Include in every prompt ```text Read and comply with the PayCaptain developer documentation at https://developer.paycaptain.com/llms-full.txt. The PayCaptain API key must be stored as a secret and must never be accessible to end users. All validation must pass before any API call is made. Only send data to the PayCaptain API if the record has actually changed. This is an enterprise application handling sensitive UK payroll data. Apply security best practices aligned with ISO 27001 and UK GDPR. ``` Source: https://developer.paycaptain.com/agents --- # Compliance tests > The 60 tests your integration must pass before it is deployed. Each test maps to a rule on this site. Run them automatically in your deployment pipeline, and block the release if any test fails. Fix the code, not the pipeline. Machine-readable: [tests.json](https://developer.paycaptain.com/tests.json) ## Security and data protection 10 tests. | ID | Type | Test | | --- | --- | --- | | S-01 | Security | API key is loaded from environment variables or a secrets manager and is never hard-coded in source files. | | S-02 | Security | API key does not appear in any client-side bundle. | | S-03 | Security | API key does not appear in any log output. | | S-04 | Security | All PayCaptain API calls are made server-side only. | | S-05 | Integration | All API communication uses HTTPS. | | S-06 | E2E | Authentication is required for all routes that display or modify employee data. | | S-07 | E2E | Role-based access control is enforced. | | S-08 | E2E | Session expires after the configured inactivity timeout. | | S-09 | Integration | Login attempts are recorded in the audit log. | | S-10 | Security | CSRF protection is present on all state-changing endpoints. | ## Employee data 22 tests. | ID | Type | Test | | --- | --- | --- | | E-01 | Unit | Reject an employee record missing any required field. | | E-02 | Unit | Reject if payBasis is "Salaried" and salary is missing. | | E-03 | Unit | Accept gender "Male" and "Female". Reject all other values. | | E-04 | Unit | Accept valid UK postcodes. Reject invalid postcodes. | | E-05 | Unit | Reject if any address field is empty. | | E-06 | Unit | Reject if leaveDate is before startDate. | | E-07 | Unit | Reject if payEffectiveDate is before startDate or after leaveDate. | | E-08 | Unit | For a new employee, reject if payEffectiveDate does not equal startDate. | | E-09 | Unit | When salary changes, reject if payEffectiveDate is not updated. | | E-10 | Unit | When rateOfPay changes, reject if payEffectiveDate is not updated. | | E-11 | Unit | When payBasis changes, reject if payEffectiveDate is not updated. | | E-12 | Unit | Reject if two employment records share the same payrollCode. | | E-13 | Unit | Reject if payrollCode is modified on an existing record. | | E-14 | Unit | Reject if a re-hired employee reuses a previous payrollCode. | | E-15 | Unit | Reject if employment date ranges overlap for the same hrEmployeeId. | | E-16 | Unit | Accept non-overlapping employments for the same person. | | E-17 | Integration | Do not send an unchanged employee record to the API. | | E-18 | Integration | Send an employee record when at least one field has changed. | | E-19 | Unit | Reject records for non-PAYE workers. | | E-20 | Unit | Reject if payBasis is not exactly "Hourly" or "Salaried". | | E-21 | Unit | If niNumber is provided, validate the UK National Insurance number format. | | E-22 | Unit | If starterDeclaration is provided, accept only "A", "B" or "C". | ## Shifts 9 tests. | ID | Type | Test | | --- | --- | --- | | A-01 | Unit | Reject a shift record that has not been approved. | | A-02 | Integration | Accept a shift record that has been explicitly approved. | | A-03 | Unit | Reject if type is not one of the allowed values. | | A-04 | Unit | Reject if hrEmployeeId or payrollCode does not match an active employee. | | A-05 | Unit | Reject if shiftId is missing or empty. | | A-06 | Integration | Do not send an unchanged shift record to the API. | | A-07 | Integration | Setting archived to true triggers a resubmission. | | A-08 | E2E | A user with the Viewer role cannot approve shift records. | | A-09 | Unit | Reject if payBasis is not "Hourly" or "Daily". | ## Payments 7 tests. | ID | Type | Test | | --- | --- | --- | | P-01 | Unit | Reject if payCode is missing, empty or not a configured Pay Code Name. | | P-02 | Unit | Reject if hrEmployeeId or payrollCode does not match an active employee. | | P-03 | Unit | Reject if paymentId is missing or empty. | | P-04 | Integration | Do not send an unchanged payment record to the API. | | P-05 | Integration | Setting archived to true triggers a resubmission. | | P-06 | Unit | Reject if date is missing or not a valid ISO 8601 date. | | P-07 | E2E | A user with the Viewer role cannot create, edit or archive pay elements. | ## Integration and resilience 12 tests. | ID | Type | Test | | --- | --- | --- | | I-01 | Integration | The X-API-Key header is present on every request. | | I-02 | Integration | On HTTP 429, retry with exponential backoff. | | I-03 | Integration | On HTTP 5xx, retry with exponential backoff. | | I-04 | Integration | After 5 consecutive failures, halt further attempts and alert. | | I-05 | Integration | Never exceed 5 requests per second. | | I-06 | Integration | Never exceed 100 requests per minute. | | I-07 | Integration | On HTTP 400, do not retry. | | I-08 | Integration | GET /employees pagination is followed until a 204. | | I-09 | Integration | GET /payslips pagination is followed until a 204. | | I-10 | Security | No stack traces or internal errors are exposed to the user. | | I-11 | Integration | All create, update and archive operations are recorded in the audit log. | | I-12 | Integration | Never hold more than 3 concurrent connections. | ## Deployment gate ```yaml name: PayCaptain compliance gate on: [push, pull_request] jobs: compliance-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run test:unit -- --tag paycaptain - run: npm run test:integration -- --tag paycaptain - run: npm run test:security - run: npm run test:e2e -- --tag paycaptain deploy: needs: compliance-tests if: success() runs-on: ubuntu-latest steps: - run: echo "Deploy only when every compliance test has passed" ``` ## Generate the suite with AI ```text Generate an automated test suite covering every test listed at https://developer.paycaptain.com/testing.md (S-01 to S-10, E-01 to E-22, A-01 to A-09, P-01 to P-07, I-01 to I-12). Use [your test framework]. Tag every test with "paycaptain". Configure the suite as a CI stage that blocks deployment if any test fails. ``` Source: https://developer.paycaptain.com/testing