Purple Expense — travel & expense management. Search bookable flight and hotel inventory, book trips, capture receipts, submit expense reports, and run approvals. Resembles tools like SAP Concur, Navan, and Expensify.
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Purple Expense — travel & expense management. Search bookable flight and hotel inventory, book trips, capture receipts, submit expense reports, and run approvals. Resembles tools like SAP Concur, Navan, and Expensify.
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Operations
GET /trips
List business trips
A trip is the travel-request container in Purple Expense: traveler, dates, origin/destination, purpose, cost centre and approval state. Bookings hang off a trip via trip_id and an expense report can link to one via trip_id. Use this to find a traveler's upcoming or past travel; use bookings for the individual flight/hotel reservations. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List business trips",
"description": "A trip is the travel-request container in Purple Expense: traveler, dates, origin/destination, purpose, cost centre and approval state. Bookings hang off a trip via trip_id and an expense report can link to one via trip_id. Use this to find a traveler's upcoming or past travel; use bookings for the individual flight/hotel reservations. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Trip"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /trips
Create a business trip
Opens a new trip in `planned` status for a traveler. Create the trip first when you want to group bookings and expenses under one itinerary — note that booking a flight or hotel without a trip_id auto-creates one for you.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create a business trip",
"description": "Opens a new trip in `planned` status for a traveler. Create the trip first when you want to group bookings and expenses under one itinerary — note that booking a flight or hotel without a trip_id auto-creates one for you.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
}
}
}
GET /trips/{id}
Get a business trip by id
Returns one trip with its itinerary, cost fields and approval stamps (approved_by/at). Bookings and expense items are separate records that reference this trip's id.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a business trip by id",
"description": "Returns one trip with its itinerary, cost fields and approval stamps (approved_by/at). Bookings and expense items are separate records that reference this trip's id.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /trips/{id}
Edit a business trip
Partial update of trip details (dates, destination, purpose, cost centre, estimated cost). For the planned → approved transition use the dedicated approve action instead of setting `status` here.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a business trip",
"description": "Partial update of trip details (dates, destination, purpose, cost centre, estimated cost). For the planned → approved transition use the dedicated approve action instead of setting `status` here.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /trips/{id}
Delete a business trip
DESTRUCTIVE and irreversible: removes the trip record. Bookings and expense items that referenced it are left pointing at a missing trip. Prefer setting status to `cancelled` unless the user really wants it gone.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a business trip",
"description": "DESTRUCTIVE and irreversible: removes the trip record. Bookings and expense items that referenced it are left pointing at a missing trip. Prefer setting status to `cancelled` unless the user really wants it gone.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /trips/{id}/approve
Pre-approve a trip
Manager pre-approval of the TRIP itself (the travel request), not of any spend. Transitions a trip from planned to approved and stamps approved_by/approved_at. Cannot approve a trip that is already approved, in_progress, completed or cancelled. Reimbursement of the money is a separate flow on expense reports.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Pre-approve a trip",
"description": "Manager pre-approval of the TRIP itself (the travel request), not of any spend. Transitions a trip from planned to approved and stamps approved_by/approved_at. Cannot approve a trip that is already approved, in_progress, completed or cancelled. Reimbursement of the money is a separate flow on expense reports.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"approved_by": {
"type": "string",
"example": "manager@acme.com"
},
"approval_notes": {
"type": "string",
"example": "Within policy. Approved."
}
},
"required": [
"approved_by"
]
}
}
}
},
"responses": {
"200": {
"description": "Trip approved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Trip"
}
}
}
},
"404": {
"description": "Trip not found"
},
"422": {
"description": "Trip cannot be approved in its current state"
}
}
}
GET /bookings
List travel bookings (reservations)
A booking is one confirmed reservation — flight, hotel, car or rail — with vendor, confirmation number, cost and start/end datetimes, usually attached to a trip via trip_id and, when made from inventory, to the source flight_id/hotel_id. Bookings are what the traveler actually holds; flights and hotels are the bookable catalogue. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List travel bookings (reservations)",
"description": "A booking is one confirmed reservation — flight, hotel, car or rail — with vendor, confirmation number, cost and start/end datetimes, usually attached to a trip via trip_id and, when made from inventory, to the source flight_id/hotel_id. Bookings are what the traveler actually holds; flights and hotels are the bookable catalogue. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Booking"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /bookings
Record a travel booking manually
Creates a booking row for a reservation made outside the inventory (an existing confirmation number, a car or rail segment). It does NOT touch flight/hotel availability — to book from the catalogue and decrement seats/rooms, use the flight or hotel book action instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Record a travel booking manually",
"description": "Creates a booking row for a reservation made outside the inventory (an existing confirmation number, a car or rail segment). It does NOT touch flight/hotel availability — to book from the catalogue and decrement seats/rooms, use the flight or hotel book action instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
}
}
}
GET /bookings/{id}
Get a travel booking by id
Returns one reservation with its vendor, confirmation number, cost, cabin/room class and, if cancelled, the cancellation stamps and refund amount.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a travel booking by id",
"description": "Returns one reservation with its vendor, confirmation number, cost, cabin/room class and, if cancelled, the cancellation stamps and refund amount.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /bookings/{id}
Edit a travel booking
Partial update of reservation details (times, vendor, confirmation number, cost, traveler). To cancel, use the dedicated cancel action rather than setting `status` here — cancel is what writes the cancellation stamps and refund.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a travel booking",
"description": "Partial update of reservation details (times, vendor, confirmation number, cost, traveler). To cancel, use the dedicated cancel action rather than setting `status` here — cancel is what writes the cancellation stamps and refund.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /bookings/{id}
Delete a travel booking
DESTRUCTIVE and irreversible: erases the reservation record entirely, with no refund and no release of inventory. Use the cancel action instead for a normal traveler-facing cancellation.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a travel booking",
"description": "DESTRUCTIVE and irreversible: erases the reservation record entirely, with no refund and no release of inventory. Use the cancel action instead for a normal traveler-facing cancellation.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /bookings/{id}/cancel
Cancel a booking
The traveler-facing way to cancel a reservation: marks the booking cancelled and records who cancelled it, why, and the refund amount. Cannot cancel a booking that is already cancelled or completed. Prefer this over deleting the booking, which erases the record and issues no refund.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Cancel a booking",
"description": "The traveler-facing way to cancel a reservation: marks the booking cancelled and records who cancelled it, why, and the refund amount. Cannot cancel a booking that is already cancelled or completed. Prefer this over deleting the booking, which erases the record and issues no refund.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"cancelled_by": {
"type": "string",
"example": "alice@acme.com"
},
"cancellation_reason": {
"type": "string",
"example": "Trip rescheduled"
},
"refund_amount": {
"type": "number",
"example": 434
}
},
"required": [
"cancelled_by"
]
}
}
}
},
"responses": {
"200": {
"description": "Booking cancelled",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"404": {
"description": "Booking not found"
},
"422": {
"description": "Booking cannot be cancelled in its current state"
}
}
}
GET /expense_reports
List expense reports
An expense report is the reimbursement envelope an employee submits: a named batch of expense items with an employee_email, optional trip_id, cost centre, approver and a status of draft → submitted → approved/rejected → reimbursed. `total_amount` is server-maintained from the attached items. Use this to answer 'what have I claimed / what is awaiting my approval'; use expense items for individual receipts. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List expense reports",
"description": "An expense report is the reimbursement envelope an employee submits: a named batch of expense items with an employee_email, optional trip_id, cost centre, approver and a status of draft → submitted → approved/rejected → reimbursed. `total_amount` is server-maintained from the attached items. Use this to answer 'what have I claimed / what is awaiting my approval'; use expense items for individual receipts. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExpenseReport"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /expense_reports
Create a draft expense report
Opens a new report in `draft` for an employee, optionally linked to a trip. Add expense items to it (they carry report_id) and then call the submit action — do not set total_amount, it is computed.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create a draft expense report",
"description": "Opens a new report in `draft` for an employee, optionally linked to a trip. Add expense items to it (they carry report_id) and then call the submit action — do not set total_amount, it is computed.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
}
}
}
GET /expense_reports/{id}
Get an expense report by id
Returns one report with its status, server-computed total and the full audit stamps for each transition (submitted_by/at, approved_by/at, rejection_reason, reimbursed_at, reimbursement_reference).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an expense report by id",
"description": "Returns one report with its status, server-computed total and the full audit stamps for each transition (submitted_by/at, approved_by/at, rejection_reason, reimbursed_at, reimbursement_reference).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /expense_reports/{id}
Edit an expense report's details
Partial update of report metadata (name, cost centre, project code, approver). Use the dedicated submit / approve / reject / reimburse actions to move `status` — they enforce the state machine and write the audit stamps. `total_amount` is server-maintained and ignored on input.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit an expense report's details",
"description": "Partial update of report metadata (name, cost centre, project code, approver). Use the dedicated submit / approve / reject / reimburse actions to move `status` — they enforce the state machine and write the audit stamps. `total_amount` is server-maintained and ignored on input.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /expense_reports/{id}
Delete an expense report
DESTRUCTIVE and irreversible: removes the report, including any approval and reimbursement history. Expense items that referenced it are orphaned. Confirm with the user before calling.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete an expense report",
"description": "DESTRUCTIVE and irreversible: removes the report, including any approval and reimbursement history. Expense items that referenced it are orphaned. Confirm with the user before calling.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /expense_reports/{id}/submit
Submit a draft expense report for approval
Step 1 of the reimbursement chain (submit → approve → reimburse). The EMPLOYEE sends their draft report to its approver: transitions draft → submitted and stamps submitted_by/at. Only draft reports can be submitted; use approve for the manager's decision.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Submit a draft expense report for approval",
"description": "Step 1 of the reimbursement chain (submit → approve → reimburse). The EMPLOYEE sends their draft report to its approver: transitions draft → submitted and stamps submitted_by/at. Only draft reports can be submitted; use approve for the manager's decision.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"submitted_by": {
"type": "string",
"example": "alice@acme.com",
"description": "Defaults to employee_email if omitted"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Report submitted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Report not found"
},
"422": {
"description": "Report is not in draft status"
}
}
}
POST /expense_reports/{id}/approve
Approve a submitted expense report
Step 2 of the reimbursement chain: the APPROVER signs off on a submitted report, transitioning submitted → approved with approved_by/at and optional approver_notes. Approving does not pay the employee — call the reimburse action for that. Only submitted reports can be approved; use reject to decline one.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Approve a submitted expense report",
"description": "Step 2 of the reimbursement chain: the APPROVER signs off on a submitted report, transitioning submitted → approved with approved_by/at and optional approver_notes. Approving does not pay the employee — call the reimburse action for that. Only submitted reports can be approved; use reject to decline one.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"approved_by": {
"type": "string",
"example": "manager@acme.com"
},
"approver_notes": {
"type": "string",
"example": "Within policy. Approved."
}
},
"required": [
"approved_by"
]
}
}
}
},
"responses": {
"200": {
"description": "Report approved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Report not found"
},
"422": {
"description": "Report is not in submitted status"
}
}
}
POST /expense_reports/{id}/reject
Reject a submitted expense report
The approver's decline decision, the counterpart of approve: transitions submitted → rejected with rejected_by/at. `rejection_reason` is REQUIRED and is shown back to the employee. Only submitted reports can be rejected.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Reject a submitted expense report",
"description": "The approver's decline decision, the counterpart of approve: transitions submitted → rejected with rejected_by/at. `rejection_reason` is REQUIRED and is shown back to the employee. Only submitted reports can be rejected.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"rejected_by": {
"type": "string",
"example": "manager@acme.com"
},
"rejection_reason": {
"type": "string",
"example": "Receipt missing for >$75 item"
}
},
"required": [
"rejected_by",
"rejection_reason"
]
}
}
}
},
"responses": {
"200": {
"description": "Report rejected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Report not found"
},
"422": {
"description": "Report is not in submitted status, or rejection_reason is missing"
}
}
}
POST /expense_reports/{id}/reimburse
Mark an approved expense report as paid out
Final step of the reimbursement chain, normally performed by finance: transitions approved → reimbursed and records reimbursed_at, reimbursement_method (defaults to direct_deposit) and a payment reference. This is the money-moving step and is terminal — only approved reports qualify, and approve alone does not pay.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Mark an approved expense report as paid out",
"description": "Final step of the reimbursement chain, normally performed by finance: transitions approved → reimbursed and records reimbursed_at, reimbursement_method (defaults to direct_deposit) and a payment reference. This is the money-moving step and is terminal — only approved reports qualify, and approve alone does not pay.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"reimbursement_method": {
"type": "string",
"enum": [
"direct_deposit",
"check",
"wire"
],
"example": "direct_deposit"
},
"reimbursement_reference": {
"type": "string",
"example": "REIM-AB12CD"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Report reimbursed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseReport"
}
}
}
},
"404": {
"description": "Report not found"
},
"422": {
"description": "Report is not in approved status"
}
}
}
GET /expense_items
List expense line items (receipts)
An expense item is a single spend line — one receipt: merchant, category, transaction_date, amount (plus FX to base currency), payment method, receipt_url and a status that can be flagged_policy with a policy_violation note. Items attach to an expense report via report_id and optionally a trip via trip_id. Use this for individual charges; use expense reports for the submitted envelope. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List expense line items (receipts)",
"description": "An expense item is a single spend line — one receipt: merchant, category, transaction_date, amount (plus FX to base currency), payment method, receipt_url and a status that can be flagged_policy with a policy_violation note. Items attach to an expense report via report_id and optionally a trip via trip_id. Use this for individual charges; use expense reports for the submitted envelope. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExpenseItem"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /expense_items
Add an expense line item to a report
Records one charge against the report named in `report_id` (and optionally a trip). Creating an item recomputes the parent report's total_amount. Categories and limits are governed by the expense_policies collection.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add an expense line item to a report",
"description": "Records one charge against the report named in `report_id` (and optionally a trip). Creating an item recomputes the parent report's total_amount. Categories and limits are governed by the expense_policies collection.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseItem"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseItem"
}
}
}
}
}
}
GET /expense_items/{id}
Get an expense line item by id
Returns one spend line with its amount, currency conversion, receipt link, billability and any policy violation recorded against it.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an expense line item by id",
"description": "Returns one spend line with its amount, currency conversion, receipt link, billability and any policy violation recorded against it.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseItem"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /expense_items/{id}
Edit an expense line item
Partial update of one charge (amount, category, merchant, receipt_url, billability). Changing the amount recomputes the parent report's total_amount.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit an expense line item",
"description": "Partial update of one charge (amount, category, merchant, receipt_url, billability). Changing the amount recomputes the parent report's total_amount.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseItem"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpenseItem"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /expense_items/{id}
Remove an expense line item from a report
DESTRUCTIVE and irreversible: deletes the receipt line and recomputes the parent report's total_amount downward. There is no undo.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Remove an expense line item from a report",
"description": "DESTRUCTIVE and irreversible: deletes the receipt line and recomputes the parent report's total_amount downward. There is no undo.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /expense_policies
List spend policy rules by expense category
An expense policy is the SPEND rule for one category (meals, lodging, mileage, …): daily_limit, per_transaction_limit, the receipt threshold, the pre-approval threshold, allowed payment methods and the mileage rate/unit. Read these to answer 'is this claim within policy' or 'what is the mileage rate'. Distinct from travel_policies, which govern flight cabin eligibility, and per_diem_rates, which are destination allowances. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List spend policy rules by expense category",
"description": "An expense policy is the SPEND rule for one category (meals, lodging, mileage, …): daily_limit, per_transaction_limit, the receipt threshold, the pre-approval threshold, allowed payment methods and the mileage rate/unit. Read these to answer 'is this claim within policy' or 'what is the mileage rate'. Distinct from travel_policies, which govern flight cabin eligibility, and per_diem_rates, which are destination allowances. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExpensePolicy"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /expense_policies
Create a spend policy rule for a category
Defines the limits and receipt/pre-approval thresholds for one expense category. Administrative configuration — it changes what counts as a violation for future claims.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create a spend policy rule for a category",
"description": "Defines the limits and receipt/pre-approval thresholds for one expense category. Administrative configuration — it changes what counts as a violation for future claims.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpensePolicy"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpensePolicy"
}
}
}
}
}
}
GET /expense_policies/{id}
Get a spend policy rule by id
Returns one category's limits, thresholds, allowed payment methods and effective date.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a spend policy rule by id",
"description": "Returns one category's limits, thresholds, allowed payment methods and effective date.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpensePolicy"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /expense_policies/{id}
Edit a spend policy rule
Partial update of a category's limits or thresholds. Administrative change that alters what is flagged as a policy violation going forward; it does not re-evaluate existing expense items.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a spend policy rule",
"description": "Partial update of a category's limits or thresholds. Administrative change that alters what is flagged as a policy violation going forward; it does not re-evaluate existing expense items.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpensePolicy"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExpensePolicy"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /expense_policies/{id}
Delete a spend policy rule
DESTRUCTIVE and irreversible: removes the category's limits entirely, so that category becomes unconstrained. Confirm with the user before calling.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a spend policy rule",
"description": "DESTRUCTIVE and irreversible: removes the category's limits entirely, so that category becomes unconstrained. Confirm with the user before calling.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /approvals
List approval requests (trips and expense reports)
An approval is the routing/audit record for one decision: target_type (trip or expense_report) plus target_id, the requester, the assigned approver, amount, status and decision notes. Query it to build an approver's queue across both trips and reports. The decision itself is made with the trip approve or expense-report approve/reject actions, not by editing this record. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List approval requests (trips and expense reports)",
"description": "An approval is the routing/audit record for one decision: target_type (trip or expense_report) plus target_id, the requester, the assigned approver, amount, status and decision notes. Query it to build an approver's queue across both trips and reports. The decision itself is made with the trip approve or expense-report approve/reject actions, not by editing this record. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Approval"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /approvals
Raise an approval request
Creates a routing record asking a named approver to decide on a trip or expense report. It records the request only; it does not itself change the target record's status.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Raise an approval request",
"description": "Creates a routing record asking a named approver to decide on a trip or expense report. It records the request only; it does not itself change the target record's status.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Approval"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Approval"
}
}
}
}
}
}
GET /approvals/{id}
Get an approval request by id
Returns one approval record — what it targets, who must decide, the amount, and the decision and notes if made.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an approval request by id",
"description": "Returns one approval record — what it targets, who must decide, the amount, and the decision and notes if made.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Approval"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /approvals/{id}
Edit an approval request
Partial update of the routing record (reassign the approver, note a decision). Prefer the trip approve or expense-report approve/reject/reimburse actions to actually move the target through its state machine.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit an approval request",
"description": "Partial update of the routing record (reassign the approver, note a decision). Prefer the trip approve or expense-report approve/reject/reimburse actions to actually move the target through its state machine.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Approval"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Approval"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /approvals/{id}
Delete an approval request
DESTRUCTIVE and irreversible: removes the approval record and its decision history. The target trip or report is left unchanged.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete an approval request",
"description": "DESTRUCTIVE and irreversible: removes the approval record and its decision history. The target trip or report is left unchanged.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /airlines
List airline carriers (reference data)
An airline is reference data about a carrier — IATA code, name, alliance, hub airports and historical on-time rate. Use it to resolve or describe the airline_code on a flight; it holds no schedules or availability, which live on flights. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List airline carriers (reference data)",
"description": "An airline is reference data about a carrier — IATA code, name, alliance, hub airports and historical on-time rate. Use it to resolve or describe the airline_code on a flight; it holds no schedules or availability, which live on flights. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Airline"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /airlines
Add an airline carrier
Adds a carrier to the reference list (code, name, alliance, hubs). Seed/admin data — it does not create any bookable flights.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add an airline carrier",
"description": "Adds a carrier to the reference list (code, name, alliance, hubs). Seed/admin data — it does not create any bookable flights.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Airline"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Airline"
}
}
}
}
}
}
GET /airlines/{id}
Get an airline carrier by id
Returns one carrier's reference details: IATA code, name, alliance, hubs and on-time rate.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an airline carrier by id",
"description": "Returns one carrier's reference details: IATA code, name, alliance, hubs and on-time rate.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Airline"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /airlines/{id}
Edit an airline carrier
Partial update of carrier reference data. Does not affect flights already referencing the carrier code.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit an airline carrier",
"description": "Partial update of carrier reference data. Does not affect flights already referencing the carrier code.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Airline"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Airline"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /airlines/{id}
Delete an airline carrier
DESTRUCTIVE and irreversible: removes the carrier from reference data. Flights keep their airline_code but can no longer be resolved to a carrier.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete an airline carrier",
"description": "DESTRUCTIVE and irreversible: removes the carrier from reference data. Flights keep their airline_code but can no longer be resolved to a carrier.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /flights/search
Search bookable flights
The shopping entry point for air travel — prefer this over listing flights. Filters the inventory by route, date, cabin and price, returns only flights with at least one matching fare that still has seats available, cheapest fare first. Read-only: pass a returned flight's id to the flight book action to actually reserve a seat.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "Search bookable flights",
"description": "The shopping entry point for air travel — prefer this over listing flights. Filters the inventory by route, date, cabin and price, returns only flights with at least one matching fare that still has seats available, cheapest fare first. Read-only: pass a returned flight's id to the flight book action to actually reserve a seat.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "origin",
"schema": {
"type": "string"
},
"example": "JFK",
"description": "Departure airport code"
},
{
"in": "query",
"name": "destination",
"schema": {
"type": "string"
},
"example": "SFO",
"description": "Arrival airport code"
},
{
"in": "query",
"name": "date",
"schema": {
"type": "string",
"format": "date"
},
"example": "2026-06-10",
"description": "Departure date (YYYY-MM-DD)"
},
{
"in": "query",
"name": "cabin",
"schema": {
"type": "string",
"enum": [
"Economy",
"Premium Economy",
"Business",
"First"
]
},
"description": "Only return flights offering this cabin with seats available"
},
{
"in": "query",
"name": "max_price",
"schema": {
"type": "number"
},
"description": "Only return flights with a matching fare at or below this price"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 20
}
}
],
"responses": {
"200": {
"description": "Matching flights",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Flight"
}
},
"total": {
"type": "integer"
}
}
}
}
}
}
}
}
GET /flights
List the raw flight inventory
A flight is one scheduled segment in the bookable catalogue: number, carrier, origin/destination, times, stops, status and a `fares` array of cabins with price and seats_available. This is the unfiltered catalogue dump — to shop for travel prefer the flight search operation, which filters by route/date/cabin/price and returns only flights with seats left, cheapest first. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List the raw flight inventory",
"description": "A flight is one scheduled segment in the bookable catalogue: number, carrier, origin/destination, times, stops, status and a `fares` array of cabins with price and seats_available. This is the unfiltered catalogue dump — to shop for travel prefer the flight search operation, which filters by route/date/cabin/price and returns only flights with seats left, cheapest first. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Flight"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /flights
Add a flight to the inventory
Seeds a new bookable segment with its cabins/fares. Inventory administration — this is not how a traveler books; use the flight book action.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add a flight to the inventory",
"description": "Seeds a new bookable segment with its cabins/fares. Inventory administration — this is not how a traveler books; use the flight book action.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flight"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flight"
}
}
}
}
}
}
GET /flights/{id}
Get a flight by id
Returns one segment with its schedule and live per-cabin fares/seats_available. Use before booking to confirm the cabin and price.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a flight by id",
"description": "Returns one segment with its schedule and live per-cabin fares/seats_available. Use before booking to confirm the cabin and price.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flight"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /flights/{id}
Edit a flight in the inventory
Partial update of a segment's schedule, status or fares. Inventory administration — booking already decrements seats_available for you.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a flight in the inventory",
"description": "Partial update of a segment's schedule, status or fares. Inventory administration — booking already decrements seats_available for you.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flight"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Flight"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /flights/{id}
Delete a flight from the inventory
DESTRUCTIVE and irreversible: removes the segment from the catalogue. Existing bookings keep their flight_id but it no longer resolves.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a flight from the inventory",
"description": "DESTRUCTIVE and irreversible: removes the segment from the catalogue. Existing bookings keep their flight_id but it no longer resolves.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /flights/{id}/book
Book a flight
Reserves a seat in the given cabin and creates a confirmed booking that links back to this flight via flight_id. Decrements the cabin's seats_available. Fails if the cabin is sold out or the flight is cancelled. Premium cabins are subject to fare-class policy (see the travel_policies collection): if the cabin's min_seniority exceeds the traveler's level, the booking is rejected with 403. If no trip_id is supplied, a trip is auto-created from the flight's route/dates and the booking is attached to it.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Book a flight",
"description": "Reserves a seat in the given cabin and creates a confirmed booking that links back to this flight via flight_id. Decrements the cabin's seats_available. Fails if the cabin is sold out or the flight is cancelled. Premium cabins are subject to fare-class policy (see the travel_policies collection): if the cabin's min_seniority exceeds the traveler's level, the booking is rejected with 403. If no trip_id is supplied, a trip is auto-created from the flight's route/dates and the booking is attached to it.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"traveler_email": {
"type": "string",
"example": "alice@acme.com"
},
"cabin": {
"type": "string",
"enum": [
"Economy",
"Premium Economy",
"Business",
"First"
],
"example": "Economy",
"description": "Defaults to Economy"
},
"trip_id": {
"type": "string",
"example": "TRIP-0001",
"description": "Optional. Trip to attach the booking to; if omitted, a new trip is auto-created from the flight."
},
"confirmation_number": {
"type": "string",
"description": "Optional; auto-generated if omitted"
}
},
"required": [
"traveler_email"
]
}
}
}
},
"responses": {
"201": {
"description": "Flight booked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"403": {
"description": "Traveler is not eligible for the requested cabin under fare-class policy"
},
"404": {
"description": "Flight not found"
},
"422": {
"description": "Cabin sold out, flight cancelled, or traveler_email missing"
}
}
}
GET /hotels/search
Search bookable hotels
The shopping entry point for lodging — prefer this over listing hotels. Filters the inventory by city, nightly rate and star rating, returns only active properties with rooms available, cheapest first. Read-only: pass a returned hotel's id to the hotel book action to actually reserve a room.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "Search bookable hotels",
"description": "The shopping entry point for lodging — prefer this over listing hotels. Filters the inventory by city, nightly rate and star rating, returns only active properties with rooms available, cheapest first. Read-only: pass a returned hotel's id to the hotel book action to actually reserve a room.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "city",
"schema": {
"type": "string"
},
"example": "SFO",
"description": "Matches city name, city_code, or address (case-insensitive substring)"
},
{
"in": "query",
"name": "max_nightly_rate",
"schema": {
"type": "number"
},
"description": "Only hotels at or below this nightly rate"
},
{
"in": "query",
"name": "min_star_rating",
"schema": {
"type": "number"
},
"description": "Only hotels at or above this star rating"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 20
}
}
],
"responses": {
"200": {
"description": "Matching hotels",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Hotel"
}
},
"total": {
"type": "integer"
}
}
}
}
}
}
}
}
GET /hotels
List the raw hotel inventory
A hotel is one bookable property: name, brand, city/city_code, star and guest ratings, nightly_rate, amenities, room_types and rooms_available. This is the unfiltered catalogue dump — to shop for lodging prefer the hotel search operation, which filters by city, rate and star rating and returns only active properties with rooms left. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List the raw hotel inventory",
"description": "A hotel is one bookable property: name, brand, city/city_code, star and guest ratings, nightly_rate, amenities, room_types and rooms_available. This is the unfiltered catalogue dump — to shop for lodging prefer the hotel search operation, which filters by city, rate and star rating and returns only active properties with rooms left. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Hotel"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /hotels
Add a hotel to the inventory
Seeds a new bookable property with its rates and room types. Inventory administration — travelers book via the hotel book action.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add a hotel to the inventory",
"description": "Seeds a new bookable property with its rates and room types. Inventory administration — travelers book via the hotel book action.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Hotel"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Hotel"
}
}
}
}
}
}
GET /hotels/{id}
Get a hotel by id
Returns one property with its rates, amenities, room tiers and current rooms_available. Use before booking to confirm rate and availability.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a hotel by id",
"description": "Returns one property with its rates, amenities, room tiers and current rooms_available. Use before booking to confirm rate and availability.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Hotel"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /hotels/{id}
Edit a hotel in the inventory
Partial update of a property's rate, status, amenities or room types. Inventory administration — booking already decrements rooms_available.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a hotel in the inventory",
"description": "Partial update of a property's rate, status, amenities or room types. Inventory administration — booking already decrements rooms_available.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Hotel"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Hotel"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /hotels/{id}
Delete a hotel from the inventory
DESTRUCTIVE and irreversible: removes the property from the catalogue. Existing bookings keep their hotel_id but it no longer resolves.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a hotel from the inventory",
"description": "DESTRUCTIVE and irreversible: removes the property from the catalogue. Existing bookings keep their hotel_id but it no longer resolves.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /hotels/{id}/book
Book a hotel stay
Reserves a room for the given dates and creates a confirmed booking that links back to this hotel via hotel_id. Cost is the nightly rate × nights. Decrements rooms_available. If no trip_id is supplied, a trip is auto-created from the hotel's city/dates and the booking is attached to it.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Book a hotel stay",
"description": "Reserves a room for the given dates and creates a confirmed booking that links back to this hotel via hotel_id. Cost is the nightly rate × nights. Decrements rooms_available. If no trip_id is supplied, a trip is auto-created from the hotel's city/dates and the booking is attached to it.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"traveler_email": {
"type": "string",
"example": "alice@acme.com"
},
"check_in": {
"type": "string",
"format": "date",
"example": "2026-06-10"
},
"check_out": {
"type": "string",
"format": "date",
"example": "2026-06-13"
},
"room_type": {
"type": "string",
"example": "Standard King",
"description": "Optional; uses the matching room_types rate, else the base nightly_rate"
},
"trip_id": {
"type": "string",
"example": "TRIP-0001",
"description": "Optional. Trip to attach the booking to; if omitted, a new trip is auto-created from the hotel stay."
},
"confirmation_number": {
"type": "string",
"description": "Optional; auto-generated if omitted"
}
},
"required": [
"traveler_email",
"check_in",
"check_out"
]
}
}
}
},
"responses": {
"201": {
"description": "Hotel booked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Booking"
}
}
}
},
"404": {
"description": "Hotel not found"
},
"422": {
"description": "No rooms available, invalid dates, or required fields missing"
}
}
}
GET /travel_policies
List fare-class (cabin) eligibility rules
A travel policy is a cabin-eligibility rule: the minimum seniority a traveler must hold to book a given cabin_class. It is enforced at booking time — the flight book action rejects an under-ranked traveler with 403. Read it to answer 'am I allowed to fly business'. Distinct from expense_policies (spend limits per category). Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List fare-class (cabin) eligibility rules",
"description": "A travel policy is a cabin-eligibility rule: the minimum seniority a traveler must hold to book a given cabin_class. It is enforced at booking time — the flight book action rejects an under-ranked traveler with 403. Read it to answer 'am I allowed to fly business'. Distinct from expense_policies (spend limits per category). Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TravelPolicy"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /travel_policies
Create a fare-class eligibility rule
Sets the minimum seniority required for a cabin class. Administrative — it takes effect on future flight bookings.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create a fare-class eligibility rule",
"description": "Sets the minimum seniority required for a cabin class. Administrative — it takes effect on future flight bookings.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TravelPolicy"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TravelPolicy"
}
}
}
}
}
}
GET /travel_policies/{id}
Get a fare-class eligibility rule by id
Returns one cabin rule: the cabin_class it governs, its min_seniority and effective date.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a fare-class eligibility rule by id",
"description": "Returns one cabin rule: the cabin_class it governs, its min_seniority and effective date.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TravelPolicy"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /travel_policies/{id}
Edit a fare-class eligibility rule
Partial update of a cabin's min_seniority or description. Changes who can book that cabin from now on; existing bookings are untouched.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a fare-class eligibility rule",
"description": "Partial update of a cabin's min_seniority or description. Changes who can book that cabin from now on; existing bookings are untouched.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TravelPolicy"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TravelPolicy"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /travel_policies/{id}
Delete a fare-class eligibility rule
DESTRUCTIVE and irreversible: removes the restriction, leaving that cabin bookable by anyone. Confirm with the user before calling.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a fare-class eligibility rule",
"description": "DESTRUCTIVE and irreversible: removes the restriction, leaving that cabin bookable by anyone. Confirm with the user before calling.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /per_diem_rates/calculate
Calculate per diem for a destination
Looks up a city's per diem rate (by city_code or city name) and totals it over a number of days, returning lodging, M&IE and combined amounts. Use this whenever the question is 'how much allowance for N days in X'; use the per diem rates list only to browse the underlying rate table. Read-only — it creates nothing.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "Calculate per diem for a destination",
"description": "Looks up a city's per diem rate (by city_code or city name) and totals it over a number of days, returning lodging, M&IE and combined amounts. Use this whenever the question is 'how much allowance for N days in X'; use the per diem rates list only to browse the underlying rate table. Read-only — it creates nothing.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "city_code",
"schema": {
"type": "string"
},
"example": "SFO",
"description": "Destination airport/city code (preferred). Provide this or city."
},
{
"in": "query",
"name": "city",
"schema": {
"type": "string"
},
"example": "San Francisco",
"description": "Destination city name (case-insensitive substring). Used if city_code is omitted."
},
{
"in": "query",
"name": "days",
"schema": {
"type": "integer",
"default": 1
},
"example": 3,
"description": "Number of days to total the allowance over"
}
],
"responses": {
"200": {
"description": "Per diem totals for the destination",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"example": "San Francisco"
},
"city_code": {
"type": "string",
"example": "SFO"
},
"country": {
"type": "string",
"example": "US"
},
"currency": {
"type": "string",
"example": "USD"
},
"days": {
"type": "integer",
"example": 3
},
"lodging_rate": {
"type": "number",
"example": 325
},
"mie_rate": {
"type": "number",
"example": 79
},
"total_daily_rate": {
"type": "number",
"example": 404
},
"lodging_total": {
"type": "number",
"example": 975
},
"mie_total": {
"type": "number",
"example": 237
},
"total": {
"type": "number",
"example": 1212
}
}
}
}
}
},
"404": {
"description": "No per diem rate found for the destination"
},
"422": {
"description": "Missing city_code/city, or days is not a positive integer"
}
}
}
GET /per_diem_rates
List per diem allowance rates by destination
A per diem rate is a GSA-style daily travel allowance for a city: lodging_rate plus mie_rate (meals & incidentals) and their total, in the instance's base currency. Use this to browse or compare destinations; to total an allowance over a stay use the per diem calculate operation instead. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List per diem allowance rates by destination",
"description": "A per diem rate is a GSA-style daily travel allowance for a city: lodging_rate plus mie_rate (meals & incidentals) and their total, in the instance's base currency. Use this to browse or compare destinations; to total an allowance over a stay use the per diem calculate operation instead. Server-side query: $filter/$select/$orderby/$search/$top/$skip/$count (MCP clients pass these as _filter, _top, … because $ is not a legal tool-input key).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PerDiemRate"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /per_diem_rates
Create a per diem rate for a destination
Adds a city's lodging and M&IE allowance. Administrative rate-table maintenance, not a traveler action.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create a per diem rate for a destination",
"description": "Adds a city's lodging and M&IE allowance. Administrative rate-table maintenance, not a traveler action.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PerDiemRate"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PerDiemRate"
}
}
}
}
}
}
GET /per_diem_rates/{id}
Get a per diem rate by id
Returns one destination's lodging, M&IE and combined daily allowance with its effective date.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get a per diem rate by id",
"description": "Returns one destination's lodging, M&IE and combined daily allowance with its effective date.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PerDiemRate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /per_diem_rates/{id}
Edit a per diem rate
Partial update of a destination's lodging/M&IE allowance. Affects future per diem calculations.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Edit a per diem rate",
"description": "Partial update of a destination's lodging/M&IE allowance. Affects future per diem calculations.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PerDiemRate"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PerDiemRate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /per_diem_rates/{id}
Delete a per diem rate
DESTRUCTIVE and irreversible: removes the destination's allowance, after which per diem calculation for that city will not resolve.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a per diem rate",
"description": "DESTRUCTIVE and irreversible: removes the destination's allowance, after which per diem calculation for that city will not resolve.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}