Managing Expenses via API
Expenses API Overview
This guide explains how to list, retrieve, create, approve, decline, and delete expenses using the Oyster API. All expense operations follow Oyster's standard on-cycle flow — there is no off-cycle concept in the API. See Expenses at Oyster for a full explanation of the monthly cut-off and reimbursement cycle.
For the full endpoint reference, see the Expenses API reference.
Authentication and scopes
All write operations (create, approve, decline, delete) require the manage OAuth scope. Read operations (list, retrieve) work with the default read scope.
When authenticating via client credentials (machine-to-machine), write operations additionally require an X-Oyster-Actor-Email header identifying the company admin performing the action. Without it the API returns 400 Bad Request. Read operations do not require this header.
Example request with actor header
curl --request POST \
--url https://api.oysterhr.com/v1/expenses/{id}/approve \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json' \
--header 'X-Oyster-Actor-Email: [email protected]'Review state flow
Expenses expose a reviewState field with the following values:
reviewState | Meaning |
|---|---|
UNREVIEWED | Submitted, awaiting approval |
APPROVED | Approved, queued for reimbursement |
DECLINED | Declined, will not be reimbursed |
UNREVIEWED → APPROVED (via approve)
UNREVIEWED → DECLINED (via decline)
1. List expenses
Retrieve all expenses using the Retrieve all expenses endpoint. Use the engagement_id query parameter to filter results for a specific team member.
Example request
curl --request GET \
--url https://api.oysterhr.com/v1/expenses \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
{
"data": [
{
"expenseId": "exp_AbCd1234",
"name": "Laptop stand",
"description": "Home office equipment for remote work",
"incurredOn": "2026-08-15",
"category": "HOME_OFFICE",
"reviewState": "APPROVED",
"submittedAt": "2026-08-16T10:30:00Z",
"reviewedAt": "2026-08-17T09:00:00Z",
"declineReason": null,
"receiptAmount": {
"decimal": "89.99",
"currencyCode": "GBP"
},
"engagement": {
"engagement_id": "eng_XyZ9876",
"name": "Jane Smith",
"engagement_type": "EMPLOYMENT"
}
}
],
"meta": {
"page": 1,
"pages": 1,
"firstUrl": "/v1/expenses?page=1&per_page=100",
"lastUrl": "/v1/expenses?page=1&per_page=100"
}
}2. Retrieve an expense
Fetch full details for a single expense using the Retrieve an expense endpoint.
Example request
curl --request GET \
--url https://api.oysterhr.com/v1/expenses/{id} \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
{
"data": {
"expenseId": "exp_AbCd1234",
"name": "Laptop stand",
"description": "Home office equipment for remote work",
"incurredOn": "2026-08-15",
"category": "HOME_OFFICE",
"reviewState": "UNREVIEWED",
"submittedAt": "2026-08-16T10:30:00Z",
"reviewedAt": null,
"declineReason": null,
"revertReason": null,
"receiptUrl": "https://storage.oysterhr.com/receipts/abc123.pdf",
"receiptAmount": {
"decimal": "89.99",
"currencyCode": "GBP"
},
"engagement": {
"engagement_id": "eng_XyZ9876",
"name": "Jane Smith",
"engagement_type": "EMPLOYMENT"
}
}
}3. Create an expense
Submit a new expense on behalf of a team member using the Create an expense endpoint.
By default, if the actor creating the expense (identified by X-Oyster-Actor-Email) is not the team member themselves and has expense approval permissions, the expense is automatically approved on creation — it will have reviewState: APPROVED immediately without a separate approve call.
To keep the expense in UNREVIEWED for manual review, pass pendingApproval: true in the request body.
3.1. Getting expense categories
Before creating an expense, retrieve the list of valid categories using the Retrieve expense categories endpoint. The category field in the create request must match one of the values returned here.
Example request
curl --request GET \
--url https://api.oysterhr.com/v1/expenses/sources/categories \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
{
"data": [
{ "value": "BENEFITS", "title": "Benefits" },
{ "value": "CAR", "title": "Car" },
{ "value": "EQUIPMENT", "title": "Equipment" },
{ "value": "HOME_OFFICE", "title": "Home Office" },
{ "value": "LEARNING_AND_DEVELOPMENT", "title": "Learning And Development" },
{ "value": "FOOD", "title": "Food" },
{ "value": "OFFICE_SUPPLIES", "title": "Office Supplies" },
{ "value": "RENT", "title": "Rent" },
{ "value": "TRAVEL", "title": "Travel" },
{ "value": "OTHER", "title": "Other" }
]
}3.2. Creating the expense
Example request
curl --request POST \
--url https://api.oysterhr.com/v1/expenses \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json' \
--data '
{
"engagementId": "eng_XyZ9876",
"name": "Laptop stand",
"description": "Home office equipment for remote work",
"incurredOn": "2026-08-15",
"category": "HOME_OFFICE",
"receiptUrl": "https://storage.oysterhr.com/receipts/abc123.pdf",
"receiptAmount": {
"decimal": "89.99",
"currencyCode": "GBP"
}
}
'Response
{
"data": {
"expenseId": "exp_AbCd1234",
"name": "Laptop stand",
"reviewState": "UNREVIEWED",
"submittedAt": "2026-08-16T10:30:00Z"
}
}Example error
{
"error": {
"message": "The expense could not be created. Ensure all required fields are present and valid."
}
}4. Approve an expense
Approve an UNREVIEWED expense using the Approve an expense endpoint to queue it for reimbursement in the next eligible payroll cycle. If the monthly cut-off has already passed, the expense is held and reimbursed in the following month's cycle.
Once reimbursed, payslip visibility depends on engagement type: expenses for EOR (employment) and Global Payroll (GP) team members are processed through payroll and appear on the payslip; expenses for contractors are settled via a separate invoice and do not appear on a payslip.
Example request
curl --request POST \
--url https://api.oysterhr.com/v1/expenses/{id}/approve \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
When successful, response code is 200 with an empty body.
Error response (422)
{
"errors": [
{ "message": "Expense has been approved already" }
]
}5. Decline an expense
Decline an UNREVIEWED expense using the Decline an expense endpoint. A declined expense is not reimbursed and the team member is notified.
Example request
curl --request POST \
--url https://api.oysterhr.com/v1/expenses/{id}/decline \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
When successful, response code is 200 with an empty body.
Error response (422)
{
"errors": [
{ "message": "Expense has been declined already" }
]
}6. Delete an expense
Delete an expense using the Delete an expense endpoint. Only expenses with reviewState: UNREVIEWED can be deleted — once an expense has been approved or declined it cannot be removed.
Example request
curl --request DELETE \
--url https://api.oysterhr.com/v1/expenses/{id} \
--header 'accept: application/json' \
--header 'authorization: Bearer BEARER_TOKEN_GOES_HERE' \
--header 'content-type: application/json'Response
When successful, response code is 200 with an empty body.
Error response (422)
{
"errors": [
{ "message": "This expense has already been submitted. It cannot be deleted." }
]
}Updated about 2 hours ago
