MCP App Store
by KeenEthics
Business & Operations
Perk icon

Perk

by Perk Platform SLU

Overview

Run spend reports, see trip itineraries, check event attendance, and retrieve invoices all from your AI assistant. Perk MCP connects your travel, spend, invoices and events from Perk with the AI tools you use every day. You sign in with your own Perk account, so you only ever see data you already have access to.

Tools

cars_get_car_search_results

ChatGPT
Fetch available car rental offers for a search. Call this after start_car_search with the returned transaction_id. If the response status is 'PENDING', you MUST send a visible message to the user (not just internal reasoning) saying the car search is in progress and results will be ready shortly, then wait retry_after_seconds seconds and call this tool ONCE with the same parameters - do NOT call it multiple times in the same turn. Keep repeating until status is 'COMPLETE' - NEVER stop polling to ask the user whether to continue waiting, and NEVER give up early. 'COMPLETE' is the only status you will ever see besides 'PENDING', and it is returned as soon as any offers are available, even if some suppliers failed. A search that fails outright raises a tool error - report that error to the user rather than retrying it silently. If a search does not finish in time, you will instead see 'COMPLETE' with whatever offers were found so far (possibly none) - present those normally rather than treating it as an error. sort_by and the filter parameters (supplier, insurance_level, car_type, car_size, car_seats, car_transmission, car_fuel_type, payment_mode) are optional refinements for a follow-up, more specific poll - call again with them once the user narrows their preferences. filter_options on a COMPLETE response lists the values usable for each of these filters. When presenting offers to the user, ALWAYS display a table that includes every offer with these columns (unless the user explicitly asks for a different format or level of detail): a reference number (1, 2, 3, etc. so the user can refer to an offer by number), Supplier, Car, Type, Seats, Transmission, Pick-up, Drop-off, Mileage, Payment, Price, In Policy. Never omit these columns even when multiple offers share the same value - show the value in each row.

cars_list_car_delivery_addresses

ChatGPT
List the account's car delivery and collection addresses - locations where a rental car can be delivered to and picked up from, instead of a rental office. Call this before start_car_search when the user wants delivery/collection instead of picking up at a rental office, to offer them the available addresses to choose from. Returns an empty list when the account does not have this feature enabled - in that case, use search_car_places for rental office or airport locations instead. Each returned address is a location object (place_type='car-delivery-address') that can be passed unchanged as the pick_up_location or drop_off_location for start_car_search.

cars_search_car_places

ChatGPT
Search for airports, cities, and addresses by name, to resolve the place objects required by start_car_search's pick_up_location/drop_off_location parameters. Call this before every car search - these places can only come from here, never from memory or guesswork. Results mix airports and other places (place_type). Prefer an airport result when the user names an airport, and a geocode result for any other place. Pass the chosen result object as pick_up_location or drop_off_location, unchanged.

events_create_event

ChatGPT
Create a new Perk Event. name, location, start_date and end_date are required; the event is created as a private, published event by default. Use visibility='account' to make it visible to the whole account, or is_draft=True to create it as a draft. location is a free-text description of where the event takes place (e.g. venue name, address, or neighborhood); the backend resolves it to a Google Maps place. category defaults to 'team or company event' and, when provided, must be one of: 'group trip', 'team or company event', 'external event', 'other'.

events_list_event_attendance

ChatGPT
List attendees for a specific Perk Event. Requires the caller to be an event organizer.Returns a paginated list of attendance records with RSVP status and person details.Use search to filter by attendee name or email. Use attending to filter by RSVP state. Default page size is 20; increment page to fetch more results.

events_list_events

ChatGPT
List Perk Events for the authenticated user. Results are already scoped to events the user can see in their account (organizing, invited, attending, or account-visible). By default returns upcoming events. Use date_filter='past' for ended events or date_filter='no_dates' for events without dates. Use status to narrow by role: organizing, attending, or invited.

expenses_approve_expense

ChatGPT
Approve a single expense that is awaiting the authenticated user's approval. Approving advances the expense to the next approver/stage in its approval chain. IMPORTANT — this performs a consequential state change. Before calling this tool you MUST: 1. Identify the specific expense (by id) and its submitter, typically via query_expenses with role='approver' (submitter display name from userFirstName + userLastName, or userId). 2. Explicitly confirm with the user that they want to approve THAT expense (state the submitter, amount, and merchant) before proceeding. Do not approve speculatively or in bulk. Authorization is enforced server-side: if the caller is not a current approver of the expense, or the expense is no longer pending, the approval fails and a clear message is returned. On success the tool returns a simple confirmation ({expenseId, approved: true, expenseUrl}); expenseUrl opens the expense in Perk's approver view. The updated expense is not re-read (an approved expense typically leaves the approver view).

