Expense List API
curl --request GET \
--url https://api.pazy.io/v1.0/expensesimport requests
url = "https://api.pazy.io/v1.0/expenses"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.pazy.io/v1.0/expenses', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pazy.io/v1.0/expenses",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pazy.io/v1.0/expenses"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.pazy.io/v1.0/expenses")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pazy.io/v1.0/expenses")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_bodyExpense APIs
Expense List API
Retrieve a paginated list of expenses with filters by date, state, employee, amount, and tags
Expense List API
curl --request GET \
--url https://api.pazy.io/v1.0/expensesimport requests
url = "https://api.pazy.io/v1.0/expenses"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.pazy.io/v1.0/expenses', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pazy.io/v1.0/expenses",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.pazy.io/v1.0/expenses"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.pazy.io/v1.0/expenses")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pazy.io/v1.0/expenses")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_bodyAuthentication
All requests require an API key in the request headers. Headers:Authorization: Api-Key YOUR_API_KEY
Request
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sortBy | string | No | Field to sort results by. Allowed values: total_amount, date_created, date_updated, item_count. |
sortOrder | string | No | Sort direction. Allowed values: asc (ascending), desc (descending). |
sortNullOrder | string | No | Where to place results with null values in the sorted field. Allowed values: first, last. |
limit | integer | No | Number of records to return. Min 1, Max 100, Default 30. |
cursor | string | No | Cursor for pagination. Use the nextCursor value returned by the previous response. |
startDate | string | No | Start date for filtering (ISO 8601 UTC format: YYYY-MM-DDTHH:mm:ssZ). |
endDate | string | No | End date for filtering (ISO 8601 UTC format: YYYY-MM-DDTHH:mm:ssZ). |
state | array | No | Expense states to filter by. Allowed values: DRAFTED, PENDING, APPROVED, OUT_OF_POLICY. Currently supports exactly one state. Note: DECLINED expenses may appear in unfiltered results but cannot be used as a filter value. |
employeeName | string | No | Filter by the name of the employee who raised the expense (partial match). |
tagId | integer | No | Filter expenses containing this organization tag id. |
minAmount | number | No | Filter expenses with amount greater than or equal to this value. |
maxAmount | number | No | Filter expenses with amount less than or equal to this value. |
tag[<tagSlug>] | string or array | No | Filter expenses by the value of a specific tag. See Filtering by Tags. |
Filtering by Tags
Expenses can be filtered by any tag configured for your organization using thetag[<tagSlug>] query parameter, where <tagSlug> is the tag’s slug (the id returned by the Tag List API).
?tag[cost-center]=Engineering&tag[project]=Apollo
Tag dataType | Accepted value | Matching |
|---|---|---|
DROPDOWN | The tag value text (for example Engineering) or the tag value id (for example 1024) | Exact match, case-insensitive. Both forms are accepted for the same tag and can be mixed. Get the values and their ids from the Tag Detail API. |
VARCHAR (text) | The text entered on the expense | Exact match, case-insensitive — engineering matches Engineering, but Eng does not match Engineering |
DATETIME (date) | Date in YYYY-MM-DD format (for example 2026-01-15) | Exact match on the date |
- Repeat the parameter to pass multiple values for the same tag — the values are combined with OR:
?tag[cost-center]=Engineering&tag[cost-center]=Marketingreturns expenses tagged with either value - Pass different tag slugs to combine them with AND:
?tag[cost-center]=Engineering&tag[project]=Apolloreturns only expenses that carry both - Values must match exactly (case-insensitive); partial matches are not supported
- Filtering is supported for
DROPDOWN,VARCHAR, andDATETIMEtags. Numeric tags (NUMBER,NUMERIC) cannot be used as filters - A slug that does not exist as a tag in your organization is ignored rather than rejected — verify slugs against the Tag List API if a filter returns unexpected results
Code Examples
curl -X GET "https://api.pazy.io/v1.0/expenses?limit=30&state=APPROVED&tag%5Bcost-center%5D=Engineering" \
-H "Authorization: Api-Key YOUR_API_KEY"
const params = new URLSearchParams({
limit: '30',
state: 'APPROVED',
'tag[cost-center]': 'Engineering'
});
const response = await fetch(`https://api.pazy.io/v1.0/expenses?${params}`, {
method: 'GET',
headers: {
'Authorization': 'Api-Key YOUR_API_KEY'
}
});
const result = await response.json();
import requests
url = "https://api.pazy.io/v1.0/expenses"
headers = {
"Authorization": "Api-Key YOUR_API_KEY"
}
params = {
"limit": 30,
"state": "APPROVED",
"tag[cost-center]": "Engineering"
}
response = requests.get(url, headers=headers, params=params)
result = response.json()
Success Response
HTTP Status:200 OK
Response Fields:
| Field | Type | Description |
|---|---|---|
ok | boolean | Indicates whether the request was successful |
data | object | Contains the expense list response data |
data.expenses | array | List of expenses |
data.expenses[].id | string | Unique identifier (slug) for the expense |
data.expenses[].state | string | Approval lifecycle state of the expense. See Field Reference → state for all values. |
data.expenses[].syncState | string | Accounting sync state. See Field Reference → syncState for all values. |
data.expenses[].expenseType | string | Type of the expense |
data.expenses[].paymentState | string | Bank transaction state. Always FINISHED in list responses. See Field Reference → paymentState for details. |
data.expenses[].currency | string | Currency code (e.g., INR) |
data.expenses[].totalAmount | number | Total amount of the expense |
data.expenses[].itemCount | integer | Number of line items in the expense |
data.expenses[].description | string | Description of the expense |
data.expenses[].identifier | string | Human-readable identifier / number for the expense |
data.expenses[].selfUrl | string | Shareable URL for viewing the expense in the web interface |
data.expenses[].transactionData | object | Transaction details for the expense |
data.expenses[].transactionData.utr | string | Unique Transaction Reference (UTR) of the payment |
data.expenses[].transactionData.dateConfirmed | string | Date the transaction was confirmed (falls back to expense date or creation date) |
data.expenses[].transactionData.narration | string | Bank narration for the transaction |
data.expenses[].transactionData.ledgerId | string or null | Ledger / instrument identifier the transaction is mapped to |
data.expenses[].transactionData.isSelfTransfer | boolean | Whether the transaction is a self-transfer (between the user’s own accounts) rather than a payment to an external merchant. See Field Reference → isSelfTransfer. |
data.expenses[].merchant | object | Merchant details |
data.expenses[].merchant.name | string or null | Merchant name |
data.expenses[].merchant.vpa | string or null | Merchant VPA (UPI address) |
data.expenses[].initiator | object | Details of the user who raised the expense |
data.expenses[].initiator.slug | string | Unique slug identifier of the initiator |
data.expenses[].initiator.name | string | Full name of the initiator |
data.expenses[].initiator.email | string | Email address of the initiator |
data.expenses[].initiator.id | string or null | Internal id of the initiator |
data.expenses[].cardInfo | object or null | Card details if the expense was paid by card. null otherwise |
data.expenses[].cardInfo.maskedCardNumber | string | Masked card number |
data.expenses[].cardInfo.description | string | Card description |
data.expenses[].cardInfo.name | string | Card name |
data.expenses[].syncStatus | string | Human-readable accounting sync status label. See Field Reference → syncStatus for all values. |
data.expenses[].status | string | Human-readable computed status label. See Field Reference → status for all values. |
data.expenses[].tags | object | Tags applied to the expense, keyed by tag slug. Absent when the expense carries no tags. |
data.expenses[].tags.<tagSlug> | array | Values applied to the expense for this tag |
data.expenses[].tags.<tagSlug>[].id | number | Identifier of the applied tag. For DROPDOWN tags this is the tag value id (usable in the tag[<tagSlug>] filter); for text, date, and number tags this is the tag field id |
data.expenses[].tags.<tagSlug>[].value | string or number | The value applied on the expense. Date tags are returned as YYYY-MM-DD |
data.totalAmountAndCountByCurrency | array | Aggregated total amount and count of matching expenses, grouped by currency |
data.totalAmountAndCountByCurrency[].currency | string | Currency code for the aggregate |
data.totalAmountAndCountByCurrency[].amount | number or string | Sum of expense amounts in this currency |
data.totalAmountAndCountByCurrency[].count | number or string | Number of expenses in this currency |
data.context | object | Pagination metadata |
data.context.count | number | Number of expenses returned in this page |
data.context.hasMore | boolean | Indicates whether there are more expenses to fetch |
data.context.nextCursor | string or null | Cursor to fetch the next page. null when there are no more results |
Field Reference
state — Expense approval state
The approval lifecycle state of the expense.
| Value | Description |
|---|---|
DRAFTED | Expense is open/in draft. It has not been submitted for approval yet. |
PENDING | Expense has been submitted and is awaiting approval from the assigned approver. |
APPROVED | Expense has been approved by the approver. |
OUT_OF_POLICY | Expense was flagged as out of policy by an approver or admin. |
DECLINED | Expense was declined by the approver. |
syncState — Accounting sync state
Indicates whether the expense has been synced to the connected accounting system (e.g., Tally, Zoho).
| Value | Description |
|---|---|
NOT_SYNCED | Expense has not been synced to any accounting integration. This is the default state. |
QUEUED | Expense is queued for sync to the accounting integration. The sync will be processed shortly. |
SYNCED | Expense has been successfully synced to the accounting integration. |
SKIPPED | Sync was deliberately skipped by an admin — the expense was moved to history without syncing. |
paymentState — Bank transaction state
Indicates the state of the underlying payment/bank transaction for the expense.
| Value | Description |
|---|---|
FINISHED | Payment has been completed and settled successfully. This is the only value returned by the list API — expenses with incomplete payments are excluded from list results. |
PENDING | Payment is still being processed by the bank. These expenses do not appear in list API results. |
FAILED | Payment transaction failed (e.g., card authorization failure). These expenses do not appear in list API results. |
REJECTED | Payment was rejected during just-in-time (JIT) card authorization. These expenses do not appear in list API results. |
The list API only returns expenses where
paymentState is FINISHED. You will always see "FINISHED" for this field in list responses. Expenses with other payment states are accessible only in the Pazy web app.status — Human-readable expense status label
A computed label summarizing the overall lifecycle position of the expense. This value is derived from a combination of state, syncState, paymentState, and internal fields. The API key context determines the label set (admin perspective).
| Value | Label | Condition |
|---|---|---|
OPEN | Open | Drafted, not synced, payment finished |
FOR_APPROVAL | Under Approval | Pending approval, not synced, payment finished |
OUT_OF_POLICY | Out of Policy | Out of policy or declined, not synced, payment finished |
ACCOUNTING | Ready To Sync | Approved, not yet synced or queued, payment finished |
HISTORY | History | Approved and synced/skipped; or out-of-policy/declined and skipped |
UNKNOWN | Unknown | Unrecognized state combination (rare) |
syncStatus — Human-readable accounting sync status label
A computed label describing the expense’s position in the accounting sync pipeline. Relevant when an accounting integration (Tally, Zoho, etc.) is connected.
| Value | Label | Description |
|---|---|---|
SYNCED | Synced | Successfully synced to the accounting system. |
QUEUED_FOR_SYNC | In Queue | Expense is in the sync queue, waiting to be processed. |
SYNC_ERROR | Sync Error | Sync was attempted but failed. Retry or manual intervention needed. |
SKIPPED | Skipped | Sync was manually skipped by an admin. |
READY_TO_SYNC | Ready To Sync | Expense is approved and has all required accounting fields — ready to be synced. |
MISSING_FIELDS | Accounting Info Missing | Required accounting fields (expense head, vendor, ledger mapping, etc.) are not filled. |
MISSING_RECEIPTS | Accounting Info Missing | Required receipt/document is not attached to the expense. |
transactionData.isSelfTransfer
A boolean indicating whether the underlying payment was a self-transfer (a transfer between the user’s own accounts) rather than a payment to an external merchant or vendor.
| Value | Description |
|---|---|
true | The transaction was a self-transfer between the user’s own accounts/wallets. |
false | Normal transaction — payment was made to a third-party merchant or vendor. |
Response Example
{
"ok": true,
"data": {
"expenses": [
{
"id": "expense_identifier",
"state": "APPROVED",
"syncState": "SYNCED",
"expenseType": "GENERAL",
"paymentState": "FINISHED",
"currency": "INR",
"totalAmount": 1500.00,
"itemCount": 2,
"description": "Team lunch",
"identifier": "EXP-2026-001",
"selfUrl": "https://app.pazy.io/p/expense/expense_identifier",
"transactionData": {
"utr": "123456789012",
"dateConfirmed": "2026-01-15T10:30:00Z",
"narration": "UPI/abc@bank/Team lunch",
"ledgerId": "ledger_identifier",
"isSelfTransfer": false
},
"merchant": {
"name": "ABC Restaurant",
"vpa": "abc@bank"
},
"initiator": {
"slug": "user_identifier",
"name": "John Doe",
"email": "john@example.com",
"id": "123"
},
"cardInfo": {
"maskedCardNumber": "XXXX XXXX XXXX 1234",
"description": "Corporate Card",
"name": "John Doe"
},
"syncStatus": "Synced",
"status": "Approved",
"tags": {
"cost-center": [
{
"id": 1024,
"value": "Engineering"
}
],
"project": [
{
"id": 44,
"value": "Apollo"
}
],
"invoice-received-on": [
{
"id": 45,
"value": "2026-01-15"
}
]
}
}
],
"totalAmountAndCountByCurrency": [
{
"currency": "INR",
"amount": "1500.00",
"count": "1"
}
],
"context": {
"count": 30,
"hasMore": true,
"nextCursor": "30"
}
}
}
Error Responses
Validation Error
HTTP Status:400 Bad Request
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed: /query/state: must be equal to one of the allowed values"
}
}
Access Denied
HTTP Status:403 Forbidden
{
"ok": false,
"error": {
"code": "ACCESS_DENIED",
"message": "Access denied: You can only view vendors you own"
}
}
Authentication Errors
HTTP Status:401 Unauthorized
{
"ok": false,
"error": {
"code": "MISSING_CREDENTIALS",
"message": "Missing Credentials"
}
}
{
"ok": false,
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API Key"
}
}
Permission Errors
HTTP Status:403 Forbidden
{
"ok": false,
"error": {
"code": "INSUFFICIENT_PERMISSIONS",
"message": "Permission check failed - PERMISSION_CHECK_FAILED"
}
}
Internal Error
HTTP Status:500 Internal Server Error
{
"ok": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "Internal error"
}
}
Best Practices
- Use
cursorandlimittogether to paginate; pass thenextCursorfrom the response as thecursorvalue for the next call startDateandendDateare filtered against the expense date — provide both for a bounded range- Use the
idfrom the response as the input to the Expense Details API for full expense information - The API returns at most 100 expenses per call regardless of the
limitvalue - Fetch the available tag slugs from the Tag List API and their dropdown values from the Tag Detail API before building
tag[<tagSlug>]filters - For
DROPDOWNtags, prefer filtering by the tag value id — it is stable even if the value text is later renamed - URL-encode the brackets in
tag[<tagSlug>](tag%5Bcost-center%5D) if your HTTP client does not do it for you - Use the
tagsobject in the response to confirm which tag values matched and to build follow-up filters - Access is limited to users with expense read permission and an admin or bookkeeper role
Was this page helpful?