Payroll Changes API Guide
What this API does
The Payroll Changes API lets you manage one-time payroll adjustments for team members. Use it for additions to regular payroll, such as bonuses, commissions, allowances, and insurance contributions.
Expense reimbursements are not payroll changes. Use the Expenses API for those.
Payroll changes are tied to an engagement, scheduled for a specific month and year, and move through states until they are applied to payroll or canceled. For more information about how Payroll Changes work in general at Oyster, see Payroll Changes at Oyster.
Key concepts
| Concept | Meaning |
|---|---|
| Engagement | The employment relationship associated with the payroll change |
| Type | The category of adjustment, such as BONUS or COMMISSION |
| Amount | The monetary value, using the engagement’s salary currency |
| Month & Year | The payroll month when the change should be applied |
| State | Current status: PLANNED, APPLIED, or CANCELED |
The engagement.type field in responses is either EMPLOYMENT or CONTRACT.
Endpoints at a glance
| Action | Method | Endpoint | Notes |
|---|---|---|---|
| List payroll changes | GET | /v1/payroll_changes | Supports filtering by engagement and payroll period |
| Retrieve payroll change | GET | /v1/payroll_changes/:id | Returns one payroll change |
| Create payroll change | POST | /v1/payroll_changes | Creates a new PLANNED payroll change |
| Update payroll change | PATCH | /v1/payroll_changes/:id | Only updates the amount |
| Cancel payroll change with reason | POST | /v0.1/payroll_changes/:id/cancel | Requires a cancellation reason |
| Delete payroll change | DELETE | /v1/payroll_changes/:id | Cancels with default reason: Cancelled via API |
Payroll change lifecycle
Payroll changes typically move through this flow:
PLANNED- Initial state after creation.
- The change is scheduled for a future or current open payroll period.
APPLIED- The change has been applied to payroll processing.
CANCELED- The change has been canceled and will not be processed.
- Cancellation is permanent.
Deprecated states may appear in legacy data: PAID, PENDING, and COMMITTED.
Supported payroll change types
| API type | Description |
|---|---|
BONUS | One-time bonus payment |
COMMISSION | Sales commission or performance payment |
EMPLOYER_PENSION_RETIREMENT_CONTRIBUTION | Employer pension or retirement contribution |
HEALTH_INSURANCE | Health insurance supplement |
RISK_INSURANCE | Risk or life insurance supplement |
TRAVEL_ALLOWANCE | Travel allowance |
WORK_FROM_HOME_ALLOWANCE | Work from home allowance |
WELLBEING | Health and wellbeing allowance |
MEALS | Meals or sustenance allowance |
The list of supported types can also be retrieved at runtime from GET /v1/payroll_changes/sources/types.
Invalid types return a 422 Unprocessable Entity error with a message like:
{
"errors": [
{
"message": "Type must have a valid type."
}
]
}Listing payroll changes
Use this endpoint to retrieve payroll changes for your company.
GET /v1/payroll_changes
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
engagementId | string | No | Filter by engagement ID |
from | string | No | Start payroll period in YYYY-MM format, inclusive |
to | string | No | End payroll period in YYYY-MM format, inclusive |
perPage | integer | No | Results per page. Default and maximum are both 100 |
page | integer | No | Page number, starting at 1 |
Example request
GET /v1/payroll_changes?engagementId=eng_abc123&from=2024-01&to=2024-12
Example response
{
"data": [
{
"id": "psc_xyz789",
"type": "BONUS",
"engagement": {
"id": "eng_abc123",
"name": "John Smith",
"type": "EMPLOYMENT"
},
"amount": {
"decimal": "500.00",
"currencyCode": "USD"
},
"month": "3",
"year": "2024",
"notes": "Q1 performance bonus",
"state": "PLANNED"
}
],
"meta": {
"items": 100,
"count": 1,
"page": 1,
"pages": 1
}
}Filtering guidance
- Use
fromandtoto filter by scheduled payroll period. - Both dates must use
YYYY-MM. - Date filtering works across year boundaries, such as
from=2023-11&to=2024-02. - Invalid formats like
YYYY-MM-DDreturn400 Bad Request. - Filtering by a non-existent engagement ID returns an empty result set.
Retrieving a payroll change
Use this endpoint to get details for a specific payroll change.
GET /v1/payroll_changes/:id
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Payroll change ID in strong ID format |
Example request
GET /v1/payroll_changes/psc_xyz789
Example response
{
"data": {
"id": "psc_xyz789",
"type": "BONUS",
"engagement": {
"id": "eng_abc123",
"name": "John Smith",
"type": "EMPLOYMENT"
},
"amount": {
"decimal": "500.00",
"currencyCode": "USD"
},
"month": "3",
"year": "2024",
"notes": "Q1 performance bonus",
"state": "PLANNED"
}
}Common error
| Status | Meaning |
|---|---|
404 Not Found | Payroll change does not exist or does not belong to your company |
Creating a payroll change
Use this endpoint to create a one-time payroll adjustment.
POST /v1/payroll_changes
Request body
| Field | Type | Required | Description |
|---|---|---|---|
engagementId | string | Yes | Engagement ID in strong ID format |
type | string | Yes | Payroll change type |
amount | object | Yes | Amount object |
amount.decimal | string | Yes | Positive decimal string, such as "150.30" |
amount.currencyCode | string | Yes | ISO 4217 currency code matching the engagement currency |
month | string | Yes | Month as a string, "1" to "12". A leading zero is accepted, such as "03". Responses return it unpadded |
year | string | Yes | Year as a string, such as "2026". Must be between "2020" and "2040" |
notes | string | No | Optional description, max 2,000 characters |
Example request
POST /v1/payroll_changes
{
"engagementId": "eng_abc123",
"type": "BONUS",
"amount": {
"decimal": "500.00",
"currencyCode": "USD"
},
"month": "3",
"year": "2024",
"notes": "Q1 performance bonus"
}Example response
{
"data": {
"id": "psc_xyz789",
"type": "BONUS",
"engagement": {
"id": "eng_abc123",
"name": "John Smith",
"type": "EMPLOYMENT"
},
"amount": {
"decimal": "500.00",
"currencyCode": "USD"
},
"month": "3",
"year": "2024",
"notes": "Q1 performance bonus",
"state": "PLANNED"
}
}Creation validation rules
Before creating a payroll change, confirm:
- The amount is positive.
- The amount currency matches the engagement’s salary currency.
- The month and year are in the current or a future open payroll period.
- The selected month is not before the engagement start date.
- The selected month is not after the engagement termination date, if the engagement is terminated.
- The payroll period is not locked.
- The engagement exists and belongs to your company.
- The engagement is active or has a future start date.
- The engagement is not a pay-as-you-go contractor.
- The request does not duplicate a recently created payroll change. A create is rejected as a duplicate when the engagement, type, year, month, amount, currency, and notes all match a payroll change created within the previous 30 seconds. Canceled payroll changes are included in this check.
Common creation errors
| Status | Cause | Example |
|---|---|---|
400 Bad Request | Invalid request format or parameters | Invalid amount format |
422 Unprocessable Entity | Business rule validation failure | Payroll period is locked |
Example 400 response:
{
"errors": [
{
"field": "amount.decimal",
"message": "Amount decimal is in invalid format"
}
]
}Example 422 response:
{
"errors": [
{
"message": "Payroll is locked for selected month. Please schedule payroll change for the future."
}
]
}Updating a payroll change
Use this endpoint to update the amount of an existing payroll change.
PATCH /v1/payroll_changes/:id
Important restrictions
- Only the amount can be updated.
- To change the type, month, year, or engagement, delete the payroll change and create a new one.
- The currency code must match the engagement’s currency.
- The amount must be positive.
- Canceled payroll changes cannot be updated.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
amount | object | Yes | Updated amount object |
amount.decimal | string | Yes | New positive decimal amount |
amount.currencyCode | string | Yes | ISO 4217 currency code |
Example request
PATCH /v1/payroll_changes/psc_xyz789
{
"amount": {
"decimal": "750.00",
"currencyCode": "USD"
}
}Example response
{
"data": {
"id": "psc_xyz789",
"type": "BONUS",
"engagement": {
"id": "eng_abc123",
"name": "John Smith",
"type": "EMPLOYMENT"
},
"amount": {
"decimal": "750.00",
"currencyCode": "USD"
},
"month": "3",
"year": "2024",
"notes": "Q1 performance bonus",
"state": "PLANNED"
}
}Canceling or deleting a payroll change
There are two ways to cancel a payroll change.
Option A: Cancel with a reason (Preferred)
Use this endpoint when you want to provide a specific cancellation reason. It currently exists only on /v0.1, which is a live, supported API version. There is no /v1 equivalent, so calling /v0.1 for this operation alongside /v1 for everything else is expected.
POST /v0.1/payroll_changes/:id/cancel
Request body:
{
"reason": "Incorrect amount - will recreate with correct value"
}Successful response:
200 OK
The response body is empty.
Option B: Delete the payroll change
Use this endpoint for a convenience cancellation with a default reason.
DELETE /v1/payroll_changes/:id
This is equivalent to canceling with:
{
"reason": "Cancelled via API"
}Successful response:
200 OK
The response body is empty.
Cancellation restrictions
- Only payroll changes in
PLANNEDorAPPLIEDstate can be canceled. - Already canceled payroll changes cannot be canceled again.
- Cancellation is permanent and cannot be undone.
- Cutoff date validation may apply.
Updated about 2 hours ago