expenses_create_expense

ChatGPT
Create a new draft expense with no receipt (manual / out-of-pocket), using the same expense-service create contract as the Spend UI. IMPORTANT — before calling this tool you MUST: 1. Confirm merchant, amount, currency, and expense date with the user. 2. Prefer user-friendly progress language (e.g. “Creating a €12 lunch expense”). This tool is NOT idempotent: calling it again creates another expense. Do not retry on success. If the user has a receipt image/PDF, use receipt_upload (or receipt_upload_fallback when the client cannot PUT to a signed URL) instead of this tool. Required: merchant, total_amount, currency, expense_date. Optional: category_id, cost_center_items, comment, payment_method (defaults to paidPrivately), company_credit_card_id, tags, legal_entity_id. When legal_entity_id is omitted, the caller's home legal entity is used automatically. For selectable ids (category, cost centers, tags, cards), resolve options after create via get_expense_form_options / update_expense, or from known catalogs when the user already chose them. Successful responses include expenseUrl, which opens the expense in Perk. The expense starts as a draft — edit with update_expense, attach a receipt later via receipt upload tools, or submit with submit_expense.

expenses_get_expense_form_options

ChatGPT
Load selectable options for editing an expense, using the same expense-service form-options APIs as the Spend UI. ALWAYS call this before proposing or applying updates that need ids (category, cost center, tags, payment method / company card, etc.). By default this fetches only the common edit catalogs: categories, costObjects (cost centers), tags, and paymentMethods. Pass fields to request additional or different catalogs (e.g. currencies, countries, customFields, trips). Pass include_cards=true when the user may select a company credit card — cards are not loaded by default. IMPORTANT — present options to the user in user-friendly language (labels and plain names), for example: “Available cost centers: Engineering, Marketing” or “Your cards: Visa ••••1234”. Do NOT dump raw JSON or lead with technical ids/enums. Keep ids only for the subsequent update_expense call after the user chooses.

expenses_query_expenses

