API Reference

Expenses

Manage operational and project expense records.

REST /api/v1API key authentication7 endpoints
GET/api/v1/expenses

List expenses with optional filters

Query schema
const expenseTypeSchema = z.enum(['supplier_invoice', 'staff_claim', 'direct_cost']);

const expenseStatusSchema = z.enum(['draft', 'pending', 'approved', 'rejected', 'paid']);

const listQuerySchema = z.object({
  rootTaskId: z.string().uuid().optional(),
  taskId: z.string().uuid().optional(),
  supplierId: z.string().uuid().optional(),
  claimedByResourceId: z.string().uuid().optional(),
  expenseType: expenseTypeSchema.optional(),
  category: z.string().optional(),
  status: expenseStatusSchema.optional(),
  dateFrom: z.string().optional(),
  dateTo: z.string().optional(),
  isBillable: z
    .string()
    .transform((v) => v === 'true')
    .optional(),
  isInvoiced: z
    .string()
    .transform((v) => v === 'true')
    .optional(),
  limit: z.coerce.number().int().min(1).max(100).default(50),
  offset: z.coerce.number().int().min(0).default(0),
});
Response200JSON response with data and pagination envelopes.
POST/api/v1/expenses

Create a new expense

JSON body schema
const expenseTypeSchema = z.enum(['supplier_invoice', 'staff_claim', 'direct_cost']);

const expenseStatusSchema = z.enum(['draft', 'pending', 'approved', 'rejected', 'paid']);

const createBodySchema = z.object({
  rootTaskId: z.string().uuid().optional(),
  taskId: z.string().uuid().optional(),
  supplierId: z.string().uuid().optional(),
  claimedByResourceId: z.string().uuid().optional(),
  purchaseOrderId: z.string().uuid().optional(),
  purchaseOrderLineId: z.string().uuid().optional(),
  expenseType: expenseTypeSchema,
  category: z.string().optional(),
  amount: z.number().min(0),
  currency: z.string().length(3).optional(),
  date: z.string(),
  description: z.string().optional(),
  referenceNumber: z.string().optional(),
  reimbursementStatus: expenseStatusSchema.optional(),
  isBillable: z.boolean().optional(),
  markupPercentage: z.number().optional(),
  billableAmount: z.number().optional(),
  status: expenseStatusSchema.optional(),
  tags: z.array(z.string()).optional(),
});
Response201JSON response with a data envelope.
DELETE/api/v1/expenses/:id

Delete an expense

Path parametersid: string
Response204No response body.
GET/api/v1/expenses/:id

Get a single expense

Path parametersid: string
Response200JSON response with a data envelope.
PATCH/api/v1/expenses/:id

Update an expense

Path parametersid: string
JSON body schema
const expenseTypeSchema = z.enum(['supplier_invoice', 'staff_claim', 'direct_cost']);

const expenseStatusSchema = z.enum(['draft', 'pending', 'approved', 'rejected', 'paid']);

const updateBodySchema = z.object({
  rootTaskId: z.string().uuid().nullable().optional(),
  taskId: z.string().uuid().nullable().optional(),
  supplierId: z.string().uuid().nullable().optional(),
  claimedByResourceId: z.string().uuid().nullable().optional(),
  purchaseOrderId: z.string().uuid().nullable().optional(),
  purchaseOrderLineId: z.string().uuid().nullable().optional(),
  expenseType: expenseTypeSchema.optional(),
  category: z.string().nullable().optional(),
  amount: z.number().min(0).optional(),
  currency: z.string().length(3).optional(),
  date: z.string().optional(),
  description: z.string().nullable().optional(),
  referenceNumber: z.string().nullable().optional(),
  reimbursementStatus: expenseStatusSchema.nullable().optional(),
  isBillable: z.boolean().optional(),
  markupPercentage: z.number().optional(),
  billableAmount: z.number().nullable().optional(),
  status: expenseStatusSchema.optional(),
  tags: z.array(z.string()).optional(),
});
Response200JSON response with a data envelope.
POST/api/v1/expenses/:id/approve

Approve an expense

Path parametersid: string
Response200JSON response with a data envelope.
POST/api/v1/expenses/:id/reject

Reject an expense

Path parametersid: string
JSON body schema
const bodySchema = z.object({ reason: z.string().optional() });
Response200JSON response with a data envelope.