ChatGPT
Query expenses from Spend for the authenticated user. This single tool covers two perspectives, selected by the role parameter: - role='submitter' (DEFAULT): the caller's OWN expenses — ones they submitted or are drafting. Use for 'my expenses', 'expenses I still need to submit', etc. - role='approver': expenses awaiting the caller's approval as an approver/manager (queried in the approval view context, viewRole: approval). Use for 'what expenses are waiting for my approval?', 'show me Anna's expense I need to approve', or to review a specific expense before approving it (filter by id). Results are always scoped server-side to the caller in the chosen role — for role='approver' you do NOT need to (and cannot) filter by approver id yourself. When role='approver' and no filters are given, it defaults to expenses awaiting approval/review (status in [2, 3]). Before using role='submitter', clarify whether the user wants their own individual data or org-wide data. Do NOT use this tool for company/account-wide spend, expenses across multiple users or a team, aggregated/summarised reports, or cost objects — use the reporting domain tools for those. Supports flexible filtering, field selection, and sorting. When an explicit fields list is requested, the response always also includes id (for the MCP-generated expenseUrl), digitization progress (isProcessing), the AI (OCR) fields (merchant, totalAmount, currency, expenseDate, taxItems, categoryId), and submitter identity (userFirstName, userLastName, userId) — compose a display name from first+last (fall back to userId). These are limited to expense-service's public query allowlist; receiptPath, attachments, and isPdf cannot be selected on projected queries (omit fields if you need the backend's fuller default field set including receipt metadata). Every expense includes an expenseUrl that opens its canonical detail page in Perk. Common filter patterns: - My open unsubmitted expenses (submitter): isOpen==true, tripId==null, status in [0,1,9,10] - Awaiting my approval (approver): status in [2,3] (applied by default) - A specific expense to review (approver): id=='<expenseId>' - Date range: expenseDate >= '2026-01-01' AND expenseDate <= '2026-03-31' For role='approver', an empty result is a normal, successful outcome — do NOT retry. With NO filters it means the approval queue is empty (tell the user they have nothing to approve); for a FILTERED approver query (e.g. id=='<expenseId>') it means no expense matching that filter is awaiting your approval (not found), not that the queue is empty.

expenses_receipt_upload

ChatGPT
Add a receipt (image or PDF) to an expense in Perk. Use this to start a new expense or to complete one that is missing its receipt. Two modes, selected by the optional expense_id: - Leave expense_id empty to create a brand-new expense from the receipt. Perk reads the details (merchant, amount, date, and so on) from the receipt for you, so there is no need to ask the user for them. - Provide expense_id to attach the receipt to an existing expense that does not have one yet. Note: attaching converts a receiptless expense into a receipt expense and may clear a category or tags that are not valid for receipt expenses. For privacy and speed, the receipt file is never sent through the chat. Instead, this tool returns a secure, temporary upload link, and the app uploads the file directly using it. After calling this tool, the app should: 1. Upload the receipt file to the returned signedUrl. 2. Send the returned requiredHeaders exactly as provided. 3. Finish the upload before the link expires (expiresInMs). That is all — there is no separate confirmation step; the receipt is filed automatically once the upload finishes. Uploading to the signed URL typically requires the client to reach Google storage (googleapis). If the upload to signedUrl fails because the client cannot use that URL, ask the user to contact their administrator (for example to allowlist googleapis / enable outbound upload to the storage host). Accepted files: JPEG, PNG, HEIC, HEIF, WEBP, or PDF, up to 10 MB. The response includes expenseUrl, which opens the created or updated expense's canonical detail page in Perk.

expenses_receipt_upload_fallback

ChatGPT
Alternate receipt upload that sends the file through the MCP server. Only use this tool when the user explicitly asks for fallback or alternate receipt upload — do not use it automatically when receipt_upload or the signed URL fails. Two modes, selected by the optional expense_id: - Leave expense_id empty to create a brand-new expense from the receipt. Perk reads the details (merchant, amount, date, and so on) from the receipt for you, so there is no need to ask the user for them. - Provide expense_id to attach the receipt to an existing expense that does not have one yet. Note: attaching converts a receiptless expense into a receipt expense and may clear a category or tags that are not valid for receipt expenses. Pass the receipt file as base64 in file_base64. Accepted files: JPEG, PNG, HEIC, HEIF, WEBP, or PDF, up to 5 MB. The response includes expenseUrl, which opens the created or updated expense's canonical detail page in Perk.

expenses_send_back_expense

ChatGPT
Send a single expense awaiting the authenticated user's approval back to its submitter for revision. This transitions the expense to needsRevision and includes a required reason for the submitter. This is not a hard reject. IMPORTANT — this performs a consequential state change. Before calling this tool you MUST: 1. Identify the specific expense (by id) and its submitter, typically via query_expenses with role='approver'. 2. Explicitly confirm with the user that they want to send back THAT expense, identifying the submitter and stating the reason that will be shown to them. Do not send back expenses speculatively or in bulk. Authorization is enforced server-side: if the caller is not a current approver, or the expense is no longer pending, the action fails safely. On success the tool returns {expenseId, sentBack: true, expenseUrl}; expenseUrl opens the expense in Perk's approver view.

expenses_submit_expense

ChatGPT
Submit one of the authenticated user's expenses for approval, using the same expense-service submit action as the Spend UI. IMPORTANT — before calling this tool you MUST: 1. Resolve the expense via query_expenses. 2. Confirm the expense identity (merchant, amount, date) and the user's intent to submit it for approval. 3. If required fields are missing, complete them first via update_expense (and get_expense_form_options for selectable fields). 4. Prefer user-friendly progress language (e.g. “Submitting your expense for approval”). Trip expenses that are only ready for trip submission must be submitted with the trip, not through this tool. Successful responses include expenseUrl, which opens the canonical expense detail page in Perk. If an unsupported custom field prevents submission, the error guidance includes the same URL so the user can complete it in Perk.

expenses_update_expense

ChatGPT
Partially update one of the authenticated user's editable expenses (draft / needs revision / similar), using the same expense-service PATCH contract as the Spend UI. IMPORTANT — before calling this tool you MUST: 1. Resolve the expense via query_expenses. 2. ALWAYS call get_expense_form_options when changing selectable fields (category, cost center, tags, payment method / company card). Use include_cards=true when the user may pick a company card. Present those options to the user in friendly labels/names and confirm their choice before patching. 3. Confirm the intended field changes with the user. 4. Prefer user-friendly progress language (e.g. “Updating the cost center on your draft expense”). Only allowlisted fields are accepted. For cost_center_items, send the full intended snapshot (merge with current values from query_expenses first — that response includes costCenterItems). For tags, send only the dimension→option pairs you want to set or change; this tool merges them onto the expense's current tags so omitted dimensions are preserved (query_expenses cannot return the tags id map). Amount/currency edits reuse the expense's frozen FX rates — do not invent rates. Mileage- and per-diem-specific fields are not supported. total_amount and currency are also rejected for mileage and per-diem expenses (those amounts are calculated by Spend and would be silently ignored); that guidance includes the expense's Perk URL. Successful responses include expenseUrl, which opens the canonical expense detail page in Perk.

flights_check_visa_requirements

ChatGPT
Check visa and entry requirements for a flight itinerary. REQUIRED: destinations - the same leg objects passed to start_flight_search. Passport nationality is inferred from the origin airport. For round-trips, only check the outbound leg — the return goes back to the traveler's starting country where we can assume they are allowed to enter. For multi-city trips, call this tool once per origin→destination leg and present the combined results. Present results using the display_hint in the tool response. One sentence only — no headers, no disclaimers, no passport assumptions.

flights_get_flight_leg_options

ChatGPT
Fetch available flight offers for the current leg of a search. Called with the transaction_id returned from start_flight_search. When status is 'loading' the search is still running. Early on, offers is empty - tell the user the search is in progress, then wait retry_after_seconds and call this tool ONCE with the same parameters (do NOT call it multiple times in the same turn). Once the search has run for a few seconds, a 'loading' response also carries the offers found so far: show them, say more are still arriving, and keep polling. Stop early and use what you have only if the user asks to - otherwise keep polling until status is 'complete', which means no more offers are coming. The offers returned are always bookable. When partial_results is true some providers did not respond, so the list may be missing options - mention that more may be available if the user does not see what they want, but present the offers normally. Fetch one leg at a time, sequentially - NOT in parallel: call with leg_number=1 and no selected_offer_id for the first leg's options; only after the user has picked an offer for it, call again with leg_number=2 and selected_offer_id set to that offer's offer_id to get the next leg's options (the return, for a round-trip). Multi-city trips keep going the same way - leg_number=3 with the offer picked for leg 2, and so on up to the number of legs searched. Always pass the offer_id picked for the immediately previous leg exactly as returned: it already carries every earlier choice, so nothing else from the offer needs passing back. IMPORTANT: The price on each offer is the total trip cost for the complete itinerary (all legs). For round-trips, the price already covers both outbound and return - do NOT add the outbound and return prices together; either price alone is the full roundtrip total. Use the sorting parameter to let the user compare cheapest ('price'), fastest ('travel_time'), best-value ('best') or lowest-carbon ('emissions') options without starting a new search. Sort by departure or arrival time with 'departure_first_leg'/'arrival_first_leg'; these run earliest-first, so add sort_order='desc' when the user wants the latest instead. When the user narrows their request, re-call this tool with the SAME transaction_id and the matching filter: max_stops (0 for direct only), airlines, depart_after/depart_before, arrive_after/arrive_before, max_duration_minutes, max_layover_minutes, in_policy_only. Filtering re-queries the search that is already running; never start a new search to apply a filter. Filters combine, so keep the ones still wanted when adding another. The response reports total_results against total_results_without_filters: use it to tell the user how much a filter narrowed things. When total_results is 0 but total_results_without_filters is not, the search did find flights and the filters excluded them all - say which filter is too narrow and offer to relax it. Never report this as 'no flights available'. The response includes current_page and total_pages - re-call with page=2, 3, etc. when the user asks for more options, keeping all other parameters the same. Each offer is a different flight, shown at its cheapest fare, and carries travel_class and fare_names. The same flight is usually also sold as dearer fares in higher cabins - those are NOT listed here; use start_flight_upsell on an offer to see them. When presenting offers to the user, ALWAYS display a table that includes every offer with these columns (unless the user explicitly asks for a different format or level of detail): a reference number (1, 2, 3, etc. so the user can refer to a flight by number), Airline, Departure, Arrival, Duration, Stops (show 'Direct' for 0 stops, otherwise 'N stop(s)' - the connecting airports are not returned, so never name one), Price, and In Policy. IMPORTANT: For round-trip searches the price column MUST be labelled 'Round-trip total' (not 'Price'). For one-way searches label it 'Price'. Never omit these columns even when multiple offers share the same value - show the value in each row.

flights_get_flight_upsell_options

ChatGPT
Fetch the upgrade options found for an offer, using the transaction_id and offer_id returned by start_flight_upsell. When status is 'loading' the upsell is still running: show any options already there, say more are still arriving, then wait retry_after_seconds and call this tool ONCE with the same parameters (do NOT call it multiple times in the same turn). Keep polling until status is 'complete'. The options returned are always bookable. partial_results being true means a provider failed, so cabins or fares it would have offered are missing - mention that more may be available, but present what came back normally. The results include the cabin the original offer was already on, so an option here may be the same flight the user started from - compare prices before describing one as an upgrade. total_results is how many options this response holds; when fewer offers than that come back the list was capped, so say so ('showing 20 of 24 options') rather than presenting it as everything. IMPORTANT: as in get_flight_leg_options, each price is the total for the complete itinerary (all legs), not the extra cost of upgrading - do NOT present it as a top-up, and do NOT add it to the original offer's price. If the user asks what the upgrade costs, subtract the original offer's price from the option's price and say so explicitly. Each option carries travel_class (e.g. 'economy', 'economy-comfort', 'business') and fare_names (e.g. ['MAIN CABIN'], one per segment). Use them to say what each option actually is: two options can share a travel_class and differ only by fare, which is what makes one refundable or changeable. Never infer the cabin from the price. When presenting options, ALWAYS display a table with a reference number (1, 2, 3, ...), Airline, Cabin, Fare, Departure, Arrival, Duration, Stops (show 'Direct' for 0 stops, otherwise 'N stop(s)'), Price, and In Policy - unless the user asks for a different format. To book one, pass its offer_id to build_flight_deeplink.

flights_search_flight_places

ChatGPT
Search for airports and cities by name or IATA code, to resolve the place objects required by start_flight_search's destinations (origin/destination). Call this before every flight search - these places can only come from here, never from memory, guesswork, or an invented IATA code. Results mix cities and airports (place_type). Prefer an airport result when the user names a specific airport, and a city result when they just name a city (it covers all airports serving that city, e.g. all London airports). Pass the chosen result object as origin or destination within each destinations leg, unchanged.

flights_start_flight_upsell

ChatGPT
Look for higher cabin or fare alternatives to ONE offer the user has already seen in get_flight_leg_options - e.g. 'what would business class cost on that flight?' or 'is there a fare that lets me change it?'. Optional: only call it when the user asks about upgrading a specific offer. FIRST LEG ONLY: the offer must come from the get_flight_leg_options call made with leg_number=1, and passing that same leg_number here is what confirms it. An offer from a later leg is rejected - upgrade the first leg's flight before the user picks the next one. One call returns every cabin and fare on that flight, so there is no need to ask for a particular one. It rides the existing search rather than starting a new one, so poll get_flight_upsell_options with the (transaction_id, offer_id) pair this returns. Only a few upsells can run at once: finish one before starting another.

hotels_get_hotel_rates

ChatGPT
Fetch the rooms and rates for one hotel. Call after start_hotel_rates with the same transaction_id and hotel_id. Status 'PENDING' means the rooms are still being fetched: tell the user, wait retry_after_seconds and call again with the same parameters until status is 'COMPLETE'. more_rates counts rates left out: say so, and pass that room_id in show_all_rates_for on request. When total_pages is above page, offer the next page. Filter with the parameters, not by leaving rooms out. - ALWAYS open with the hotel name and hotel_address, e.g. 'AC Hotel Madrid Feria by Marriott - Via de los Poblados 3, 28033 Madrid, ES'. - ALWAYS display one table per room option, with its name as the heading, e.g. '2 Double Room; 1 Standard Room'. - ALWAYS show every rate's title, price_total, free_cancellation_until, is_in_policy and tags, each value exactly as returned. - ALWAYS use this table template, with a numbered row per rate: | | Rate | Total | Free cancellation | In policy | Tags | | 1 | Breakfast included | EUR 502.92 | - | true | | - NEVER drop a column or move it into a footnote, however many rates leave it empty. - ALWAYS ask which rate they want and, in the same message, whether they have anything to pass on to the hotel, e.g. 'I arrive after midnight'. - NEVER pass anything internal on to the user, as a value or in your own words, e.g. room_id, rate_id, provider_id or which provider sells a rate.

hotels_get_hotels

ChatGPT
Fetch the hotels matching a search. Call after start_hotel_search with its transaction_id. Status 'PENDING' means the search is still running: tell the user, wait retry_after_seconds and call again with the same parameters until status is 'COMPLETE'. When total_pages is above page, offer the next page. Filter with the parameters, not by leaving hotels out; if the user asks for a filter there is no parameter for, say so. - ALWAYS open with search_location exactly as returned, e.g. 'Madrid, Community of Madrid, Spain'. - ALWAYS give every hotel its own numbered row, with these columns: name, price_total, stars, rating, proximities and policy_result.is_in_policy, e.g. '1 | AC Hotel Madrid Feria by Marriott | EUR 554.40 | 4 | 8.2 | 12 min by car | in policy'. - NEVER show hotel_id.

hotels_select_hotel_rate

ChatGPT
Put the rate the user picked on their trip. Call after get_hotel_rates with the same transaction_id and hotel_id, the rate_id and provider_id of the rate they picked, and the trip_id from start_hotel_search. It adds the hotel rate to the trip; nothing is booked or paid. Anything the user asked to pass on to the hotel goes in note_for_hotel, in their own words. - ALWAYS ask the user for a note for the hotel first, unless they have already given you one. - ALWAYS confirm what went on the trip by name: the hotel, the room option, the rate title and its price_total, e.g. 'Added AC Hotel Madrid Feria by Marriott, Twin Room, breakfast included, EUR 542.96'. - ALWAYS present the returned url as a markdown hyperlink with label as the link text, e.g. [Complete your booking on Perk](url). - NEVER pass anything internal on to the user, as a value or in your own words, e.g. rate_id, provider_id, which provider sells the rate, or the raw url.

hotels_start_hotel_rates

ChatGPT
Start fetching the rooms and rates for one hotel. Call after get_hotels with its transaction_id and the hotel_id of the hotel the user picked. This tool returns no rooms or rates - call get_hotel_rates next with the same parameters.

identity_get_current_user

ChatGPT
Get the profile and identity of the currently authenticated user. Returns name, email, roles, account, cost centre, and other identity details.

identity_search_users

ChatGPT
Search and filter users in your Perk account. Use filter_text to find users by name or email. Combine with state, roles, exclude_roles, user_ids, login_emails, manager_ids, is_manager, company_ids, or cost_center_ids to narrow results server-side. Returns up to 10 users by default; pass limit (max 1000) to retrieve more. Returns user identity and organisational details. FILTERING FOR ABSENCE — several ID-based filters accept -1 as a special sentinel meaning 'no value assigned'. Supported fields: manager_ids=[-1] (users with no line manager), approval_process_ids=[-1] (users with no approval process), cost_center_ids=[-1] (users with no cost center), payment_profile_ids=[-1] (users with no payment profile). Example: to find users with no line manager AND no approval process, pass manager_ids=[-1] and approval_process_ids=[-1].

invoices_get_invoice

ChatGPT
Get a single invoice by its serial number. Returns full invoice details including billing information, tax summary, amounts, line items preview, and a PDF download URL.

invoices_list_invoice_lines

ChatGPT
List individual line items across invoices. Each line represents a single booked service (flight, hotel, car, etc.) and includes trip metadata, traveller, cost center, tax, and amount. Filter by profile, serial number, or date range.

invoices_list_invoice_profiles

ChatGPT
List the invoice profiles (payment profiles) associated with your Company. Every Invoice is associated to an Invoice Profile. The Invoice Profile holds billing preferences like payment method, billing address, and billing period.

invoices_list_invoices

ChatGPT
List invoices for your company. Supports filtering by profile, status, date range, billing period, and serial number. Returns paginated results with invoice metadata including amounts, due dates, and a PDF download URL on each invoice.

policies_get_travel_policy

ChatGPT
Get the authenticated user's travel policy for a single vertical: flights, hotels, cars, or trains. Returns budget limits, booking windows, cabin or star restrictions, and other rules for that vertical. Returns null when the user has no travel policy assigned.

reporting_create_spend_report

ChatGPT
Start generating an org-wide spend report exported from Yokoy. Intended for admins and financial controllers — data scope covers the entire organisation or can be filtered by legal entity or user set. This is NOT a substitute for expenses_query_expenses, which is scoped to the individual traveller's own expenses. Export time varies: small date ranges are typically ready in under a minute, larger datasets can take several minutes to a few hours. Once started, poll reporting_get_spend_report with the returned task_id — call it every minute until state is complete or error. When state is complete, the response includes a url field with the download link.

reporting_create_travel_report

ChatGPT
Start generating a travel report for your Perk account. Specify a date range and optional filters, then use the returned task_id to retrieve results. Use reporting_get_travel_report_data for structured JSON exploration, or reporting_get_travel_report for a full CSV/XLSX file download. Looker async results expire ~5 minutes after creation — fetch data promptly with reporting_get_travel_report_data (ideally within 4 minutes). Reports can only cover dates up to yesterday — today's data is not available. Date filter guidance: Use booking_date for procurement or expense transaction analysis — this is the date the booking was confirmed and a sale transaction was created. Use start_date to analyse trips by when travel actually happened. Use invoice_issue_date for finance reconciliation against invoices. Cost field guidance: Use cost_per_traveler_preferred_currency for spend totals — it converts all bookings to your company's reporting currency so cross-currency comparisons are accurate. Use cost_per_traveler if you need the original booking currency. Avoid the estimated_* fields for actuals reporting — they reflect pre-booking estimates, not final charges.

reporting_get_spend_report

ChatGPT
Check the status of a spend report started with reporting_create_spend_report. Poll every minute until state is 'complete' or 'error'. When state is 'complete', url contains a signed download link for the report file. Stop polling if state is 'error'.

reporting_get_travel_report

ChatGPT
Check the status of a report started with reporting_create_travel_report. If state is 'running', wait a few seconds and call again — reports typically take 10–60 seconds. When state is 'complete', url contains a presigned S3 link to download the file (valid for a limited time). Stop polling if state is 'error'.

reporting_get_travel_report_data

ChatGPT
Retrieve structured JSON rows from a report started with reporting_create_travel_report. Poll this tool (not reporting_get_travel_report) until state is 'complete' — reports typically take 10–60 seconds. Stop polling if state is 'error'. Requires created_at from reporting_create_travel_report — pass it back on every call. The expiry guard applies only when starting or polling for data (no cursor); paginated fetches with cursor continue an in-progress read and are not re-checked client-side. Looker async results expire ~5 minutes after task creation — start fetching within 4 minutes. Follow next_cursor until null for additional pages. Set limit on the first call only to control page size (default 2000, max 2000). On the first call, pass the same fields as reporting_create_travel_report. Scope the report (date range, fields, filters) in reporting_create_travel_report. Use reporting_get_travel_report when you need a full CSV/XLSX export.

trains_get_train_search_inbound_results

ChatGPT
Fetch available inbound (return) train offers for a search. Call this after start_train_search_inbound with the returned transaction_id. If the response status is 'PENDING', you MUST send a visible message to the user (not just internal reasoning) saying the train search is in progress and results will be ready shortly, then wait retry_after_seconds seconds and call this tool ONCE with the same parameters - do NOT call it multiple times in the same turn. Keep repeating until status is 'COMPLETE' - NEVER stop polling to ask the user whether to continue waiting, and NEVER give up early. 'COMPLETE' is the only status you will ever see besides 'PENDING', and it is returned as soon as any offers are available, even if some providers failed. A search that fails outright raises a tool error - report that error to the user rather than retrying it silently. If a search does not finish in time, you will instead see 'COMPLETE' with whatever offers were found so far (possibly none) - present those normally rather than treating it as an error. These are the trains departing soonest after the requested return_departure_time. When the user wants different return times, start the whole search over with start_train_search using a later return_departure_time rather than re-calling this tool - there is no second page. When presenting offers to the user, ALWAYS display a table with the same columns used for outbound offers (reference number, Vendor, Origin, Destination, Departure, Arrival, Duration, Stops, Price, Fare, In Policy). IMPORTANT: unlike flights, the inbound price is NOT the round-trip total - the final cost is the outbound offer's price plus the inbound offer's price.

trains_get_train_search_results

ChatGPT
Fetch available outbound train offers for a search. Call this after start_train_search with the returned transaction_id. If the response status is 'PENDING', you MUST send a visible message to the user (not just internal reasoning) saying the train search is in progress and results will be ready shortly, then wait retry_after_seconds seconds and call this tool ONCE with the same parameters - do NOT call it multiple times in the same turn. Keep repeating until status is 'COMPLETE' - NEVER stop polling to ask the user whether to continue waiting, and NEVER give up early. 'COMPLETE' is the only status you will ever see besides 'PENDING', and it is returned as soon as any offers are available, even if some providers failed. A search that fails outright raises a tool error - report that error to the user rather than retrying it silently. If a search does not finish in time, you will instead see 'COMPLETE' with whatever offers were found so far (possibly none) - present those normally rather than treating it as an error. These are the trains departing soonest after the requested departure_time. When the user wants different departure times, call start_train_search again with a later departure_time rather than re-calling this tool - there is no second page. When presenting offers to the user, ALWAYS display a table that includes every offer with these columns (unless the user explicitly asks for a different format or level of detail): a reference number (1, 2, 3, etc. so the user can refer to an offer by number), Vendor, Origin, Destination, Departure, Arrival, Duration, Stops (show 'Direct' for 0 stops, otherwise 'N change(s)'), Price, Fare (fare.name, or '-' if unknown), and In Policy. Summarize fare.description in your own words when present (e.g. refund/exchange terms) rather than showing it verbatim, and mention notable fare.amenities (e.g. a meal-inclusive fare) since they distinguish otherwise-similar fares. Never omit these columns even when multiple offers share the same value - show the value in each row.

trains_search_train_places

ChatGPT
Search for train stations and cities by name, to resolve the place objects required by start_train_search's origin/destination parameters. Call this before every train search - these places can only come from here, never from memory or guesswork. Results mix cities and train stations (place_type). Prefer a train_station result when the user names a specific station, and a city result when they just name a city (it resolves to that city's main station). Pass the chosen result object as origin or destination, unchanged.

trains_start_train_search_inbound

ChatGPT
Start the inbound (return) leg of a round-trip train search. Call this only after the user has selected an outbound offer from get_train_search_results. Returns a new transaction_id for the inbound leg - use it with get_train_search_inbound_results, keeping the outbound transaction_id and offer_id for build_train_deeplink.

transactions_list_available_cards

ChatGPT
List the authenticated user's own company cards plus any lodge cards in their company — i.e. the cards they can submit expenses against. Use this when the user asks which cards they have, their card details, or which card to use for an expense. Takes no parameters; the user and company are resolved from their identity. Returns a list of company cards (no pagination). Card fields: - id (str): card identifier - name (str): card name - currency (str): ISO 4217 currency code - status (str): active, inactive, draft, ordered, processing, frozen, or deleted - number (str): last 4 digits of the card number - cardType (str): physical, virtual, singleUse, or lodge - provider (str): Perk, or Other

transactions_list_pending

ChatGPT
List the authenticated user's pending card transactions — unmatched, open transactions on cards they own that still need to be matched to an expense. Use this when the user asks what card transactions they need to act on, reconcile, or attach receipts to. Takes no parameters; the user and company are resolved from their identity. Lodge-card transactions are excluded. Returns a list of card transactions (all results, no pagination). Transaction fields: - id (str): transaction identifier - transactionDate (str): ISO date (YYYY-MM-DD) - merchant (str): merchant name - amount (number): transaction amount - currency (str): ISO 4217 currency code - cardId (str): id of the card the transaction belongs to - expenseId (str|null): linked expense id; null when unmatched - status (str): unmatched, matched, private, or archived (pending results are always unmatched) - isClosed (bool): always false for pending results - sourceAmount (number|null): original amount in the source currency - sourceCurrency (str|null): ISO 4217 source currency code - expenseStatus (str|null): notReadyForExport, readyForExport, or exported - isProcessing (bool): whether the transaction is still being processed - isCreditNote (bool): whether the transaction is a credit note / refund

trips_get_trip

ChatGPT
Get the full picture of one of your Perk trips — the traveller, what's booked, when they're going, and how much it costs.

trips_get_trip_itinerary_pdf

ChatGPT
Get the itinerary confirmation PDF download URL for a Perk trip. Returns a URL to the full booking confirmation including all services (flights, hotels, cars, trains). The trip must have at least one traveler and one booking. Present the returned URL as a markdown hyperlink using the label field as the link text, e.g. [Download PDF](url). Never display the raw URL.

trips_list_trips

ChatGPT
List trips for the authenticated user. By default, only returns trips where the authenticated user is a traveler. Set only_as_traveler=false to include trips the user manages (admins/travel managers only). Returns newest trips first by default. Filter by date range, status, or trip content.

expenses_query_expenses

Claude

identity_search_users

Claude

reports_create_travel_report

Claude

reports_get_travel_report

Claude

trips_get

Claude

trips_list

Claude

Capabilities

Writes

App Stats

59

Tools

ChatGPT, Claude

Platforms

Works with

ChatGPT
Claude

Data refreshed daily