# Finance for teams that run on agents
> Ramp helps agents buy, pay, procure, and complete work for the business with the controls finance teams need.
---
URL: https://agents.ramp.com/
# Finance for teams that run on agents
Ramp lets your AI agents buy, pay, procure, and complete work through Ramp while every transaction stays connected to the business context finance teams need.
Built on Ramp, the finance platform powering more than $200 billion in annual purchase volume across cards and bill payments.
## One identity per agent. Every action attributed.
- **One key per agent** — tied to a human sponsor on your team.
- **Flexible permissions** — decide what each agent can read and do.
- **Long-running access** — for background agents that run on a schedule.
- **Full attribution** — every action traces back to the agent that took it.
## Let your agents pay. You stay in control.
- **Flexible spend restrictions** — per agent, per task, per merchant.
- **Customizable human-in-the-loop policy** — auto-approve the routine, hold what matters for a human.
- **Auto-expiring cards** — from seconds to quarters.
- **No pre-funding needed** — run on credit, settle later.
## Talk to the team first
Talk to the team at /talk-to-team for beta access, embedded payments, internal agent tools, or rollout help.
If you want a self-serve starting point after that, begin with /skills/ramp-agentic-purchase.
---
URL: https://agents.ramp.com/talk-to-team
# Talk to the Team
Share the agent payments use case you are exploring. Ramp routes it to the right team for beta access, embedded payments, internal agent tools, or company rollout questions.
Open the contact form at /talk-to-team.
---
URL: https://agents.ramp.com/skills
# Skills
Official Ramp skills for AI agents — portable instruction sets that teach compatible agents how to work with Ramp: cards, bill pay, procurement, travel, banking, and accounting.
The catalog is synced from the public repo at github.com/ramp-public/skills. Install skills with `npx skills add ramp-public/skills`, or point an agent at a skill's SKILL.md directly. Browse the full set at /skills.
---
URL: https://agents.ramp.com/community
# Community
See what builders ship with agents. Join the Ramp agents community to swap MCP setups, CLI workflows, and skills.
Join on Slack from /community.
---
URL: https://agents.ramp.com/events
# Events
Workshops and meetups for builders using Ramp MCP and CLI. Bring your laptop, meet the team, and ship something live.
Upcoming sessions include Build with Campfire + Ramp MCP, CLI & Cocktails in San Francisco, and Build with Rillet + Ramp MCP.
Browse dates and register at /events.
---
URL: https://agents.ramp.com/docs/overview/introduction
# Introduction
## Two ways Ramp models agents
### Agents acting on behalf of users
This is the model available today. A person connects Ramp, and the agent acts
with that person's identity and permissions. The agent can only see and do what
the authenticated Ramp user is allowed to see and do.
### Agents with their own identities
This model gives an agent a distinct identity, permissions, and audit trail.
[Learn more about identity](/docs/guides/identity)
## How agents use Ramp
Ramp is designed to work across any agent runtime and harness. There are three general paths:
- **CLI (Recommended)** for agents with access to a terminal or command line.
- **MCP** for Claude, ChatGPT, and other general-purpose assistants that discover and call tools.
- **Developer API** for deterministic, programmatic integrations.
If your agent can run terminal commands, we recommend starting with the CLI. The [Ramp dashboard](https://app.ramp.com/) is also available for manual verification and agents using a general-purpose browser.
[Learn more about connection options](/docs/guides/connecting)
---
URL: https://agents.ramp.com/docs/overview/getting-started
# Getting Started
Copy one prompt into your coding agent. It will check whether you already
use Ramp, guide signup if needed, install the Ramp CLI, and connect your
account.
Once setup is complete, start with a read-only overview of the work waiting
for you.
Already use Ramp? Continue to step 2.
[Sign up for Ramp](https://ramp.com/signup)
Choose the assistant you already use.
[See other ways to connect](/docs/guides/connecting)
After connecting, start with a read-only overview of the work waiting for
you.
---
URL: https://agents.ramp.com/docs/guides/connecting
# Connecting
Instant to 15 minutes
Instant
Hours to days
Inside a conversational assistant
In a terminal, local agent, script, or CI
In deterministic code
Actions are attributed to the acting user or agent. The agent acts as the
Ramp user who authenticated the connection. Permissions do not exceed the
user or agent's role.
Actions are attributed to the acting user or agent. The agent acts as the
Ramp user who authenticated the connection. Permissions do not exceed the
user or agent's role.
Actions are scoped to the business owner's identity.
Easy setup, already available in some cases, and instant with Claude chat
or an out-of-the-box connector
Structured output, broad workflows, and easy local automation
Maximum control over interfaces, workflows, and backend logic
File uploads and downloads not available, slower performance
Requires installation and access to a coding agent or terminal
Requires the most engineering, security, and lifecycle ownership
Use the [Ramp app](https://app.ramp.com) when a person needs to review context,
handle an exception, or complete work directly.
The full list of tools available in the CLI and MCP can be found
[here](/docs/reference/agent-tools).
## CLI
Use the CLI when the agent has terminal access, needs structured output, or
should follow a repeatable skill. The CLI works well in Claude Code, Codex,
Cursor, scripts, and CI.
## Connect via MCP
Choose the client you already use. Each link opens the fastest available way to
add Ramp.
### Custom MCP clients
Use this path when your client does not have a ready-made Ramp connector.
Connect a live Ramp business to:
```text
https://mcp.ramp.com/mcp
```
Use sample data instead at:
```text
https://demo-mcp.ramp.com/mcp
```
Clients that read MCP JSON usually expect:
```json
{
"mcpServers": {
"ramp": {
"url": "https://mcp.ramp.com/mcp"
}
}
}
```
The connection acts as the Ramp user who completes browser authorization. What
the client can see and change follows that user's Ramp permissions.
Custom clients and gateways must have their exact redirect URI allowlisted
before Ramp authorization can complete:
- Use `https://`, `localhost`, or `127.0.0.1`.
- Submit the exact host. Wildcard subdomains are not supported.
- [Request redirect URI access](https://docs.ramp.com/developer-api/v1/mcp-redirect-whitelist-request)
for the client or gateway.
## Developer API
Use the API when your product owns the interface and backend workflow. Your
team is responsible for OAuth, scopes, webhooks, retries, and the customer
experience.
[View API reference](https://docs.ramp.com/developer-api/v1)
## Troubleshooting
### Authentication drops or tools look stale
Reconnect Ramp in the client so OAuth and the cached tool list refresh:
- **Claude**: disconnect and reconnect Ramp in **Settings → Connectors**.
- **Cursor**: toggle Ramp in **Cursor Settings → MCP**.
- **ChatGPT**: disconnect and reconnect Ramp in **Settings → Apps** or the
connector settings.
- **Perplexity**: remove Ramp and add it again in connector settings.
If a custom client keeps stale credentials, clear its cached MCP auth. Confirm
that the browser completed authorization against the intended Ramp business.
### The CLI keeps logging out
Read-write CLI sessions normally refresh for 24 hours, while read-only sessions
can refresh for one week. If the CLI logs out sooner, confirm that the runtime
preserves `~/.config/ramp/config.toml`. Ephemeral coding environments may
replace that file between sessions.
### A query returns too much data
MCP caps a query response at 100 rows. Narrow the date range, vendor,
cardholder, department, entity, or another available filter.
### ETL operation limit reached
Wait for current data operations to finish, or clear unused tables before
trying again. Persistent capacity errors usually resolve within minutes.
### Unsupported redirect URI
Custom clients and gateways need their exact redirect URI allowlisted. Use the
[Custom MCP clients](#custom-mcp-clients) instructions to submit the URI.
### A receipt attachment does not reach MCP
Use the Ramp CLI, Ramp dashboard, or another supported upload path for receipt
images and PDFs. Chat attachments are not automatically forwarded to Ramp MCP.
---
URL: https://agents.ramp.com/docs/guides/identity
# Identity
Ramp supports two identity models. Use a user identity when an agent assists one
person. Use a standalone identity when the business wants an agent to own work,
permissions, and its lifecycle independently.
An individual Ramp user
The business
Follows the connected user's Ramp permissions
Receives access explicitly assigned by an admin
Personal assistance and interactive work
Background automation and shared company workflows
Depends on the user's connection and access
Managed independently through its own lifecycle
Work runs with the user's identity and permissions
Supported activity is attributed to the agent
## Agents acting on behalf of users
MCP and CLI actions run as the Ramp user who completed OAuth. Visibility and
actions follow that user's permissions. If Sarah connects Ramp, her agent sees
the same Ramp businesses, cards, funds, and workflows Sarah can access.
Admins can manage which users have access to MCP connectors via the [Integrations page in app](https://app.ramp.com/settings/integrations/overview?category=AGENTS).
## Agents with their own identities
Standalone agents are first-class virtual employees on Ramp. They have their
own credentials, explicitly assigned permissions, an accountable human owner,
and a lifecycle that the business manages independently.
This model works well for background automation and shared workflows that
should not depend on one employee's account. It is available in limited early
access.
### Example: first-level bill review
An invoice review agent can compare hundreds of line items with the company's
system of record, approve clean bills, and send mismatches to a person. The
agent appears in the approval chain under its own identity instead of making
every decision under an employee's account.
### Controls for standalone agents
- **Least privilege:** The agent starts with minimal access. An admin grants only the permissions required for its job.
- **Human ownership:** Every standalone agent has an accountable Ramp user who owns its setup and operation.
- **Immediate control:** Admins can pause access, rotate credentials, or remove the agent independently.
- **Separate attribution:** Supported activity identifies the agent instead of borrowing a person's identity.
Adding an agent to an approval workflow does not grant authority by itself. The
agent must also have the permissions required to review or approve that work.
Reach out to the team to get early access.
---
URL: https://agents.ramp.com/docs/guides/agentic-payments
# Payments
> Standalone agents are in limited early access. Reach out to the team to get
> started.
Use business credentials to create roles, agents, and funds. Switch to the
agent's credentials only after connecting its runtime; those credentials are
what identify the agent when it requests payment credentials and completes the
purchase record.
Python examples use the [official Ramp SDK](https://github.com/ramp-public/ramp-python).
Preview businesses receive a supported version that includes the provisioning
resources shown below.
## Optional: Create a role
Use one of Ramp's default agent roles: **Purchasing**, **Bill approvals**,
**Data analysis**, **Expense Completion**, or **Full Access**. Create a custom
role only when none of the defaults matches the agent's job.
The API and SDK accept custom roles only; built-in role IDs are not available
through those surfaces. The business integration needs `users:read` and
`users:write`, the person authorizing it must be a role admin, and custom roles
must be enabled for the business.
Open [**Company → Agents**](https://app.ramp.com/company/agents), select
**New agent**, and enter the agent's name. On the **Role** step, choose
**Purchasing agent**, **Bill approvals**, **Data analysis**, or
**Expense completion**. Ramp creates or reuses the matching role as part of
setup.
```python
from ramp import Ramp
business_client = Ramp.from_env(environment="production")
# Reuse a matching custom role when one exists.
roles = business_client.roles.list()["data"]
role = next(
(item for item in roles if item["name"] == "Purchasing Agent Role"),
None,
)
if role is None:
# Otherwise create a role with the approved capability map.
role = business_client.roles.create(
name="Purchasing Agent Role",
description="Purchases approved goods and services",
product_capabilities={
"": "",
},
)
```
```bash
# List existing custom roles.
curl --request GET \
--url https://api.ramp.com/developer/v1/roles \
--header "Authorization: Bearer $RAMP_BUSINESS_ACCESS_TOKEN"
# Create a role when none matches the agent's job.
curl --request POST \
--url https://api.ramp.com/developer/v1/roles \
--header "Authorization: Bearer $RAMP_BUSINESS_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"name": "",
"description": "",
"product_capabilities": {
"": ""
}
}'
```
```bash
# List existing custom roles.
ramp --profile human roles list
# Create a role when none matches the agent's job.
ramp --profile human roles create --json '{
"name": "Purchasing Agent Role",
"description": "Purchases approved goods and services",
"product_capabilities": {
"": ""
}
}'
```
## 1. Create an agent
Create a company-owned identity for the software.
The business integration needs `agents:write`, the person authorizing it must
be a role admin, and standalone agents must be enabled for the business.
When supplied, the owner must be an active human with Ramp access in the same
business.
Open [**Company → Agents**](https://app.ramp.com/company/agents) and select
**New agent**. Enter the agent's name, continue to **Role**, and choose the
role that matches its job. The UI stages the agent until you finish the
**Wallet** step.
```python
from uuid import UUID
# Create the standalone agent and capture its show-once credentials.
agent = business_client.agents.create(
name="Purchasing Agent",
description="Purchases approved goods and services",
owner_id=UUID(""),
role_ids=[UUID(str(role["id"]))],
)
agent_id = str(agent["id"])
agent_client_id = str(agent["client_id"])
agent_client_secret = str(agent["client_secret"])
```
```bash
# Create the standalone agent.
curl --request POST \
--url https://api.ramp.com/developer/v1/agents \
--header "Authorization: Bearer $RAMP_BUSINESS_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"name": "Purchasing Agent",
"description": "Purchases approved goods and services",
"owner_id": "",
"role_ids": [""]
}'
# Store client_id and the show-once client_secret from the response.
```
```bash
# Create the standalone agent.
ramp --profile human agent create \
--name "Purchasing Agent" \
--description "Purchases approved goods and services" \
--owner_id "" \
--role-id ""
# Store the returned Client ID and show-once Client secret.
```
## 2. Assign a fund
Give the agent a budget, merchant rules, approvals, and accounting defaults.
The SDK, API, and CLI examples below show every control supported for a
standalone fund. Amounts in fund controls use the currency's smallest unit—for
example, `10000` means $100.00 USD. Configure approval chains in Ramp.
On the **Wallet** step, search for an existing fund and select **Add**. You
can preview its limits, eligibility, policies, and recent activity before
assigning it.
To make a new fund, select **Create funds** and set its name, spend limit,
cadence, optional per-transaction limit, and merchant or category rules.
The new fund is selected automatically.
Select **Create agent** when the wallet is ready. Ramp creates the agent,
assigns the selected funds, and opens the **Credential** step. Save the
Client ID and show-once Client secret in a secret manager.
```python
from uuid import UUID
# Create a standalone, shareable fund for the agent.
fund = business_client.funds.create(
idempotency_key="",
user_id=UUID(""),
display_name="Agent purchases",
is_shareable=True,
is_exempt_from_policy_agent=False,
accounting_rules=[
{
"field_id": "",
"field_option_id": "",
},
],
spending_restrictions={
"limit": {
"amount": 10000,
"currency_code": "USD",
},
"interval": "MONTHLY",
"transaction_amount_limit": {
"amount": 5000,
"currency_code": "USD",
},
"lock_date": "",
"blocked_mcc_codes": [""],
"allowed_vendor_ids": [""],
"allowed_category_codes": [14],
},
permitted_spend_types={
"reimbursements": False,
"virtual_card": True,
"physical_card": False,
},
)
# Assign the agent to the fund.
membership = business_client.funds.add_members(
fund_id=str(fund["id"]),
user_ids=[agent_id],
)
```
```bash
# Create a standalone, shareable fund.
curl --request POST \
--url https://api.ramp.com/developer/v1/funds \
--header "Authorization: Bearer $RAMP_BUSINESS_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--header "X-Idempotency-Key: " \
--data '{
"user_id": "",
"display_name": "Agent purchases",
"is_shareable": true,
"is_exempt_from_policy_agent": false,
"accounting_rules": [
{
"field_id": "",
"field_option_id": ""
}
],
"spending_restrictions": {
"limit": {
"amount": 10000,
"currency_code": "USD"
},
"interval": "MONTHLY",
"transaction_amount_limit": {
"amount": 5000,
"currency_code": "USD"
},
"lock_date": "",
"blocked_mcc_codes": [""],
"allowed_vendor_ids": [""],
"allowed_category_codes": [14]
},
"permitted_spend_types": {
"reimbursements": false,
"virtual_card": true,
"physical_card": false
}
}'
```
```bash
# Assign the agent to the fund.
curl --request POST \
--url https://api.ramp.com/developer/v1/funds/{fund_id}/members \
--header "Authorization: Bearer $RAMP_BUSINESS_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"user_ids": [""]
}'
```
```bash
# Create a standalone, shareable fund.
ramp --profile human funds create-fund "" \
--idempotency_key "" \
--display_name "Agent purchases" \
--is_shareable \
--json '{
"is_exempt_from_policy_agent": false,
"accounting_rules": [
{
"field_id": "",
"field_option_id": ""
}
],
"spending_restrictions": {
"limit": {
"amount": 10000,
"currency_code": "USD"
},
"interval": "MONTHLY",
"transaction_amount_limit": {
"amount": 5000,
"currency_code": "USD"
},
"lock_date": "",
"blocked_mcc_codes": [""],
"allowed_vendor_ids": [""],
"allowed_category_codes": [14]
},
"permitted_spend_types": {
"reimbursements": false,
"virtual_card": true,
"physical_card": false
}
}'
# Assign the agent to the fund.
ramp --profile human funds add-members "" \
--user_ids '[""]'
```
## 3. Connect a runtime
Store the agent's credentials in the system that will operate it.
Add Ramp through the hosted agent's secure connection or secret-storage
flow. Store the Client ID and Client secret as credentials, not in the
agent's instructions.
Hosted standalone-agent connections are part of the limited preview.
```python
from ramp import Ramp
# Authenticate future purchase actions as the agent.
agent_client = Ramp(
client_id=agent_client_id,
client_secret=agent_client_secret,
environment="production",
)
```
```bash
# Exchange the agent's credentials for an access token.
token_response="$(
curl --fail-with-body --silent --show-error --request POST \
--url https://api.ramp.com/developer/v1/token \
--user "$RAMP_AGENT_CLIENT_ID:$RAMP_AGENT_CLIENT_SECRET" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data "grant_type=client_credentials"
)"
RAMP_AGENT_ACCESS_TOKEN="$(jq -er '.access_token' <<<"$token_response")"
export RAMP_AGENT_ACCESS_TOKEN
unset RAMP_AGENT_CLIENT_ID RAMP_AGENT_CLIENT_SECRET token_response
```
```bash
# Refresh commands after Ramp enables the preview.
ramp --env production tools refresh
# Save the agent credentials in a dedicated CLI profile.
export RAMP_CLIENT_ID="$RAMP_AGENT_CLIENT_ID"
export RAMP_CLIENT_SECRET="$RAMP_AGENT_CLIENT_SECRET"
ramp --env production agent login
unset RAMP_CLIENT_ID RAMP_CLIENT_SECRET
# Confirm that future commands will run as the agent.
ramp --env production --profile agent auth status
```
## 4. Generate card credentials
Request a fresh credential after the fund, merchant, amount, and purpose are
approved.
The agent integration needs `cards:read_agentic` for fund discovery and
credential generation.
Ask the connected agent to use a specific fund for a named merchant and
maximum amount. Have it confirm the fund and final total before checkout.
The agent should send the credential directly to checkout, not display the
PAN or CVV in chat.
```python
# Confirm the fund is assigned to the agent.
eligible_funds = agent_client.agent_tools.agent_cards.list_funds(
rationale="Find funds assigned to this agent",
)
# Generate credentials for one approved checkout attempt.
credentials = agent_client.agent_tools.agent_cards.create_payment_token(
fund_id="",
amount="100.00",
currency_code="USD",
merchant_name="",
merchant_url="https://",
merchant_country_code="US",
rationale="Complete the approved purchase",
idempotency_key="",
)
```
```bash
# Confirm the fund is assigned to the agent.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-agent-card-funds \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"rationale": "Find funds assigned to this agent"
}'
```
```bash
# Generate credentials for one approved checkout attempt.
credential_response="$(
curl --fail-with-body --silent --show-error --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-agent-card-creds \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--header "X-Idempotency-Key: " \
--data '{
"fund_id": "",
"amount": "100.00",
"currency_code": "USD",
"merchant_name": "",
"merchant_url": "https://",
"merchant_country_code": "US",
"rationale": "Complete the approved purchase"
}'
)"
# Pass credential_response directly to checkout here without logging it.
# Then clear it from the shell.
unset credential_response
```
```bash
# Confirm the fund is assigned to the agent.
ramp --profile agent --agent funds get-agent-card-funds \
--rationale "Find funds assigned to this agent"
# Generate credentials without printing the PAN or CVV.
set +x
agent_card_credentials="$(
ramp --profile agent --agent funds creds "" \
--amount "100.00" \
--currency_code "USD" \
--merchant_name "" \
--merchant_url "https://" \
--merchant_country_code "US" \
--rationale "Complete the approved purchase" \
--idempotency_key "" |
jq -cer '.data[0]'
)"
# Pass agent_card_credentials directly to checkout here without logging it.
# Then clear it from the shell.
unset agent_card_credentials
```
The credential is limited to the merchant and requested amount. It expires
after the first authorization or 12 hours, whichever comes first.
Agent Card amounts use decimal strings: `100.00` means $100.00 USD. Fund limits
above use the currency's smallest unit, where `10000` means $100.00 USD.
## 5. Complete the purchase record
After the charge posts, add the receipt, memo, and required accounting fields.
Credential generation does not return the posted transaction ID. Find the
matching transaction after it posts. If Ramp requires accounting fields, list
the available categories and options before editing.
The agent integration needs `transactions:read`, `transactions:write`,
`accounting:read`, and `receipts:write` to finish this step. The examples treat
the memo and accounting choices as agent-selected. If the user supplied either
value directly, include its field name in `user_submitted_fields` instead.
Open the transaction in Ramp. Upload the receipt, add a specific memo,
confirm the fund, and complete any required accounting or tracking fields.
Finish when Ramp shows no missing items.
```python
import base64
from pathlib import Path
from uuid import UUID
next_page_cursor = None
matching_transactions = []
# Find exactly one posted transaction for this purchase.
while True:
transactions = agent_client.agent_tools.transactions.list(
transactions_to_retrieve="my_transactions",
next_page_cursor=next_page_cursor,
page_size=50,
rationale="Find the posted purchase",
)
matching_transactions.extend(
transaction
for transaction in transactions["transactions"]
if transaction["merchant_name"] == ""
and transaction["amount"] == "100.00"
and transaction["spend_allocation_id"] == ""
and transaction["transaction_time"].startswith("")
and transaction["transaction_uuid"] is not None
)
next_page_cursor = transactions.get("next_page_cursor")
if next_page_cursor is None:
break
if len(matching_transactions) != 1:
raise RuntimeError("Expected exactly one matching transaction")
transaction_id = UUID(matching_transactions[0]["transaction_uuid"])
# Find valid accounting values before editing the transaction.
categories = agent_client.agent_tools.accounting.categories(
transaction_uuid=str(transaction_id),
rationale="List required accounting categories",
)
options = agent_client.agent_tools.accounting.category_options(
tracking_category_uuid="",
transaction_uuid=str(transaction_id),
query_string="",
page_size=10,
rationale="List accounting options",
)
# Add the memo and accounting selections.
agent_client.agent_tools.transactions.edit(
transaction_uuid=str(transaction_id),
memo="",
tracking_category_selections=[
{
"category_uuid": "",
"option_selection": "",
},
],
user_submitted_fields=[],
rationale="Complete the approved purchase record",
)
# Attach the receipt and verify that nothing is missing.
receipt_bytes = Path("").read_bytes()
agent_client.agent_tools.receipts.upload(
filename="",
content_type="",
file_content_base64=base64.b64encode(receipt_bytes).decode("ascii"),
transaction_uuid=str(transaction_id),
rationale="Attach the purchase receipt",
)
missing_items = agent_client.agent_tools.transactions.missing(
id=transaction_id,
rationale="Verify the purchase record is complete",
)
agent_client.close()
business_client.close()
```
```bash
# Find the first page of posted transactions.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-transactions \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"transactions_to_retrieve": "my_transactions",
"page_size": 50,
"rationale": "Find the posted purchase"
}'
# Continue with the cursor returned by the previous page.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-transactions \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"transactions_to_retrieve": "my_transactions",
"next_page_cursor": "",
"page_size": 50,
"rationale": "Find the next page of posted purchases"
}'
# Find valid accounting categories and options.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-tracking-categories \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"transaction_uuid": "",
"rationale": "List required accounting categories"
}'
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-tracking-category-options \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"tracking_category_uuid": "",
"transaction_uuid": "",
"query_string": "",
"page_size": 10,
"rationale": "List accounting options"
}'
```
```bash
# Add the memo and accounting selections.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/edit-transaction \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"transaction_uuid": "",
"memo": "",
"tracking_category_selections": [
{
"category_uuid": "",
"option_selection": ""
}
],
"user_submitted_fields": [],
"rationale": "Complete the approved purchase record"
}'
```
```bash
# Attach the receipt.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/upload-receipt-file \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"filename": "",
"content_type": "",
"file_content_base64": "",
"transaction_uuid": "",
"rationale": "Attach the purchase receipt"
}'
```
```bash
# Verify that no required items remain.
curl --request POST \
--url https://api.ramp.com/developer/v1/agent-tools/get-transaction-missing-items \
--header "Authorization: Bearer $RAMP_AGENT_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"id": "",
"rationale": "Verify the purchase record is complete"
}'
unset RAMP_AGENT_ACCESS_TOKEN
```
```bash
# Find every page of posted transactions.
ramp --profile agent --agent transactions list \
--transactions_to_retrieve my_transactions \
--page_size 50 \
--rationale "Find the posted transaction"
# Repeat with each cursor returned in pagination.next_cursor.
ramp --profile agent --agent transactions list \
--transactions_to_retrieve my_transactions \
--next_page_cursor "" \
--page_size 50 \
--rationale "Find the next page of posted transactions"
# Add the memo and find valid accounting values.
ramp --profile agent --agent transactions edit "" \
--memo "" \
--rationale "Add the purchase memo"
ramp --profile agent --agent accounting categories \
--transaction_uuid "" \
--rationale "List required accounting categories"
ramp --profile agent --agent accounting category-options "" \
--transaction_uuid "" \
--query_string "" \
--page_size 10 \
--rationale "List accounting options"
# Add the accounting selections.
ramp --profile agent --agent transactions edit --json '{
"transaction_uuid": "",
"tracking_category_selections": [
{
"category_uuid": "",
"option_selection": ""
}
],
"user_submitted_fields": [],
"rationale": "Complete transaction accounting"
}'
# Attach the receipt and verify that nothing is missing.
receipt_path="/path/to/receipt.pdf"
receipt_base64="$(base64 < "$receipt_path" | tr -d '\r\n')"
ramp --profile agent --agent receipts upload \
--filename "${receipt_path##*/}" \
--content_type "application/pdf" \
--file_content_base64 "$receipt_base64" \
--transaction_uuid "" \
--rationale "Attach the purchase receipt"
unset receipt_base64
ramp --profile agent --agent transactions missing "" \
--rationale "Verify the purchase record is complete"
```
## More payment methods
ACH, checks, wires, and crypto-native payment methods are in private preview.
Talk to the team to enable the standalone-agent payment flow for your business.
---
URL: https://agents.ramp.com/docs/guides/travel
# Travel
Connected agents can search Ramp Travel, compare policy-aware options, and book
flights and hotels in plain language. The agent follows the signed-in user's
Ramp permissions, travel policy, available funds, and approval workflows.
*Available Skills*:
- [Book a flight](/skills/ramp-book-flight/) for one-way and round-trip flights
- [Book a hotel](/skills/ramp-book-hotel/) for single-property stays
Both skills work with Ramp MCP and the Ramp CLI. After [connecting Ramp](/docs/guides/connecting),
give the skill to your agent and describe the trip in plain language.
Requires [Ramp Plus](https://ramp.com/pricing)
## How booking works
1. **Describe the trip.** Give the agent the route or destination and travel dates. Add preferences only when they matter, such as a nonstop flight, an evening departure, a refundable room, or a hotel near an office.
2. **Compare current options.** The agent searches Ramp Travel and presents relevant choices with prices and policy status. Saved travel preferences and loyalty programs can improve recommendations.
3. **Choose the exact itinerary or room.** The agent waits for the traveler to select a flight, hotel, and room rate. It does not silently choose an option.
4. **Review the final terms.** Before money moves, Ramp returns a fresh booking preview with the total, policy result, funding source, required approvals, and applicable fare or cancellation terms.
5. **Confirm and verify.** The agent books only after a new, explicit confirmation. It then checks whether the reservation is confirmed, awaiting approval, still processing, or unsuccessful.
A submitted request is not always a confirmed reservation. For example, a
flight that needs manager approval remains pending until that approval and
ticketing finish. The agent should report the current state instead of saying
the traveler is booked early.
## Read trips and traveler information
An agent can also answer travel questions without starting a new booking:
- **Trips:** List upcoming, ongoing, or completed trips.
- **Bookings:** Retrieve current flight, hotel, and rental-car reservations and booking requests.
- **Booking details:** Check an itinerary or stay, approval status, amount, payment timing, cancellation state, associated spend, or booking error.
- **Traveler profile:** Check whether identity and contact details are ready for booking, including Known Traveler Number and redress information.
- **Loyalty programs:** List the airline and hotel memberships saved to the traveler profile.
- **Office locations:** Find company addresses to use as the center of a hotel search.
For example, ask "What trips do I have next month?", "Is my hotel still
awaiting approval?", or "Which airline loyalty programs are saved to my
profile?"
The [Agent Tools reference](/docs/reference/agent-tools) lists the current tools
and their inputs. Runtime access depends on the connected user's permissions,
Ramp Travel settings, and the tools available in that client.
## Traveler profiles and delegated booking
Ramp uses the intended traveler's profile for the reservation. If required
identity or contact details are missing, the agent asks the traveler to complete
them before booking. Saved airline and hotel loyalty programs, Known Traveler
Number, home airport, and travel preferences can carry into supported searches
and reservations.
An authorized delegate or Travel Manager can ask an agent to book for another
employee. The agent resolves the exact traveler first and applies that
traveler's profile, policy, eligible funds, and approval path while respecting
the delegate's permissions. It never falls back to booking for the requester
when the intended traveler cannot be found or accessed.
[Learn about delegate booking](https://support.ramp.com/travel-delegate-booking-via-assistant-role/)
## Policy, funds, and approvals
Travel policy is evaluated during search and again against the final booking.
Depending on the company's setup, Ramp can consider price, advance-booking
windows, cabin class, refundable hotel requirements, and per diem limits.
- **In-policy options** can follow the company's normal booking and approval flow.
- **Out-of-policy options** require the traveler to provide a reason that approvers can review.
- **Existing funds** are shown only when the traveler can use them for the booking.
- **New funding** can be requested when no existing fund is selected or available, with approval required before booking when applicable.
If the price, funding choice, policy result, or cancellation terms change after
the preview, the agent shows the new terms and asks for confirmation again.
## Changes, cancellations, and limitations
For supported flight and hotel bookings, an agent can look up the reservation
and may be able to preview current cancellation terms. Cancellation requires a
separate explicit confirmation and applies to the entire booking. When a
reservation is not eligible for self-service cancellation, the agent directs
the traveler to the appropriate Ramp Travel support path.
The agent booking flow does not currently support:
- Multi-city or open-jaw flight itineraries
- Group, multi-room, or rental-car bookings
- Seat selection, points redemption, or applying airline credits
- Flight or hotel changes and exchanges
Use [Ramp Travel](https://app.ramp.com/home/travel) or travel support for those
workflows. Availability and supplier rules can vary by booking.
## Have another travel workflow?
Talk to the team if you want to use agents for another travel use case or need
help deciding whether a workflow belongs in Ramp Travel, an agent skill, or a
custom integration.
---
URL: https://agents.ramp.com/docs/guides/incorporation
# Incorporation
If you do not have a US business entity yet, your agent can form a US LLC as
part of Ramp signup. The agent and the `incorporate-with-ramp` skill handle the
application while this guide explains the cost, timing, and limits.
## Pricing
## Timelines
| Milestone | Typical timeline |
|---|---|
| LLC filing | Days |
| Limited Ramp access | After formation is submitted |
| EIN delivery | 4–6 weeks |
| Full Ramp access | Automatic after the EIN arrives |
## Access before the EIN arrives
Limited Ramp access begins after formation is submitted. Card issuance, Agent
Cards, Ramp Business Checking, inbound deposits, yield, and supported domestic
payments can be available before the EIN arrives. Stablecoin rails,
international payments, and full identity verification wait for the EIN.
## Limits
- **US only.** The current path supports US LLCs with US-based founders.
- **LLC only.** C-Corp formation is not supported in this flow.
- **State availability changes.** Ask the agent for the current supported list.
- **SSN handling stays outside the agent context.** The CLI reads the required
last four digits from environment variables rather than command arguments.
- **Third-party filing.** doola handles the state filing and presents its data
handling disclosure before authorization.
---
URL: https://agents.ramp.com/docs/guides/banking
# Banking
---
URL: https://agents.ramp.com/docs/guides/data-analysis
# Data Analysis
---
URL: https://agents.ramp.com/docs/guides/users
# Users
---
URL: https://agents.ramp.com/docs/guides/bills
# Bills
---
URL: https://agents.ramp.com/docs/guides/procurement
# Procurement
---
URL: https://agents.ramp.com/docs/guides/funds
# Funds
---
URL: https://agents.ramp.com/docs/reference/agent-tools
# Agent Tools
This is the canonical generated list of Ramp Agent Tools available through both
Ramp MCP and Ramp CLI at build time, including exact tool names, descriptions,
and parameters.
Runtime access still depends on the authenticated Ramp identity's permissions,
admin controls, scopes, connected client, environment, and installed CLI
version. If this list differs from the tools available in your MCP client or
CLI, reconnect Ramp or update the CLI and use the live tool list for that
connection.
For the product-level choice, see [Connecting](/docs/guides/connecting).
---
URL: https://agents.ramp.com/skills/ramp-agentic-purchase
---
name: ramp-agentic-purchase
area: Agentic Commerce
supported_surfaces: [browser, cli, mcp]
description: "Make purchases using Ramp agent cards via browser checkout, then complete all transaction requirements (memo, tracking categories, receipt, trip). Use when asked to buy something with an agent card, make a payment using Ramp, spend from a fund, complete missing transaction items, or test the agent card payment flow. Also use when asked about agent card access, availability, or how to get started with Agentic Cards. Requires ramp CLI and a playwright-cli-driven browser (see bundled browser-checkout.md)."
---
# Agentic Purchase
End-to-end agent card purchasing: pick a fund, get a payment token via `ramp` CLI, pay via browser, then fill all missing transaction items.
## Access & Enrollment
Agent Cards are available via self-serve enrollment. **Enrollment is a mutation** — it enrolls the authenticated user's business in Agent Cards, enabling agent card funds and fund access. An availability, setup, or eligibility question is **not** consent to enroll: first answer the question, tell the user that enrolling activates Agent Cards for their business, and ask for explicit confirmation. Only after the user confirms, enroll using the CLI:
```bash
ramp agent_cards enroll --agent --rationale "User confirmed enrolling the business in Agent Cards"
```
The user must be authenticated (`ramp auth login`) and have appropriate permissions on their Ramp business. Once enrollment succeeds, the user can immediately use `agent_cards list` and `agent_cards creds` to access their agent card funds.
For more information, visit **https://agents.ramp.com/cards**.
Use this guidance for:
- "What are Agentic Cards?" or "How do I get agent cards?"
- Access, availability, enrollment, or setup questions
- Users who hit eligibility errors when running `agent_cards list` or `agent_cards creds`
In every case, describe what enrollment does and get an explicit "yes" before running `agent_cards enroll`.
If the user is already enrolled, proceed with the workflow below.
## Prerequisites
- `ramp` CLI installed and authenticated (`ramp auth login`)
- `playwright-cli` browser automation set up per the bundled [browser-checkout.md](browser-checkout.md) reference (formerly the standalone ramp-browser-automation skill, now shipped inside this skill)
- Business enrolled in Agent Cards (see [Access & Enrollment](#access--enrollment) to enroll if not)
## CLI conventions
- Pass `--agent` for machine-readable JSON output (documented shape is top-level, immediately after `ramp`: `ramp --agent agent_cards list`)
- Use positional arguments where supported (e.g., `ramp --agent agent_cards creds `, `ramp --agent transactions missing `)
- Use `--json` for complex payloads (e.g., `ramp --agent transactions edit`)
- Every subcommand accepts `--json`, `--dry_run` (`-n`), and `--help`
- **`--rationale` is required on every subcommand** — a non-empty string (max 1024 chars) explaining why you are making the call. With `--json`, put it in the body as a `"rationale"` key instead. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, even in `--agent` mode.
## Hard rules
- **Two distinct amount thresholds — respect both:**
- **User preauth tolerance:** policy-level, ~10% over requested. Stop and re-ask if exceeded.
- **Visa cryptogram auth cap:** the cryptogram from `ramp --agent agent_cards creds ... --amount X` rejects any charge > X at the network level — even $0.01 over declines. If the merchant's final total exceeds the cryptogram amount (bag fees, surprise tax, currency conversion), the old cryptogram is unusable — burn it, pull fresh creds at the corrected amount, and re-preauth if the new amount exceeds the user's original 10% tolerance.
- **Run the browser headed, never `--headless`.** Purchase flows need the user able to see and intervene — bot checks, 3DS, and login walls all require human input. See [browser-checkout.md](browser-checkout.md) for the headed-default rule.
- **Stop if anything unexpected happens.** 3DS challenge, login wall, CAPTCHA, bot-block page, merchant form you can't parse → screenshot, hand off to the user via the visible Chrome window per the human-handoff pattern in [browser-checkout.md](browser-checkout.md), and wait. Do not retry blindly.
## Phase 1: Payment
### Step 1 — Pick a fund
```bash
ramp agent_cards list --agent --rationale "List the user's agent card funds"
```
The response contains `funds`, each with `id`, `display_name`, `available_balance`, `currency`, `interval`, `max_transaction_amount`, `allowed_merchants`, and `allowed_categories`. Select for purpose fit first, then technical eligibility:
- Use `display_name` to identify likely purpose fit, then confirm the choice with the user. Do not infer an intended use that is not present in the response.
- Treat broad, admin, or shared-access funds as a hazard: technical access is not approval, and the purpose-fit bar is higher when many funds are visible.
- If no appropriate fund exists, stop and ask the user which fund to use or whether to proceed through the standard card request/approval flow. Do not keep trying broad/admin funds automatically.
Then verify the selected fund has:
- `available_balance` covers the purchase amount
- `currency` matches the merchant
- `max_transaction_amount` is `null` or covers the purchase amount
- `allowed_merchants` / `allowed_categories` permit the purchase (empty = unrestricted)
- `interval` is the fund's replenishment cadence, not additional balance available for the current purchase
Use the selected fund's `id` as `` when requesting credentials.
**Guest-checkout PII:** If no signed-in merchant account (header shows "Sign in"), guest checkout needs full name, email, phone, and shipping/billing address. Do NOT proxy these from `cardholder_name` / `billing_address` on the creds — those are for the payment form only, not the merchant's contact fields. Ask the user explicitly.
**Cart-state pre-flight:** Before generating creds for a merchant with a persistent cart (Walmart, Amazon, CVS, etc.), navigate to the cart and confirm it contains only the intended item at the expected price. Pre-existing items cookie-persist and will be charged alongside yours. If stale items exist, remove them (or ask the user to) before generating creds.
### Step 2 — Get payment token
Run `agent_cards creds` only after the user confirms the selected fund, merchant/amount, and rationale.
```bash
ramp --agent agent_cards creds "" \
--amount "45.00" \
--currency_code "USD" \
--merchant_name "Children's Hunger Fund" \
--merchant_url "https://childrenshungerfund.org" \
--merchant_country_code "US" \
--rationale "User approved \$45 donation to Children's Hunger Fund"
```
Returns `pan`, `cvv`, `expiration_month`, `expiration_year`.
**Zsh `$` escape trap:** Zsh double-quoted `"Purchase $5 credits"` expands `$5` to the 5th positional (empty), so the rationale silently loses the amount. Escape as `\$5` or use single quotes when passing dollar amounts in `--rationale` or other flags.
**Key behaviors:**
- Each call returns a **fresh CVV** — get creds immediately before checkout
- Tokens are **single-use**
- Funds are **reusable** across multiple calls
### Step 3 — Pay via browser
Read the bundled [browser-checkout.md](browser-checkout.md) reference (setup, `pw` usage, handoff patterns), then:
1. Open merchant site:
```bash
cd ~/.pw-agent && ./pw open "https://merchant.com/donate"
```
2. Navigate to checkout / donation page
3. Take a snapshot to find form fields:
```bash
./pw snapshot
SNAPSHOT=$(ls -t .playwright-cli/*.yml | head -1)
grep -iE "card|number|name|expir|cvv|cvc|amount|donate" "$SNAPSHOT" | head -20
```
4. Fill payment form:
```bash
./pw fill ""
./pw fill ""
./pw fill ""
./pw fill ""
```
5. If the merchant has saved cards, click "Add a new card" first
6. Submit the payment:
```bash
./pw click
```
7. Take a screenshot to confirm success:
```bash
./pw screenshot
```
**Tip:** If the donation/checkout page has an amount field, fill it before the card details. Some sites validate amount first.
**Synthetic-fill detection:** Stripe Elements and similar modern tokenizers silently reject synthetic property-setter fills — the submit button stays enabled, no error surfaces, nothing submits, no network request fires. If `./pw fill` dispatches real keystroke events this is fine; if it only sets `.value`, the submit will stall for PAN/CVV/expiry fields specifically. Workaround: use coordinate-click to focus plus compositor-level keystroke events (`Input.insertText` at CDP) for card fields. Billing-address fields are not guarded the same way.
**Card-swap close-and-reopen rule:** After pulling fresh creds mid-flow (e.g., retry at a higher amount), close the merchant's payment form entirely and re-open before filling. The processor's validator can retain stale state from the prior card and refuse to tokenize the new one (observed on Stripe, 2026-04-21). Do not overwrite PAN/CVV in place.
#### Merchant form quirks — expiration date
- **Stripe Elements:** single combined field, format `MM / YY`, auto-inserts the ` / `.
- **Vantiv eProtect** (e.g., CVS): separate Month/Year `` elements, values are 2-digit (`"29"` for 2029). Passing `"2029"` silently fails — the dropdown stays empty, with "Select a year / Expiration date invalid" only showing after submit.
- Always check whether the year field expects `YY` or `YYYY` before filling.
## Phase 2: Complete transaction
Digital merchants (donations, SaaS) post transactions **within seconds**. Physical merchants may take minutes to hours.
### Step 4 — Find the transaction
```bash
ramp transactions list --agent \
--transactions_to_retrieve my_transactions \
--page_size 5 \
--details_to_include_in_response submitted_items --rationale "List the user's transactions"
```
Match by amount + merchant name to find the transaction `id`.
### Step 5 — Check missing items
```bash
ramp transactions missing --agent "" --rationale "Check missing items on the transaction"
```
### Step 6 — Fill missing items
#### Memo
Use AI suggestions when available:
```bash
ramp transactions memo-suggestions --agent "" --rationale "Fetch AI-suggested memos"
```
Then set the memo:
```bash
ramp transactions edit --agent "" \
--memo "Donation to Children's Hunger Fund" \
--user_submitted_fields memo --rationale "Update the transaction for the user"
```
#### Tracking categories
First, list required categories:
```bash
ramp accounting categories --agent --transaction_uuid "" --rationale "List tracking categories"
```
Then look up options for each category:
```bash
ramp accounting category-options --agent "" \
--transaction_uuid "" \
--query_string "search term" \
--page_size 10 --rationale "List options for the tracking category"
```
Set categories via `--json` for batch updates:
```bash
ramp transactions edit --agent --json '{
"rationale": "Update the transaction for the user",
"transaction_uuid": "",
"tracking_category_selections": [
{"category_uuid": "", "option_selection": ""}
],
"user_submitted_fields": ["tracking_category_selections"]
}'
```
**When you don't know what to fill:** Do not guess. Look up available options, present 3-5 best matches to the user, and ask them to pick.
#### Trip
For travel-related transactions:
```bash
ramp transactions trips --agent --rationale "List trips to assign to the transaction"
```
Then assign:
```bash
ramp transactions edit --agent --json '{
"rationale": "Update the transaction for the user",
"transaction_uuid": "",
"trip_selection": {"trip_uuid": ""},
"user_submitted_fields": ["trip_selection"]
}'
```
Not travel-related:
```bash
ramp transactions edit --agent --json '{
"rationale": "Update the transaction for the user",
"transaction_uuid": "",
"trip_selection": {"mark_not_part_of_trip": true},
"user_submitted_fields": ["trip_selection"]
}'
```
#### Receipt
**Upload a receipt from file (e.g., screenshot of confirmation page):**
```bash
ramp receipts upload --agent \
--filename "receipt.png" \
--content_type "image/png" \
--file_content_base64 "$(base64 < /path/to/receipt.png)" \
--transaction_uuid "" --rationale "Upload the receipt"
```
**Attach an existing receipt:**
```bash
ramp receipts attach --agent "" "" --rationale "Attach the receipt to the transaction"
```
**No receipt — provide reason:**
```bash
ramp transactions explain-missing --agent "" \
--reason "Online donation — no receipt issued" --rationale "Record why the receipt is missing"
```
**No receipt — flag as missing:**
```bash
ramp transactions flag-missing --agent "" --rationale "Generate a missing-receipt affidavit link"
```
**Pro tip:** After a successful browser checkout, take a screenshot of the confirmation page, save it, and upload it as the receipt. This covers the receipt requirement automatically.
### Step 7 — Verify completion
```bash
ramp transactions missing --agent "" --rationale "Check missing items on the transaction"
```
Confirm all items resolved: `missing_receipt: false`, `missing_memo: false`, `missing_accounting_items: []`.
## Error handling
| Error | Action |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| No agent card access | Enroll the business via `ramp agent_cards enroll --agent`, then retry |
| Fund not eligible / no eligible card | Re-run purpose-first selection; if no appropriate eligible fund exists, ask the user instead of trying broad/admin funds automatically |
| Insufficient balance | Pick a fund with more balance |
| Credential retrieval failed | Retry once, then try a different fund |
| 401 / token expired | Re-authenticate for the active env: `ramp --env auth login` (auth state is stored per environment) |
| Transaction not found after payment | Wait 30s and retry `transactions list` — some merchants have delayed posting |
| Card form submitted but nothing happens (button enabled, no error, no processing, no spinner) after 15s | Suspected silent tokenizer rejection of synthetic property-setter fills. Close form, re-open, re-fill via real keystrokes. |
| Final charge amount exceeds cryptogram auth | Cryptogram caps at `--amount`. Burn current creds, pull fresh at new amount. Re-preauth if > 10% over original. |
| Pre-existing items in cart from prior session | Remove them (or ask the user to) before generating creds. |
| 3DS challenge | Stop. Screenshot, then tell the user to complete 3DS in the visible Chrome window and reply when done. Resume Phase 2 once the card clears. See human-handoff in [browser-checkout.md](browser-checkout.md). |
| CAPTCHA / reCAPTCHA / bot-check page | Stop. If the browser is headless, `./pw stop` and re-open headed first — the user cannot interact otherwise. Then screenshot and hand off per human-handoff in [browser-checkout.md](browser-checkout.md). Do not attempt a programmatic solve. |
| Merchant shows "declined" | A BIN decline at one processor (e.g., Vantiv eProtect at CVS) does NOT predict decline at another (e.g., Stripe at Anthropic) — each has its own risk engine. Report the merchant AND the processor (identifiable from the payment iframe's `src` if possible) so we track the BIN-vs-processor matrix. Do not retry at the same merchant. |
### Tips
- Each `agent_cards creds` call generates a fresh CVV. If you need to retry a checkout, call `creds` again first.
- Prefer merchants with single-page checkout forms over multi-step embedded widgets.
- Some merchants have minimum transaction amounts — check before attempting small purchases.
## Workflow summary
```
agent_cards list → pick fund
→ agent_cards creds → get PAN/CVV
→ browser: open merchant → fill payment → submit
→ transactions list → find txn
→ transactions missing → check gaps
→ transactions edit (memo, categories, trip)
→ receipts upload (confirmation screenshot)
→ transactions missing → verify clean
```
---
URL: https://agents.ramp.com/skills/ramp-apply-for-account
---
name: ramp-apply-for-account
area: Getting Started
supported_surfaces: [cli]
description: |-
Start or complete a Ramp financing application. Use for application signup,
missing information, documents, banking, follow-ups, progress checks, and
clear handoffs between an agent and the person applying.
---
# Apply For A Ramp Account
Use the live `ramp applications` commands and responses as the source of truth.
Keep the conversation simple. Explain business or API terms only when needed.
## Start With A Choice
First determine whether the person wants to start a new application or continue
an existing one. Then offer a collaboration style:
- **Guided:** ask short questions one section at a time.
- **Use documents:** read only the files they provide, extract relevant facts,
and ask them to confirm before writing.
- **Mostly in Ramp:** do what the agent can, then proactively provide direct
Ramp links for the person to finish.
Recommend guided mode when they are unsure. They may switch modes at any time.
Use production by default — nearly everyone using this skill is filing a real
application, and every example below uses `--env production`. Sandbox is
available only when the user explicitly requests a test application: replace
`production` with `sandbox` in every command and confirmation, and never mix
environments within one application flow. Say plainly that production starts a
real signup and underwriting process.
## Start A New Application
Inspect the current payload shape:
```bash
ramp applications schema --env production --agent
ramp applications create --env production --example --agent
```
Ask: **"What email should receive the Ramp application invite?"**
Collect only the fields required to create the application. Use only facts the
person supplied or confirmed. Never treat example values as defaults.
Ask whether the business already has a formed legal entity with an EIN. If it
does not, set `needs_incorporation: true` in the create payload and do not send
formed-entity details such as legal name, EIN, incorporation date, or
incorporation state in the initial create request. The resulting
unformed-entity flow is handled in two phases: first collect and PATCH the
provisional incorporation preferences required by the financing application,
then after the applicant reviews and submits FA, use `ramp-incorporate` for
the actual formation filing. Incorporating through Ramp is not free — pricing
and details are the source of truth at
https://agents.ramp.com/docs/account/incorporate; when the business has no legal
entity yet, tell the person incorporation has a cost and offer to fetch that page
before they commit. Before running incorporation write commands,
confirm the CLI is authenticated as the newly-created applicant business; an
older agent-key session will operate on its older business instead.
Before sending, give one concise confirmation:
> Ready to send a real Ramp application invite to `` for
> `` in production. This starts signup but does not submit the
> application. Should I send it now?
After confirmation:
```bash
ramp applications create --env production \
--json '' --wait_for_auth --agent
```
The confirmed JSON payload must include a non-empty `"rationale"` key alongside
the application fields (e.g. `"rationale": "User confirmed sending the Ramp
application invite for "`) — without it the create is rejected
with a validation error and no invite is sent.
Keep the command running. **The flow is blocked until the recipient opens the
invite email and accepts it.** Lead with the email and say it plainly — do not
imply anything opens automatically on their end. Name the inbox:
> **I've sent an application invite email to ``. Open your email
> inbox now and accept the invite** — nothing pops up automatically. I'll wait
> right here until you do.
Then tell them what to do from the email:
1. Open the Ramp invite email in ``'s inbox (check spam if you
don't see it).
2. Click the link in the email, then create an account or sign in.
3. Choose **Start application**.
4. Return here after the browser step completes.
Do not describe this as only a "browser" step or imply a window will open on its
own — the person must first open their email inbox and accept the invite.
The CLI saves scoped credentials when the browser callback succeeds. If it
returns `authenticated: false`, the application still exists. Run the returned
fallback login command, then continue below.
If the user interrupts `--wait_for_auth` after the invite is created, the CLI
returns `interrupted: true`, `authenticated: false`, and, when the API provided
one, `invite_link`. Treat this as a successful application creation with an
unfinished auth handoff: show the invite link, ask the recipient to finish the
Ramp browser step, and use the returned fallback command only if credentials are
still needed.
If creation returns a rate-limit error, explain when they can retry. Do not
automatically retry or change identities, sessions, or networks to evade it.
An existing Ramp email may continue an existing application instead of creating
a new one. Do not imply that a second application was created.
## Continue An Application
Check authorization:
```bash
ramp auth status --env production --agent
```
The normal workflow needs `applications:read`, `applications:write`, and
`bank_accounts:read`. If incorporation may be needed, also include
`incorporation:read` and `incorporation:write` so the `ramp-incorporate`
skill can continue without a second auth handoff. If missing:
```bash
ramp auth login --env production \
--auth-level business \
--scope applications:read \
--scope applications:write \
--scope bank_accounts:read \
--scope incorporation:read \
--scope incorporation:write
```
This opens a browser-based authorization. **The user must open the Ramp email
they receive and complete the sign-in step before authorization can finish.**
The CLI waits for the browser callback; do not proceed until it succeeds.
Any sanctioned connection user may authorize the intended business. If Ramp
shows a business chooser, ask them to select the business whose application
they want to complete. Read the application, state the returned legal business
name and masked applicant email, and ask them to confirm it is the intended
application before the first write. If it is not, stop and reauthorize the
correct business.
## Run The Progress Loop
Always start or resume with:
```bash
ramp applications progress --env production --agent
```
Inspect the full `required_actions`, `optional_actions`,
`ready_for_submission`, and status before acting.
When the next step is a known applicant browser action and the person is acting
now, use the progress command's wait flags instead of repeatedly polling by hand:
```bash
# After sending the person to complete phone verification
ramp applications progress --env production --agent \
--wait_for_phone_verification --wait_interval 15 --wait_timeout 900
# After sending the person to complete Onfido / identity verification
ramp applications progress --env production --agent \
--wait_for_identity_verification --wait_interval 15 --wait_timeout 900
# For any exact required-action enum, applicant-action enum, page_key, or section_key
ramp applications progress --env production --agent \
--wait_for_action --wait_interval 15 --wait_timeout 900
```
Use wait flags only after you have shown the required link or instructions and
the person has agreed to do that step now. If the wait times out, surface the
pending action from the CLI error, re-fetch progress if needed, and ask whether
they want more time or a fresh link. Do not use wait flags with `--dry_run`.
1. First look for independently actionable work, even if other required actions
are owned by Ramp or the applicant.
2. If a required action is `COMPLETE_INCORPORATION` with provider `doola`, use
the `ramp-incorporate` skill to file the LLC before continuing. For any
other provider, explain that the incorporation provider is not supported by
the CLI yet and show the current Ramp link if one is returned.
If `ramp-incorporate` is not already installed in the active agent
environment, fetch it from the same Ramp CLI binary and version you are using:
`ramp skills show ramp-incorporate`. Do not search old local checkouts,
prior Codex work directories, or other stale skill snapshots.
3. Do not use `WAIT_FOR_RAMP` alone as an incorporation signal; formed
businesses also wait on ordinary Ramp review. If there is no active
`COMPLETE_INCORPORATION` blocker, use `ramp-incorporate` only when the
person explicitly asked to file an LLC for the current unformed-entity
application, the application still shows `needs_incorporation=true` or no
formed entity/EIN, and the submitted application is waiting on Ramp (usually
`status`/`state` = `WAIT_FOR_RAMP`). Confirm the current business-scoped
OAuth session before write commands. Do not wait for Ramp approval,
underwriting, or identity review after submission in this explicitly
unformed-entity path.
4. Complete independently actionable `actor=AGENT` work.
5. Re-fetch progress after every meaningful write.
6. For `actor=APPLICANT`, explain the step and show its current Ramp link.
7. If any remaining required action has `actor=RAMP`, explain that Ramp is
reviewing or processing and stop before writing.
8. Show optional actions separately. They never block required work.
Do not stop at the first human action if other agent-owned work can be completed
independently. Treat Ramp-owned actions as stop conditions only after
incorporation and other independent agent-owned work have been handled.
Do not hand off an applicant-owned browser step while required agent-owned
application data is still visible and independently writable.
Continue until the agent has no remaining work, the person must act, Ramp owns
the next step, or the application is `APPROVED`, `REJECTED`, or `WITHDRAWN`.
When progress returns `ready_for_submission=true`, run
`ramp applications get --env production --agent` before telling the person the
application is ready. For `needs_incorporation=true` applications, verify that
`business.incorporation` contains the provisional entity type, filing state, and
intended filing date whenever `date_of_incorporation` is present in the live
application/edit schema. Progress may omit a validation issue for this field, so
do not treat missing progress validation as proof the intended date can be
skipped. If those fields are null or missing, collect and patch them first,
leaving EIN empty/null unless Ramp has already issued one. This check applies
even when the only remaining required action is `REVIEW_AND_SUBMIT`; do not send
the person to final review/submission while provisional incorporation fields are
still missing or unconfirmed.
After that check, `ready_for_submission=true` means the application is ready for
the person to review and submit in Ramp. The agent must not perform final
submission or accept legal agreements. Do not start incorporation formation yet;
show the submission link, wait for the person to submit, then re-fetch progress.
When the submitted application is waiting on Ramp (usually `WAIT_FOR_RAMP`),
start incorporation if the LLC still needs to be filed.
## Minimize Browser Touchpoints
During application data collection, the only forced browser touchpoints should
be:
- accepting the invite email;
- phone verification, when progress returns it;
- SSN entry;
- Onfido identity verification.
Everything else should be completed through the CLI when the API allows it. In
guided mode, collect all visible non-sensitive missing facts from the current
progress response before the next browser handoff. This includes general
details, principal business address, financial estimates, provisional
`business.incorporation` fields for `needs_incorporation=true` applications,
domain-mismatch explanation, and owner/controller fields such as title, birth
date, residential address, email, phone, role, and ownership percentage.
When every API-writable/non-sensitive application field has been submitted and
the only remaining applicant-owned actions are SSN entry and optionally phone
verification, present the returned Ramp form link(s) together so the person can
complete them in one browser session. If they are completing phone verification
or Onfido now, run the matching
`ramp applications progress --wait_for_phone_verification` or
`--wait_for_identity_verification` command so the agent resumes as soon as the
required action clears. After the wait returns, continue from the returned
progress payload as the new source of truth. Final review and submission is also
applicant-only, but do not send the person there until all API-writable data and
required browser verification steps are complete.
## Unformed Entity Incorporation Fields
For `needs_incorporation=true` applications, the financing application may still
require an **Incorporation** section before the person can submit FA. Treat those
fields as intended/provisional filing facts needed by FA, not proof that the
entity already exists.
Before surfacing phone verification, SSN entry, identity verification, or final
review/submission, inspect the live application edit schema and formation
reference data:
```bash
ramp applications get --env production --agent
ramp tools schema applications edit --env production --agent
ramp applications edit --env production --help
ramp incorporation states --env production --agent
ramp incorporation industries search --env production --q "" --agent
```
Use the edit schema as the source of truth for which provisional incorporation
fields are writable, but do not present unsupported formation types as options.
The current Ramp formation flow is LLC-only; if the application schema asks for
entity type, confirm the applicant wants an LLC and set the LLC value the schema
expects. If the applicant wants another entity type, stop and hand off to Ramp
or the team instead of continuing with `ramp-incorporate`. Use
`ramp incorporation states` to present valid filing states, and search
industries with business-specific keywords when an industry/NAICS field is
needed.
When the FA surface asks for business type and state:
- fill the intended entity type and state before FA submission;
- ask the user which valid state they want to incorporate in after fetching the
available states;
- confirm the applicant wants to form an LLC before writing the entity type;
the current Ramp formation flow does not support choosing other entity types;
- patch `date_of_incorporation` before FA submission whenever the current
application/edit schema exposes it for the unformed-entity provisional FA
section. Treat it as the intended filing date, confirm that the user is
providing an intended filing date, and ask whether it should be today's date,
naming the exact date in the user's local timezone. Do not infer or reuse an
actual incorporation date because Ramp backfills that after approval;
- leave EIN empty/null; treat it as optional here, but do not provide it in
tool calls for `needs_incorporation=true` because Ramp backfills it after
incorporation completes;
- do not skip this step because Doola formation has not been submitted yet;
- after the person submits FA, use `ramp-incorporate` for the actual Doola
formation filing.
## Make Human Handoffs Obvious
Whenever a current `deep_link_url` is returned, show it prominently:
> **Required human step:** Verify your identity
>
> **Open in Ramp:** ``
For an agent-owned step that also has a link:
> I can complete this here. If you prefer to take over:
>
> **Open in Ramp (optional):** ``
Be proactive; do not wait for the person to ask for the link. Say whether the
link is required or an optional takeover. If it may be stale, fetch progress or
follow-ups again and use the newly returned URL. Never invent a Ramp or
third-party link.
## SSN — Browser Handoff Only
**Never ask for or accept SSN (last-4 or full 9-digit) in chat, CLI prompts, env vars, or `--json`.** SSN entry must happen exclusively through the Ramp browser form link that Ramp returns, including when the `ramp-incorporate` workflow is active.
Fill all other application fields programmatically via the CLI before asking the
person to handle SSN in Ramp. If `validation_issues` for
`controlling_officer` or `beneficial_owners` include both SSN and non-sensitive
missing fields such as address, birth date, title, email, phone, or ownership
percentage, collect and PATCH the non-sensitive fields first, then re-fetch
progress. Show the SSN browser link only after those non-sensitive fields are
submitted and the only other remaining applicant-owned action is phone
verification.
When SSN collection is needed, there are two cases:
**Last-4 case (Leaders & Owners — `controlling_officer` / `beneficial_owners`):**
When `ramp applications progress` returns a required action for the
`controlling_officer` or `beneficial_owners` section that returns a non-null
`deep_link_url`, present that link as the required way to enter SSN:
> **Required step — enter SSN in Ramp:**
>
> **Open in Ramp:** ``
>
> Complete SSN entry in the browser, then return here and let me know so I can
> re-fetch your application progress.
If the required action for `controlling_officer` or `beneficial_owners` returns
a **null** `deep_link_url` (feature not yet deployed), do **not** ask for SSN
in chat. Instead, tell the user:
> SSN must be entered directly in the Ramp application in your browser. Please
> open your Ramp application, navigate to the Leaders & Owners section, and
> complete SSN entry there. Let me know when done and I will re-fetch progress.
**Full-9 case (KYC follow-up):**
When `ramp applications followups` returns a follow-up with `reason` of
`KYC_SSN_VERIFICATION` or `KYC_SSN_FULL_9_VERIFICATION` that returns a
non-null `deep_link_url`, present that link prominently:
> **Required step — verify your full SSN in Ramp:**
>
> **Open in Ramp:** ``
>
> Complete SSN verification in the browser, then return here and let me know so
> I can re-fetch your follow-ups.
After the user confirms they have finished the browser step, re-fetch progress
or follow-ups and continue. When the browser step maps to a progress required
action, prefer `ramp applications progress --wait_for_action `
while the user completes it. Never invent a link; use only the `deep_link_url`
returned by the API.
## Ask Only For The Next Missing Facts
Use `page_key`, `section_key`, `guidance`, follow-up prompts, accepted document
types, and live command help to explain:
- what is missing;
- why Ramp needs it now;
- where the person can usually find it; and
- whether they can complete it directly in Ramp.
Batch related, non-sensitive questions from all current agent-owned required
actions so the person does not have to bounce between Ramp and chat. Keep
sensitive requests separate.
Prefer plain questions such as:
- "What is the legal name shown on the business's formation or tax document?"
- "What email should we use for this owner?"
- "Is the controlling officer also an owner? If so, what percentage do they own?"
- "What is the controlling officer's residential address?"
- "Ramp currently supports LLC formation here. Do you want an LLC, and which
valid filing state should we use?"
- "Would you like to open a Ramp checking account, add an external account, or
do both?"
## Owners And Control
Early-stage businesses often have a controlling officer who is also a
beneficial owner. Ask about role and ownership separately before writing the
Leaders & Owners fields.
When the controlling officer owns 25% or more of the business, include both
`controlling_officer.is_beneficial_owner: true` and
`controlling_officer.ownership_percentage` as a whole number from 25 to 100.
Do not duplicate the same person in `beneficial_owners` just because they are
both the controller and a 25%+ owner. Put other 25%+ owners in
`beneficial_owners`, and include each owner's `ownership_percentage`.
Before calling `ramp applications edit`, normalize owner/controller emails
case-insensitively and shape the desired payload:
- if any `beneficial_owners[*].email` matches `controlling_officer.email`, merge
the non-sensitive owner fields into `controlling_officer`;
- set `controlling_officer.is_beneficial_owner: true`;
- set `controlling_officer.ownership_percentage` when known;
- remove that matching entry from `beneficial_owners`;
- never retry by appending the same beneficial owner again; fetch current state
and PATCH the deduped desired state.
Example for a sole owner who is also the controlling officer:
```json
{
"controlling_officer": {
"email": "owner@example.com",
"is_beneficial_owner": true,
"ownership_percentage": 100
},
"beneficial_owners": []
}
```
When the controlling officer owns less than 25%, capture their
`controlling_officer.ownership_percentage` if the field is available, but do not
mark them as a beneficial owner. Set
`controlling_officer.is_beneficial_owner: false` when the field is present and
list only 25%+ owners in `beneficial_owners`.
If the controlling officer does not own the business, set
`controlling_officer.is_beneficial_owner: false` when the field is present and
list the actual beneficial owners separately. Only send
`ownership_acknowledgement` after the person explicitly confirms that all
25%+ beneficial owners have been captured.
When no individual owns 25% or more, require that explicit confirmation before
writing an empty owner list. Set `beneficial_owners: []`,
`controlling_officer.is_beneficial_owner: false`, and
`ownership_acknowledgement: CONFIRM_NO_INDIVIDUAL_OWNS_25_PCT`. Do not use that
acknowledgement merely because no owner was entered yet. This ownership shape is
valid for the financing application, but `ramp-incorporate` will separately
collect the LLC members required for formation after the application is
submitted.
If the applicant's business email domain does not match the business website
domain, surface that mismatch plainly before writing it, and ask for a brief
reason (for example, a personal email or a parent-company domain). Do not block
on it — capture the explanation and continue.
Useful sources include formation documents, an IRS EIN letter, a cap table,
financial statements, bank statements, and official address records. These are
examples, not requirements.
If several sections need documents, offer to use a private folder chosen by the
person. Read only files intentionally provided for this application. Do not scan
unrelated directories or write extracted PII, credentials, OTPs, or assembled
application payloads to disk.
## Record Agent Context When Supported
Before each generated mutation, inspect its live schema or help. When it exposes
the following fields, include both:
- `user_prompt`: the person's natural-language request that led to the write.
- `data_source_summary`: a short description of where the written facts came
from.
Keep the source summary specific but free of sensitive values. Examples:
- `Legal name and EIN from the IRS letter provided by the user.`
- `Revenue confirmed by the user from QuickBooks.`
- `Document selected and authorized by the user for this follow-up.`
Use the current generated command options or JSON body. If the live operation
does not expose these fields, omit them rather than inventing unsupported
arguments. Do not invent provenance.
## Update Application Data
Read current state and schema first:
```bash
ramp applications get --env production --agent
ramp tools schema applications edit --env production --agent
ramp applications edit --env production --help
```
PATCH only supplied or source-backed fields. Omit unknown fields:
```bash
ramp applications edit --env production \
--json '{
"":""
}' --agent
ramp applications progress --env production --agent
```
When the live edit schema exposes agent-context fields, add the `user_prompt`
and `data_source_summary` described above to the JSON body.
Treat validation errors as correctable input issues. Ask for the rejected value;
do not guess or repeatedly PATCH alternatives.
## Handle Follow-Ups
```bash
ramp applications followups --env production --agent
```
Inspect `actor`, `type`, `status`, `is_complete`, `is_required`, `prompt`,
`description`, `deep_link_url`, `accepted_document_types`, and
`selected_document_type`.
- Skip completed follow-ups.
- Label `is_required=false` as optional.
- For `REVISION_REQUESTED`, explain the current request and reassess the prior
response.
- If the application is still in the Ramp incorporation flow (`COMPLETE_INCORPORATION`
is active, or `ramp incorporation status` is still pre-`APPROVED`), do not
surface certificate-of-incorporation, articles-of-incorporation, or EIN/EIN
letter KYB follow-ups as current user action items yet. Acknowledge them as
pending formation outputs and continue with any unrelated work.
- If any follow-up has `actor=RAMP`, explain that Ramp owns the next step and
stop before writing.
- Never mutate an applicant-owned follow-up. Show its link.
- Complete agent-owned follow-ups before asking the person to handle unrelated
applicant-owned work.
Text response:
```bash
ramp applications update-followup --env production \
--json '{
"response_text":""
}' --agent
```
When the live follow-up schema exposes agent-context fields, add them to the
JSON body.
For a document follow-up, prefer a matching `accepted_document_types` value. If
the prompt or description requests a bespoke type, use that exact
human-confirmed type even when it is not listed. Do not invent or generalize
types. If `selected_document_type` already matches, do not update it again.
Upload only the file the person selected and explicitly authorized; selecting a
type does not upload the file.
When `selected_document_type` must be set or changed:
```bash
ramp applications update-followup --env production \
--json '{"selected_document_type":""}' --agent
```
```bash
ramp applications upload --env production \
--file --purpose FOLLOWUP --followup_id --agent
ramp applications followups --env production --agent
```
When the live upload schema exposes agent-context fields, include them using its
generated options.
After all required follow-ups are complete and the response reports
`is_submission_ready=true`, submit the follow-up package and refresh progress:
```bash
ramp applications submit --env production --agent
ramp applications progress --env production --agent
```
When the live submit operation exposes agent-context fields, include them using
its generated options. This is not final application submission.
## Handle Banking
Banking has three possible paths:
- open Ramp checking;
- link one or more external accounts in Ramp; or
- add one or more manual accounts through the agent workflow.
Ask which path the person wants. Ramp checking and external accounts are not
mutually exclusive.
For provider linking, show the current Ramp link as a required or optional human
handoff. Never ask for online-banking credentials.
For a manual account:
1. List current accounts and documents before making changes.
2. Reuse an existing applicant-confirmed manual account when it matches the
intended account; associate uploads with its `manual_bank_account_id`.
3. Add a manual account only when the person explicitly wants a new one, using
details they provided for this application and agent context when exposed.
4. Upload only the supporting documents missing for the selected account.
5. Re-list accounts and documents, then refresh progress.
Paginate until `pagination.next_cursor` is null. Never alter a cursor.
For the standard manual-statement path, match documents by
`manual_bank_account_id`. Three active `BANK_STATEMENT` documents complete that
path; three is not a general upload maximum. Do not upload extras after the
requirement is met.
For each missing statement:
```bash
ramp applications upload --env production \
--file --purpose BANK_STATEMENT \
--manual_bank_account_id --agent
ramp applications documents --env production --page_size 100 --agent
```
When the live upload schema exposes agent-context fields, include them using its
generated options. Recount active statements for that account after each upload.
Adding accounts does not complete a `BANK_ACCOUNT_LINK_REQUEST` follow-up. After
refreshing the account list, ask the person to confirm that there are no more
accounts to link. Only after explicit confirmation, update that follow-up:
```bash
ramp applications update-followup --env production \
--json '{
"no_more_accounts_to_link":true
}' --agent
ramp applications followups --env production --agent
ramp applications progress --env production --agent
```
When the live follow-up schema exposes agent-context fields, include the
person's confirmation as `user_prompt` and identify the person as the source in
`data_source_summary`.
Do not use this response to bypass an account-linking flow the person still
wants to complete.
If an active agent-managed Ramp account number becomes available and the person
asks for receiving details, use `ramp treasury account-numbers --env production
--agent` with the narrow `agent_account_numbers:read` scope. Treat returned
account and routing numbers as sensitive: show them only when requested and do
not repeat them in status summaries.
## Safety Boundaries
- Never invent application facts or sources.
- Never request full government IDs, OTPs, online-banking credentials, or other
secrets in chat.
- Never collect SSN (last-4 or full 9-digit) in chat; always hand off to the
returned `deep_link_url` as described in the **SSN — Browser Handoff Only**
section above.
- Mask sensitive values in confirmations and summaries.
- Do not use `--dry_run` with sensitive data because it prints the request.
- The person must accept invitations, complete applicant-owned identity steps,
accept legal agreements, review the application, and submit it.
- An agent may transmit an explicitly selected identity document only when the
API marks that follow-up `actor=AGENT`; Ramp evaluates the document.
- If sanctioned browser, email, or SMS access exists and the person authorizes
it, the agent may assist outside this API workflow. This is not officially
supported and must not be assumed.
## If You Get Blocked
If the user wants to report an unresolved public CLI error, ask for consent and
have them approve the exact message before sending feedback:
```bash
ramp feedback ""
```
This sends the approved message to Ramp support. Do not include secrets,
resource identifiers, raw application output, or diagnostic artifacts.
## Optional Goal And Monitoring Examples
When the agent environment supports `/goal`, suggest:
```text
/goal Help me finish my Ramp application. Complete agent-owned steps, show me
links for human steps, and stop before final submission.
```
When it supports `/loop`, suggest:
```text
/loop Check my Ramp application every 30 minutes. Complete agent-owned
follow-ups that need no new information, and tell me when I need to act or when
the application is approved.
```
A loop must not invent information, bypass confirmation, perform final
submission, or retry a rate limit.
---
URL: https://agents.ramp.com/skills/ramp-approval-dashboard
---
name: ramp-approval-dashboard
area: Approvals
supported_surfaces: [cli, mcp]
description: |-
Review and approve pending transactions, bills, reimbursements, and requests.
Use when: 'approve', 'pending approvals', 'what needs my approval',
'review transactions', 'approve bills', 'reject', 'approval queue',
'clear my approvals'. Do NOT use for: transaction analysis, receipt uploads,
or spend tracking.
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Always show the item details before approving or rejecting. Never blind-approve.
- Rejections require a reason. Approvals do not, but a note is helpful.
- Confirm with the user before executing approvals — especially bulk operations.
- Present items sorted by priority: highest dollar amount first.
- Amounts vary by endpoint: bill `amount` values are numeric **major currency units**; display them directly with the accompanying `currency` and never divide by 100. When totaling bills, group subtotals by `currency`; never combine different currencies or include them in a cross-queue aggregate. Reimbursements are in **dollars**, and transactions are formatted strings ("$135.40").
- **Deep links**: If the response contains a `bill_url` (bills) or `reimbursement_link` (reimbursements) field, include it when presenting entity details so the user can click through to the Ramp web app. If these fields are absent, direct the user to the relevant Ramp page (e.g., `https://app.ramp.com/bills`) instead. Never fabricate deep link URLs.
## Workflow
### Step 1: Fetch all pending items
Run these in parallel to build the full approval queue. **Paginate each endpoint until there are no more results** — a single page may not return everything.
```bash
# Pending transactions (paginate with --next_page_cursor)
ramp transactions list --transactions_to_retrieve transactions_awaiting_my_approval \
--agent --page_size 50 --rationale "List the user's transactions"
# Pending bills (paginate with --page_cursor)
ramp bills pending --agent --limit 50 --rationale "Review bills pending approval"
# Pending reimbursements (no cursor pagination — use --limit)
ramp reimbursements pending --agent --limit 50 --rationale "Review reimbursements pending approval"
# Pending requests (paginate with --start; --thoughts is required)
ramp requests pending --thoughts "Reviewing all pending requests" --page_size 50 --agent --rationale "Review pending requests"
```
For each endpoint, check `pagination.next_cursor` in the JSON envelope. If it is not null, re-run the command with that cursor value (via `--next_page_cursor` for transactions, `--page_cursor` for bills, `--start` for requests) until all pages are fetched. Note: `reimbursements pending` does not support cursor-based pagination — it only has `--limit`, so increase the limit if you need more results. Aggregate results before presenting.
### Step 2: Present the queue
Summarize what's pending. If the response includes `bill_url` or `reimbursement_link` fields, include them so the user can open items directly in the Ramp web app:
```
Approval queue: 14 items
Bills (4 items):
USD 3,500 HighSpot Invoice #1234 Due 2026-03-28 →
USD 2,500 Cometeer Invoice #5678 Due 2026-04-01 →
...
Reimbursements (6 items, $2,100):
$ 520 Michael Scott Uber rides Submitted 2026-03-20 →
$ 312 Oscar Martinez Office supplies Submitted 2026-03-22 →
...
Transactions (3 items, $12,500):
$5,000 Dana Alhasawi AWS 2026-03-15
...
Requests (1 item, $650):
...
```
For request rows, preserve the exact `unified_request_id` returned by
`ramp requests pending`; use it for both detail lookup and approval.
### Step 3: Review and act
For each item the user wants to act on, get details first:
```bash
# Bill details
ramp bills get {bill_id} --agent --rationale "Review bill details"
# Transaction details
ramp transactions get {transaction_uuid} --agent --rationale "Review transaction details"
# Transaction missing items (if relevant)
ramp transactions missing {transaction_uuid} --rationale "Check missing items on the transaction"
# Reimbursement details (use list with specific UUID)
ramp reimbursements list --reimbursement_uuids '["{uuid}"]' --include_policy_assessment --agent --rationale "List the user's reimbursements"
# Request details
ramp requests get {unified_request_id} --agent --rationale "Review request details"
```
### Step 4: Execute approvals
```bash
# Approve a transaction
ramp transactions approve {transaction_uuid} --action APPROVE --thoughts "Reviewed — within policy" --rationale "Act on the transaction approval"
# Reject a transaction (reason required)
ramp transactions approve {transaction_uuid} \
--action REJECT_AND_REQUEST_CHANGES \
--thoughts "Missing receipt and over budget" \
--user_reason "Please attach the receipt and update the memo" --rationale "Act on the transaction approval"
# Approve a bill — not yet available via CLI.
# Use the bill_url from the response to direct the user to the Ramp app.
# Approve a reimbursement
ramp reimbursements approve {reimbursement_uuid} --action approve --rationale "Act on the reimbursement approval"
# Reject a reimbursement
ramp reimbursements approve {reimbursement_uuid} --action reject \
--user_reason "Receipt doesn't match the claimed amount" --rationale "Act on the reimbursement approval"
# Approve a request
ramp requests approve {unified_request_id} --action APPROVE --thoughts "Approved — within team budget" --rationale "Act on the request approval"
```
### Step 5: Add comments (optional)
Leave a comment on any Ramp object for context:
```bash
ramp general comment {object_uuid} \
--ramp_object_type transaction \
--message "Approved — this covers the Q1 offsite catering" --rationale "Add a comment for the user"
```
Valid object types: `bill`, `booking_request`, `card`, `payee`, `provisional_bill`, `reimbursement`, `spend_intent`, `spend_request`, `ticket`, `transaction`, `vendor_payment`.
## Bulk Approval
When the user wants to approve multiple items at once:
1. Present all items with amounts and details
2. Ask: "Approve all N items, or would you like to review individually?"
3. If bulk: confirm once, then execute sequentially
4. Report results: "Approved 12/14 items. 2 failed: [reasons]"
## Transaction Approval Actions
| Action | When to use |
|---|---|
| `APPROVE` | Item is within policy, receipt/memo present |
| `REJECT_AND_REQUEST_CHANGES` | Needs corrections — missing memo, wrong coding, etc. |
| `REJECT_AND_REQUEST_FULL_REPAYMENT` | Personal expense or policy violation requiring repayment |
## Example Session
```
User: What needs my approval?
Agent: Let me check all your pending queues.
> ramp transactions list --transactions_to_retrieve transactions_awaiting_my_approval --agent --page_size 50 --rationale "List the user's transactions"
> ramp bills pending --agent --limit 50 --rationale "Review bills pending approval"
> ramp reimbursements pending --agent --limit 50 --rationale "Review reimbursements pending approval"
> ramp requests pending --thoughts "Checking approval queue" --page_size 50 --agent --rationale "Review pending requests"
Approval queue: 6 items
Bills (2 items):
USD 2,200 HighSpot Invoice #4401 Due 2026-03-28
USD 500 Cometeer Invoice #882 Due 2026-04-01
Reimbursements (3 items, $1,430):
$ 520 Michael Scott Uber rides Submitted 2026-03-20
$ 312 Oscar Martinez Office supplies Submitted 2026-03-22
$ 598 Jan Levinson Flight change Submitted 2026-03-23
Transactions (1 item, $220):
$ 220 Dana Alhasawi DoorDash 2026-03-15
Want to review individually or approve all?
User: Approve the bills and reimbursements. Let me see the transaction first.
Agent: I can approve the reimbursements now. Bills must be approved in the Ramp app.
> ramp reimbursements approve reimb-001 --action approve --rationale "Act on the reimbursement approval"
> ramp reimbursements approve reimb-002 --action approve --rationale "Act on the reimbursement approval"
> ramp reimbursements approve reimb-003 --action approve --rationale "Act on the reimbursement approval"
3 reimbursements approved. For the 2 bills, approve them in the Ramp app:
HighSpot Invoice #4401 →
Cometeer Invoice #882 →
Now let me get details on that DoorDash transaction:
> ramp transactions get txn-001 --agent --rationale "Review transaction details"
> ramp transactions missing txn-001 --rationale "Check missing items on the transaction"
DoorDash $220 — Dana Alhasawi, 2026-03-15
Memo: "Team lunch" | Receipt: attached | Accounting: complete
User: Approve it
Agent: > ramp transactions approve txn-001 --action APPROVE --thoughts "Team lunch — receipt and memo present" --rationale "User confirmed approving the DoorDash transaction"
Done. Approval queue is clear.
```
## Deep Link Handoff
When the agent hits a limitation — bill approvals aren't supported via CLI, or step-up auth is required — hand off to the user with a link to the Ramp web app:
- **Bills**: If the response includes a `bill_url` field, use it — it routes to the correct bill page based on status (draft, approval, or paid). If absent, direct the user to `https://app.ramp.com/bills`.
- **Reimbursements**: If the response includes a `reimbursement_link` field, use it. If absent, direct the user to the Ramp reimbursements page.
Example handoff message (when `bill_url` is present):
```
I can't approve bills via the CLI. You can approve this bill directly in Ramp:
USD 3,500 HighSpot Invoice #1234 →
```
Always prefer deep link fields from the API response when available — they account for bill status and environment. Never fabricate deep link URLs.
## When NOT to Use
- **Uploading receipts or editing transaction memos/categories** — use ramp-complete-expenses
## Gotchas
| Issue | Fix |
|---|---|
| Bill amounts are numeric major currency units | Display the `amount` directly with `currency`; never divide by 100. Group totals by `currency` and never sum mixed currencies |
| Reimbursement amounts are in dollars | Display as-is |
| Transaction amounts are formatted strings | Strip "$" and "," for sorting/totaling |
| `requests pending` requires `--thoughts` | Always include it — describe what you're doing |
| Bill approvals are not yet available via CLI | Send the user to the Ramp app instead: `https://app.ramp.com/bills` |
| No undo for approvals | Confirm with user before executing. Use `-n` for dry runs on write commands. |
| Pagination varies | Check `pagination.next_cursor` in envelope. Pass it via `--next_page_cursor` (transactions), `--page_cursor` (bills), `--start` (requests). Reimbursements: `--limit` only. |
---
URL: https://agents.ramp.com/skills/ramp-book-flight
---
name: ramp-book-flight
area: Travel
supported_surfaces: [cli, mcp]
description: "Books flights conversationally: resolves cities to airports, searches one-way and round-trip flights, presents and compares offers, previews the fare, and tickets the booking on the traveler's explicit approval. Also cancels an existing flight booking with a preview-then-confirm flow when the cancellation capability is enabled. The user describes a trip in plain language ('book a flight from Toronto to SFO') and never needs to know a CLI command or tool name. Use when someone wants to book, find, search, or compare flights, says 'fly from X to Y', or wants to cancel a flight they booked. Not for changes, refund-status follow-ups, seat selection, hotels, cars, or multi-city trips."
---
# Book a Flight (conversational flight search)
The user describes a trip in plain words. Turn that into `ramp travel` commands (CLI) or
MCP tool calls, run them, and show clean results. **Never show or ask the user to type a CLI
command or tool name** — talk like a travel helper ("Searching Toronto → San Francisco,
Jul 1…"), not about flags or tool names.
The Steps, Phases, checklist, and flag names in this guide are **your** internal scaffolding —
never surface them to the user. Don't say "Phase 1," "Step 3," or name flags; just narrate in
plain travel language ("Let me pull up the fare and check it before booking…").
## Prerequisites
- **CLI:** `ramp` CLI installed and logged in (`ramp auth login`). Run where `ramp` works (or
`uv run ramp` inside the ramp-cli repo).
- **MCP:** Ramp MCP server connected. Tools are called by their CamelCase names
(`SearchFlights`, `SubmitFlightBooking`, `SubmitFlightCancellation`, etc.).
## Scope
- ✅ Resolve cities to airports; search one-way and round-trip; show offers.
- ✅ Round-trip: fetch the matching **return** flights for the outbound the user picks (Step 5).
- ✅ **Book the ticket** — preview, confirm only on an explicit yes, then verify (Step 6).
Booking spends **real money**; it defaults to the logged-in user unless the user explicitly
asks to book for another traveler.
- ✅ Compare cabins/fares — **only when asked** (see "Cabin and fare options").
- ✅ Read or update the traveler's profile, and read trips and bookings (see "Supporting tools").
- ✅ Book for another traveler when explicitly asked and authorized (see "Delegated booking").
- ✅ **Cancel an existing flight booking** — preview the exact terms, confirm only on an explicit
yes (see "Cancelling a flight booking"). Availability is per-business; degrade gracefully when
the command is missing.
- ❌ Changes/modifications, refund-status follow-ups after a cancellation, seat selection,
hotels, cars, and multi-city are outside this flow — point the traveler to the Ramp web app
or the booking's support channel instead.
## Rules for every command
- **Always `--output json`** on every `travel search-flight` call (including the Step 5 return
search). Parse it into a friendly table — never show raw JSON, and never pipe through
`python`/`jq`. MCP callers get structured JSON directly; the same table rule applies.
- Every command needs a `--rationale` (CLI) or `rationale` (MCP). Once the trip is known, name it
in every related command's rationale (route + dates, e.g. "Toronto→SFO, Jul 1-8") and keep that
same reference through search, returns, preview, book, and verify — rationales are logged, so a
consistent trip reference groups a trip's commands.
- `--departure`/`--arrival` (CLI) or `departure`/`arrival` (MCP) each take **one** value: an
airport code (`SFO`) or a Ramp city id (`search_code` from Step 2). No lists.
- **Both surfaces present results as Markdown tables** — never as UI components, cards,
interactive widgets, bullet lists, numbered lists, or prose. A Markdown table is the only
acceptable presentation format for flight offers, fare comparisons, and fund displays.
- `cabin_class` — offer **Economy**, **Premium Economy**, **Business**, **First** (API values
`ECONOMY`, `PREMIUM_ECONOMY`, `BUSINESS`, `FIRST`; never offer Basic Economy, though it can
still appear as a returned fare). Pass one value, or a list for alternatives like "Business or
First." **CLI:** required before the first search — ask it as part of Step 1's grouped
question. **MCP:** omit on the first search unless the traveler already gave a cabin; ask for
it after kickoff and send it on the next `job_id` read (Step 3).
- `include_fare_options` — always on, including pagination and the return-leg search.
- `wait_for_results` — **CLI:** always `true` (blocks until results are ready). **MCP:** always
`false` (returns immediately with a `job_id` and `search_complete: false`; gather preferences
and poll as described in Step 3) — **except** the Step 5 return-offer read, which is always
synchronous and rejects `false` on both surfaces.
These stay optional — add only when the traveler asks:
- `--limit` — off returns a default page. **Show at most 10 flights** in the results table;
if the response contains more, present the first 10 and tell the traveler they can ask for
more. Paginate with `next_cursor` (Step 3) when they do.
- `--sort_key` — off uses `WEIGHTED_SCORE`. Other keys: `LOWEST_TOTAL_AMOUNT`,
`SHORTEST_DURATION`, `LEAST_NUMBER_OF_STOPS`, `EARLIEST_DEPARTURE_TIME`,
`LATEST_DEPARTURE_TIME`, `EARLIEST_ARRIVAL_TIME`, `LATEST_ARRIVAL_TIME`. Applies to a new
search only — re-sort by starting fresh, not on a `job_id` page.
- Use `ramp travel search-flight` (CLI) or `SearchFlights` (MCP) for flight searches; check
`ramp travel --help` if the CLI alias is unavailable.
## Delegated booking
Only set `traveler_user_id` when the user explicitly asks to book for another person. Otherwise
omit it everywhere and book for the logged-in user.
When the user asks to book for someone else:
1. Resolve that traveler before profile preflight or search:
```bash
ramp users list --name_search "Taylor Smith" --page_size 5 \
--rationale "resolve the traveler for the Toronto→SFO trip" --output json
```
MCP:
```json
{
"name_search": "Taylor Smith",
"page_size": 5,
"rationale": "resolve the traveler for the Toronto→SFO trip"
}
```
Call `GetAllReducedUsers` with the above.
Use the returned traveler user UUID as `traveler_user_id`. If more than one user could match,
ask the requester to pick the exact traveler before continuing.
2. Preserve the same `traveler_user_id` across the whole delegated flow: profile preflight,
`profile-update` if needed, every `search-flight` / `SearchFlights` call (initial search,
resume pages, and round-trip return search), both `travel book` / `SubmitFlightBooking`
preview and confirm calls, and booking verification/retries. Do not switch traveler ids
mid-flow.
3. If traveler lookup fails or a target-aware call returns an authorization/empty-access result,
respond in plain travel-helper language. Do not mention `traveler_user_id`, flags, command
names, command counts, or CLI mechanics. Say you couldn't find/access that traveler and ask
whether to continue for the requester or search again with a Ramp email/exact profile name.
Example: *"I couldn't find a traveler named Emmy Song in this Ramp directory. If you want to
book for yourself, I can continue using your own traveler profile. Otherwise, send me the
traveler's Ramp email or exact profile name and I'll search again."* Do not silently fall back
to self-booking.
## Step 1 — gather trip details
Use what the user gave you; infer the rest.
- **CLI:** gather every required detail plus cabin and other preferences together in one grouped
question before searching. Never search first and ask about preferences later.
- **MCP:** gather only the required details before searching — destination, origin, dates, trip
type. Cabin and other preferences wait until after the search starts (Step 3).
Infer silently, then say back (don't ask):
| Slot | Assume |
|---|---|
| **Trip type** | round-trip if there's a return date, "back on…", or a stay length; else one-way. Ask only if truly unclear. |
| **Relative dates** | resolve to `YYYY-MM-DD` and always say the date back so mistakes surface before money moves. |
| **Airport given** | `SFO`, `JFK`, etc. → use directly, skip Step 2. |
Say assumptions in one line as you go — *"Searching JFK → SFO, Mon Jul 6, round-trip…"*.
If a required detail is still missing — plus, on CLI, cabin and other preferences — ask for all
of it in one grouped question (selectable options, one question per item). Required:
**destination**, **origin** (if no home airport to guess), **departure date**, **one-way vs
round-trip** (if unclear), and **return date** (round-trip). Keep dates in the future — 14+ days
out is safest (a common policy cutoff). Never re-ask what they told you.
## Step 2 — resolve a place to a `--departure`/`--arrival` value
- **Airport code** (`SFO`, `JFK`) → use directly, skip this step.
- **City/vague place** ("New York", "the Bay Area") → look it up first:
```bash
ramp travel locations --query "New York" --location_type city --limit 5 \
--rationale "resolve New York to a metro id for the user's trip" --output json
```
MCP:
```json
{
"query": "New York",
"location_type": "city",
"limit": 5,
"rationale": "resolve New York to a metro id for the user's trip"
}
```
Call `GetFlightBookingLocations` with the above.
For a **city**, the metro id is its **`search_code`** (a UUID; `iata_code` is empty). Pass
that one `search_code` to search the whole metro (New York covers JFK/LGA/EWR; Toronto covers
YYZ/YTZ). For one specific airport, search `--location_type airport` and pass its `iata_code`.
## Step 3 — search flights
Add `--return_date` only for round-trips and `--traveler_user_id` only for delegated bookings.
Pass `cabin_class`, `include_fare_options`, and `wait_for_results` per "Rules for every command."
**When the traveler named a departure weekday** ("leave Sunday", "out next Friday"), also pass
`--requested_weekday` with the lowercase day (`sunday`), if the flight-search command lists it
(skip it on an older CLI). The server rejects the search when the departure date doesn't fall
on that weekday, and the error names the correct nearby dates — retry with the corrected date
from the error; never clear the error by changing the weekday. It's departure-only: for an
**arrival** day ("be home by Sunday"), leave it off — the right flight may depart the day
before (a red-eye) — and let the Step 4 date strings show both days. If the rejection, or phrasing like "Sunday night", leaves it ambiguous whether the
traveler means late Sunday or a just-after-midnight Monday departure, ask them which they mean
instead of silently moving the date.
```bash
ramp travel search-flight --output json \
--departure YYZ --arrival SFO \
--departure_date 2026-07-01 --return_date 2026-07-08 \
--cabin_class ECONOMY --include_fare_options --wait_for_results=true \
--rationale "search flights for the Toronto→SFO trip, Jul 1-8"
```
MCP:
```json
{
"departure": "YYZ",
"arrival": "SFO",
"departure_date": "2026-07-01",
"return_date": "2026-07-08",
"include_fare_options": true,
"wait_for_results": false,
"rationale": "search flights for the Toronto→SFO trip, Jul 1-8"
}
```
Call `SearchFlights` with the above; include `cabin_class` only if the traveler already gave one.
### MCP: kick off search first, then gather preferences
1. The initial call above returns immediately with `search_complete: false` and a canonical
`job_id` — do not present empty offers as final; the search is still running. (It may already
return `search_complete: true` with offers; if so, skip straight to Step 4 and present them.)
2. In that same turn, ask the 1-4 most important unresolved preferences in one grouped
question: cabin class (always ask when not already known), then timing, airline, nonstop,
fare-tier, and price-versus-schedule preferences when relevant — the same set described in
"Rules for every command." Never mention that a search was started, is running, or is likely
to finish soon; lead directly into the questions. Stop after asking and do not call
`SearchFlights` again this turn.
3. On the traveler's next turn, process every answer, then call `SearchFlights` again with the
same `job_id` and `wait_for_results=false`. Resend `include_fare_options` and every currently
known preference — including `cabin_class` — since neither persists on the job on its own;
the traveler's newly given answers just add to that resent set. Omit `departure`, `arrival`,
`departure_date`, and `return_date` — the job retains the route and dates from the first call.
If the traveler **withdraws** a previously stated preference (e.g. "actually, airline doesn't
matter"), pass that field name in `clear_preferences` so the job drops it; never silently omit
a removed preference and never send a placeholder value.
4. If that response is still incomplete, acknowledge any applied preferences without narrating
search status or timing, ask one more concise question about any remaining materially useful
preference, then stop again. Limit preference gathering to **at most two turns** of questions
after the kickoff; if the search is still incomplete after that, simply wait for the next user
turn to poll without asking more questions. Once `search_complete` is `true`, present the full
results as in Step 4.
If a `SEARCH_STILL_RUNNING` error includes a `job_id`, apply the same rule and resume that job
only on a later user turn. Never sleep, back off, or present incomplete offers as final.
```json
{
"job_id": "{job_id_from_initial_response}",
"cabin_class": "ECONOMY",
"include_fare_options": true,
"wait_for_results": false,
"rationale": "apply the traveler's cabin preference to the Toronto→SFO flight search"
}
```
### CLI synchronous search
The CLI response (with `--wait_for_results=true`) normally contains the complete
ranked set. Check `search_complete` before presenting it. If it is `false`, do not
call search again in the same turn: explain that the search is still running and
wait for the traveler to ask to continue. On that later turn, re-call with the
response's canonical `job_id` and the same `cabin_class`/`include_fare_options`
settings. If a `SEARCH_STILL_RUNNING` error includes a `job_id`, apply the same
rule and resume that job only on a later user turn. Never sleep, back off, poll,
or present incomplete offers as final.
Once complete, empty `offers` = no match — tell the user and offer to change dates/airports.
(Output is also token-capped, so a long result may page: pass the canonical `job_id` and
`next_cursor` as `--cursor` to fetch more, only if the user wants beyond the first page.)
**Show at most 10 flights** in the results table. If the response returns more than 10
offers, present only the first 10 (in returned order) and tell the traveler more are
available on request. Never present more than 10 rows at once.
**On both surfaces, for round-trips, save this search's `job_id`** for the return search in
Step 5.
## Step 4 — show the offers
Turn the JSON into **one Markdown table** — never as prose, a bullet list, a numbered list,
or a plain sentence list. The table is the only acceptable presentation format for flight
results on both CLI and MCP. Keep each offer's `id` out of the table (you need it for
returns/booking). Each `offers[]` item has exactly these keys (don't invent others): `id`,
`airline_name`, `flight_number`, `departure_airport`/`arrival_airport`,
`departure_time`/`arrival_time`, `departure_date`/`arrival_date` (for the `⁺¹` next-day mark),
`duration`, `stops`, `price`, `in_policy`/`policy_reason`, `fare_name` (free-text fare label),
`ancillaries`, and `fare_options` (the per-fare grid — **present only when you passed
`--include_fare_options`**).
| # | Airline | Flight | Fare | Depart → Arrive | Duration / stops | Price (round-trip total) | Policy |
|---|---------|--------|------|-----------------|------------------|--------------------------|--------|
| 1 | JetBlue | B6 0115 | Blue Basic | 6:00 AM → 9:15 AM | 6h 15m / Nonstop | **$289** | ✓ |
- **#** — the row's on-screen position (top = `1`, no gaps), not the JSON index. If you
reorder (e.g. cheapest in-policy first), renumber top to bottom. Keep a private `#`→`id`
map so "book #3" resolves correctly.
- **Depart → Arrive** — local times; add `⁺¹` when arrival is next-day.
- **Fare** — show the selected fare's `fare_name` when present; otherwise `—`. For a
fare-options comparison, use the selected row's fare name, category, and price rather than
the parent offer's cheapest-fare values.
- **Duration / stops** — combine the returned `duration` and `stops` (for example,
`6h 15m / Nonstop`).
- **Ancillaries** — when fare benefits matter to the comparison, summarize each returned
`ancillaries` entry using its `display_name`, `offer_type`, and `price` when present. Show
`INCLUDED`, `CHARGEABLE`, or `NOT_INCLUDED` as returned; a missing category is unknown and
must not be presented as excluded. Apply the same rule to nested fare-option ancillaries.
- **Price** — always show. Round-trip header says **"round-trip total"** (covers both legs);
one-way says **"Price"**. Say which in words.
- **Policy** — when `fare_options` is present, use the applicable nested fare's
`in_policy`/`policy_reason`, never the parent offer's usually-null verdict. For the initial
offer row, use a nested fare only when the top-level offer `id` exactly matches that fare's
`id`; otherwise treat policy as unknown until the traveler selects an exact fare row. Never
match fares by price, amount, currency, or array position. After the traveler chooses a fare,
use that exact fare-option row. Render `true` as **✓**,
`false` as **✗** plus the returned reason, and `null` as **—** (unknown, not out of policy).
When `fare_options` is absent, use the offer-level verdict.
Above the table, lead with the route and travel date, taking the weekday from the offers'
`departure_date` strings (weekday included, e.g. "Mon, Jul 13, 2026" → **"SFO → EWR — Mon,
Jul 13"**). The day you show must come from the API's date strings — never pair the traveler's
words ("Sunday") with a date you computed. Judge a mismatch against the day the traveler
actually named: a departure day against `departure_date`, an arrival day against
`arrival_date` — a Saturday red-eye arriving Sunday **matches** "be home by Sunday". On a real
mismatch, re-search with the corrected date instead of presenting these offers. For an
arrival-day request, show each offer's `departure_date` **and** `arrival_date` as returned so
the traveler sees both days. If `search_policy_summary` has text, show it once as a short banner.
Do not invent recommendation reasons or infer policy from price, cabin, or approval data.
When the response includes `web_search_url`, end the results message with one final markdown link
labeled `See all results` pointing at the exact returned URL. Never rewrite, re-encode, shorten,
or substitute any part of it, and never use another label.
## Step 5 — round-trip: confirm the outbound, then fetch returns
Round-trips only. **Wait for the user to name the outbound.** Don't guess or default to
cheapest/first. Ask *"Which outbound do you want? I'll pull the matching returns once you
pick."* If vague ("the morning one") and more than one fits, confirm the exact flight.
The return step is a **second flight-search call** — same command, with the chosen outbound's
`id` as `--outbound_offer_id` plus the Step 3 `job_id` (carries outbound
context for return-policy). For delegated bookings, also pass the same `--traveler_user_id`.
Don't pass `--departure`/`--arrival`/dates again — mixing them with `--outbound_offer_id` is
rejected.
```bash
ramp travel search-flight --output json \
--outbound_offer_id "" \
--job_id "" \
--include_fare_options --wait_for_results=true \
--rationale "return offers for the chosen outbound, Toronto→SFO trip Jul 1-8"
```
MCP:
```json
{
"outbound_offer_id": "{chosen_outbound_offer_id}",
"job_id": "{job_id_from_step_3}",
"cabin_class": "ECONOMY",
"include_fare_options": true,
"wait_for_results": true,
"rationale": "return offers for the chosen outbound, Toronto→SFO trip Jul 1-8"
}
```
Call `SearchFlights` with the above.
**Return-offer reads are always synchronous, on both CLI and MCP** — unlike the outbound search,
this call rejects `wait_for_results=false`; always pass `true` and use the response directly.
The response is `is_round_trip: true` with `offers` being the return legs. (Return mode is
synchronous, so its `job_id` is null; page more returns by reusing `--outbound_offer_id` +
`--cursor`.) Show them like Step 4, including the **max 10 rows** cap. **Each return offer's
price is the full round-trip total** — say so (*"the nonstop keeps your trip at $289; the
1-stop return makes it $396 total"*). For policy, use the applicable nested fare verdict as
in Step 4; only omit the Policy column when that applicable verdict is unavailable. The id
you carry to booking is the chosen **return** offer's `id`.
## Step 6 — book (preview → confirm → verify)
Always three steps; never book in one shot, never assume a yes, never book a flight the
traveler didn't name. Booking spends **real money**.
Before previewing or confirming a booking, check whether the traveler already has a Ramp
travel profile:
```bash
ramp travel profile --output json \
--rationale "check whether the Toronto→SFO Jul 1-8 trip traveler profile is ready for booking"
```
MCP:
```json
{
"rationale": "check whether the Toronto→SFO Jul 1-8 trip traveler profile is ready for booking"
}
```
Call `GetTravelerProfile` with the above.
If `has_profile` is `false`, collect the required traveler details in one message, then update
the profile before continuing. Use the tool to save the details the traveler gives you; do not
send them to the Ramp web app for this.
For delegated bookings, pass the same `traveler_user_id` from the user lookup flow to
`travel profile` / `GetTravelerProfile` and, if needed, `travel profile-update` /
`UpdateTravelerProfile`. For self-booking, omit `traveler_user_id`.
```bash
ramp travel profile-update --output json \
--first_name "Taylor" \
--last_name "Smith" \
--date_of_birth "1990-01-15" \
--email "taylor@example.com" \
--phone_number "+14155550123" \
--id_gender MALE \
--rationale "create the traveler profile needed to book the Toronto→SFO Jul 1-8 trip"
```
MCP:
```json
{
"first_name": "Taylor",
"last_name": "Smith",
"date_of_birth": "1990-01-15",
"email": "taylor@example.com",
"phone_number": "+14155550123",
"id_gender": "MALE",
"rationale": "create the traveler profile needed to book the Toronto→SFO Jul 1-8 trip"
}
```
Call `UpdateTravelerProfile` with the above. `id_gender` is required (`MALE` or `FEMALE`); the
profile update fails without it.
Only continue when the update succeeds. Confirm the profile-update result reports success, or
re-run `travel profile` to verify the traveler now has a profile before moving on. For delegated
bookings, re-run it with the same `traveler_user_id`. If the update fails, correct the missing
details and retry instead of continuing to booking.
Once the profile exists, continue to the normal preview, confirmation, and verification flow.
**Which id:** one-way → the chosen offer's `id` from `search-flight` / `SearchFlights`;
round-trip → the chosen **return** offer's `id` from Step 5 (it represents the whole
round-trip and its both-legs total — **not** the outbound id). Pass it as the first arg
(`ramp travel book ""` on CLI). Behind it is **`flight_offer_uuid`**, so a `--json` body
or MCP `SubmitFlightBooking` call uses key `flight_offer_uuid`, not `offer_id`.
### Phase 1 — preview (always first)
Run `book` **without `--confirm`** — that returns the preview and books nothing.
For delegated bookings, pass the same `--traveler_user_id` used for profile preflight/search.
```bash
ramp travel book "" --output json \
--rationale "preview fare for the Toronto→SFO Jul 1 trip before the traveler confirms"
```
MCP:
```json
{
"flight_offer_uuid": "{flight_offer_uuid}",
"confirm": false,
"rationale": "preview fare for the Toronto→SFO Jul 1 trip before the traveler confirms"
}
```
Call `SubmitFlightBooking` with the above.
Show plainly: traveler (`traveler_name_display` when present), route/dates, airline/flight,
cabin/fare (`itinerary.fare_name` when present), payment timing (`payment_display` when
present), **total**, policy result, fare details, and the paying fund. If `loyalty_programs`
is present, show each matching program's `display_name` and only the last four characters of
its `loyalty_number`; do not expose its logo URL or full loyalty number. A preview without
`spend_allocation_id` auto-uses `recommended_fund_uuid` when an eligible fund is available.
Label a fund ` (recommended)` only when the tool auto-populated that
recommendation; a user-selected fund never gets that label, even when its UUID matches.
**Fare details:** when the preview returns `ancillaries_by_slice` (or equivalent ancillary
data), present a compact breakdown using each ancillary's `display_name`, `offer_type`, and
`price` when present. Group them as:
- **Included:** ancillaries with `offer_type=INCLUDED` — show `display_name` only.
- **Costs extra:** ancillaries with `offer_type=CHARGEABLE` — show `display_name` and `price`.
- **Not included:** ancillaries with `offer_type=NOT_INCLUDED` — show `display_name` only.
Omit each group when it has no entries. Do not infer ancillary terms that the preview did not
return.
The preview returns `eligible_funds`, `fund_eligibility_status`, and `selected_fund_uuid` when a
fund was explicitly passed; a non-null `selected_fund_uuid` is the booking fund. If
`fund_eligibility_status=lookup_failed`, do not confirm; repeat
the preview to resolve funding. If it is `none_eligible`, the valid path is to request new funds.
If the traveler chooses or changes to an eligible fund, call preview again with `confirm=false`
and that fund's `fund_uuid` as `spend_allocation_id`; present the refreshed preview and wait for
a separate explicit confirmation turn.
Use `approval_display_status` verbatim for approval messaging. Do not infer the wording from
`requires_approval` or `approval_steps` alone.
If `loyalty_program_names_to_offer` is non-empty and this is a self-booking, ask whether to save
one of the returned programs and stop for the answer before asking for booking confirmation. Save
only the exact returned program name with the membership number the traveler provides. For a
delegated booking, do not offer or attempt to save loyalty; the save action targets the requester,
not the selected traveler. If the traveler saves a program, run a fresh preview and require fresh
confirmation.
The preview returns the itinerary dates as weekday-qualified strings — **`outbound_date`**
(e.g. "Mon, Jul 13, 2026") and, for round-trips, **`return_date`**. The read-back must quote
them **exactly as returned, weekday included** — never re-derive the weekday or repeat one
from earlier conversation. These are departure dates — check them against a departure day the
traveler named; for an arrival day ("be home by Sunday"), check the chosen offer's
`arrival_date` instead and read that day back too (a Saturday-departing red-eye arriving
Sunday is correct). On a real mismatch, **stop and re-search (Step 3) with the corrected
date** — don't ask for confirmation. If the preview doesn't include these fields, verify each
ISO travel date with Python's calendar instead (use `python` if only that executable is
available):
```bash
python3 -c 'from datetime import date; import sys; dates = map(date.fromisoformat, sys.argv[1:]); print("\n".join("{}: {} {}, {}".format(d.isoformat(), d.strftime("%A, %B"), d.day, d.year) for d in dates))' 2026-07-06 2026-07-10
```
Then state the total in plain words and **ask for a clear yes**. The confirmation prompt must
include the preview's date string for every leg, along with the local departure time: *"This
books LHR → JFK on Delta, departing Mon, Jul 6, 2026 at 10:00 AM, for **$412 total**, paid
from the Travel fund. Book it?"* For a round-trip, include both outbound and return dates and
times. Stop and wait.
### Phase 2 — confirm (only after a clear "yes")
Add `--confirm` and pass the preview's numeric `expected_total_amount` verbatim as
`--expected_total_amount` (with no currency symbol). This rejects the booking if the fare moved
instead of quietly charging more. Do not copy the display-formatted `total_amount`.
For delegated bookings, pass the same `--traveler_user_id` used in the preview.
```bash
ramp travel book "" --confirm \
--expected_total_amount --output json \
--rationale "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare"
```
MCP:
```json
{
"flight_offer_uuid": "{flight_offer_uuid}",
"confirm": true,
"expected_total_amount": "{preview_expected_total_amount}",
"spend_allocation_id": "{fund_uuid_from_latest_preview}",
"rationale": "book the Toronto→SFO Jul 1 trip; traveler approved the previewed fare"
}
```
Call `SubmitFlightBooking` with the above. Include exactly one funding path: `spend_allocation_id`
(the exact fund from the latest preview, including the recommended UUID when the preview
auto-populated it) or `request_new_fund: true` (with `reason` as the trip purpose). Never omit
both and never pass both.
Extra flags, only when they apply:
- **`--spend_allocation_id `** — use the exact fund from the latest preview, including
the recommended UUID when that preview auto-populated it.
- **`--request_new_fund=true`** plus **`--reason ""`** — use only when the latest
preview showed the new-fund path. `reason` is the trip purpose shown to approvers, not a
generic booking note.
- **`--oop_reason ""`** — required for an out-of-policy quote.
- **`--trip_id `** — attach to an existing trip; off to auto-pick/create.
Confirmation requires exactly one funding path: `spend_allocation_id` or
`request_new_fund=true`. Never omit both and never pass both. Preserve the exact funding path
from the latest preview.
If `confirm=true` fails for **any** reason, stop. Relay the error `message`, follow
`agent_guidance`, and never retry, tweak parameters, switch offer/fare, or confirm again without
a fresh preview and a fresh explicit confirmation. A price-change error therefore requires a
new preview and new approval; it is not permission to retry the confirmation.
### Phase 3 — verify it went through
**The confirm response is optimistic, not final** — it can say `approved`/`pending_approval`
and still fail in fulfillment. Don't say "you're booked" off the confirm alone:
On a successful confirmation, retain the exact `booking.booking_request_id` from the response.
Use it to select the matching entry from `travel bookings`; never select an older entry by route,
flight number, or timestamp.
```bash
ramp travel bookings --include_flights --output json \
--rationale "verify the Toronto→SFO Jul 1 booking reached a terminal status"
```
MCP:
```json
{
"include_flights": true,
"rationale": "verify the Toronto→SFO Jul 1 booking reached a terminal status"
}
```
Call `GetBookings` with the above.
For delegated bookings, pass the same `--traveler_user_id` when verifying and on every retry;
otherwise `travel bookings` checks the requester's bookings.
Each `travel bookings` entry has a generic `id` and a `booking_request_id`. Match the retained
confirmation `booking_request_id` exactly, then use that matching entry's generic `id` with
`travel booking-details` for detailed status questions. Do not fuzzy-match by route, flight number,
or `booked_at`. If the matching request is missing from the default result, retry once with
`--include_failed`; if it is still missing, report that verification could not locate the submitted
request rather than using another entry. Report the matching entry's `status`. Cancelled, rejected,
and failed requests do not block rebooking. Most read
for themselves (`CONFIRMED`, `PENDING_APPROVAL`, `CANCELLED`). Two need care:
- **`PROCESSING`** is **not final** — report that fulfillment is still processing; do not report
it as booked yet.
- **`FAILED`** — show `error_message` exactly. If it points to missing traveler details, use
`travel profile` / `GetTravelerProfile` and `travel profile-update` /
`UpdateTravelerProfile` with the same traveler target to complete the profile before a new
booking attempt.
`travel booking-details` (CLI) / `GetBookingDetails` (MCP) is flag-gated by
`OMNI_TRAVEL_BOOKING_SUPPORT_SKILL_ENABLED`. If it is not available, degrade gracefully with
the information from `travel bookings` / `GetBookings` rather than claiming the detailed
lookup succeeded. When available, relay `request_status`, `current_total_amount`,
`error_message`, and `approval.pending_approval_summary` verbatim when approval is pending.
## Cabin and fare options
Every search response already returns the fare grid because `include_fare_options=true` is
always on. The cabin answer — collected before searching on CLI, or applied to the running job
on MCP (see Step 3) — determines which cabins are policy-evaluated. If the traveler asks to
broaden or change cabins, re-read the existing `job_id` with the new `cabin_class` and
`include_fare_options=true`; do not start a new route search unless the route, dates, trip, or
traveler changed.
`include_fare_options: true` adds each flight's bookable fare classes to `fare_options` (each
with `fare_name`, `fare_category` — `Basic Economy`/`Economy`/`Economy Plus`/`Premium`/
`Business`/`First` — `price`, `in_policy`/`policy_reason`, and a bookable fare `id`).
The offer's top-level `price` is the **cheapest** fare; `fare_options` lists the upgrades. To
book a specific fare, pass that fare option's `id`, **not** the offer's top-level `id`.
Send the always-on search fields via the command flags when available, otherwise via the `--json`
body:
```bash
ramp travel search-flight --output json --json '{
"departure": "JFK", "arrival": "SFO",
"departure_date": "2026-07-06", "return_date": "2026-07-10",
"cabin_class": "ECONOMY", "wait_for_results": true,
"include_fare_options": true,
"rationale": "compare cabin/fare classes for the JFK→SFO trip, Jul 6-10"
}'
```
Here cabin is already known because the traveler explicitly asked for a cabin/fare comparison,
so MCP callers can include `cabin_class` on this fresh search too; still set
`wait_for_results=false` and poll as in Step 3:
```json
{
"departure": "JFK",
"arrival": "SFO",
"departure_date": "2026-07-06",
"return_date": "2026-07-10",
"cabin_class": "ECONOMY",
"include_fare_options": true,
"wait_for_results": false,
"rationale": "compare cabin/fare classes for the JFK→SFO trip, Jul 6-10"
}
```
Call `SearchFlights` with the above.
Re-send `include_fare_options` and the active `cabin_class` on every follow-up call (pagination,
the cabin refinement, and the Step 5 return search); these settings do not persist on their own.
MCP callers keep using `wait_for_results=false` and poll on outbound follow-up calls, but the
Step 5 return search is always synchronous — pass `wait_for_results=true` there instead.
Present a **single cabin-grid Markdown table** — one row per flight, one column per cabin
category — ordered by departure time (a comparison, not a ranked list). Never present this
as a bullet list or prose:
| # | Depart → Arrive | Airline / Flight | Basic Econ | Economy | Econ+ | Premium | Business |
|---|-----------------|------------------|-----------|---------|-------|---------|----------|
| 1 | 7:00 AM → 9:49 AM | DL 0742 | **$154** | **$209** | $354 | $759 | $3,849 |
- **#** — display position (top = `1`); keep an internal `#`→per-cabin-fare-`id` map so
"book #2 in economy" resolves to the right fare option `id`.
- **Depart → Arrive** — local times, `⁺¹` for next-day.
- One column per `fare_category`; cell = that fare's `price`, `—` if the flight doesn't sell it.
- **Bold = in policy** (`in_policy: true`); plain otherwise; render `null` plain (don't claim
out-of-policy). Add a one-line legend.
Above the table, give the lead line (cheapest fare anywhere + the highest in-policy cabin) and
make the upgrade math explicit (*"Basic Economy $289, in policy; Economy $399; Business
$1,101, out of policy"*).
## Supporting tools (profile, trips, bookings)
Five supporting tools; use when relevant, not on every booking.
- **`travel profile`** (CLI) / `GetTravelerProfile` (MCP) — the traveler's saved profile (name,
email, phone, DOB, gender, KTN/TSA, redress, loyalty). Use for "what's my known traveler
number?" and before booking to check whether `has_profile` is true.
- **`travel profile-update`** (CLI) / `UpdateTravelerProfile` (MCP) — saves missing traveler
details before booking when `travel profile` / `GetTravelerProfile` returns `has_profile:
false`, or when a failed booking points to missing traveler details.
- **`travel list`** (CLI) / `GetUserTrips` (MCP) — the traveler's trips
(`--status completed|ongoing|upcoming`, `--cursor` / `status`, `cursor`). Each has `id`,
`trip_name`, dates, locations. Use to find a trip `id` for `--trip_id` / `trip_id` on `book` /
`SubmitFlightBooking`. Note: delegated trip lookup is not exposed on MCP; `GetUserTrips`
returns the caller's own trips only.
- **`travel bookings`** (CLI) / `GetBookings` (MCP) — existing flight/hotel bookings
(`--include_flights`/`--include_hotels`, `--limit` / `include_flights`, `include_hotels`,
`limit`). Each has a `status` (`CONFIRMED`/`PENDING_APPROVAL`/`FAILED` + `error_message`),
route/times, `trip_name`/`trip_id`. **Source of truth for whether a booking succeeded**
(Phase 3) and for "what flights do I have booked?".
- **`travel booking-details`** (CLI) / `GetBookingDetails` (MCP) — detailed status for one
exact `travel bookings` / `GetBookings` entry ID when the booking support capability is
available. Use it for `request_status`, `current_total_amount`, `error_message`, and
approval details; relay a pending `pending_approval_summary` verbatim. If unavailable,
degrade gracefully to `travel bookings` / `GetBookings`.
## Cancelling a flight booking
Cancelling forfeits or spends real money, so it follows the same preview → explicit yes →
confirm discipline as booking. The command is `ramp travel cancel-flight` (CLI) /
`SubmitFlightCancellation` (MCP); it is enabled per business, so it may be absent for some
accounts (see "If cancellation is unavailable").
### Identify the exact booking first
Never guess which booking to cancel. Resolve it from `ramp travel bookings --include_flights
--output json` (same `--traveler_user_id` for delegated travelers) and use the exact entry
`id` — the same exact ID the skill already uses with `travel booking-details`. If more than
one booking could match ("cancel my SFO flight"), show the likely matches and ask which one;
never pick by route, date, or recency on your own. If the traveler pasted an ID that this
conversation's `travel bookings` never returned, look the booking up first instead of
trusting the pasted value.
Cancellation applies to a fulfilled booking. For an unfulfilled request (e.g.
`PENDING_APPROVAL`), the entry `id` is the request UUID, which this command will not find —
direct the traveler to the request in the Ramp web app instead.
### Preview the terms (read-only)
Always call without `--confirm` first. This books/cancels nothing and returns the authoritative
terms plus a `preview_id`:
```bash
ramp travel cancel-flight --booking_id "" --output json \
--rationale "preview cancellation terms for the Toronto→SFO Jul 1 booking"
```
MCP:
```json
{
"booking_id": "{booking_id}",
"confirm": false,
"rationale": "preview cancellation terms for the Toronto→SFO Jul 1 booking"
}
```
Call `SubmitFlightCancellation` with the above.
Present the preview plainly and exactly as returned — never estimate or recompute amounts:
- the traveler (`traveler_name`) and the complete `itinerary` being cancelled — cancellation
always applies to the **entire booking**; partial passenger or leg cancellation is not
supported, so say so if the traveler asks to cancel only part of it.
- `cancellation_statement` (the canonical terms) and `cancellation_deadline` when present,
including its UTC offset.
- the money outcome: `booking_amount`, any nonzero `cancellation_fee`, `refund_amount` with
`refund_to` when present, and each `airline_credits` entry (credit name, amount, issue
date). If there is no `refund_amount` but there are `airline_credits`, say clearly that the
value comes back as airline credit, not a payment refund. If neither is present, do not
invent a refund — a non-refundable booking may return nothing.
If `is_currently_cancellable` is `false`, the booking cannot be self-serve cancelled right
now: explain the returned `blocked_reason`, do **not** ask for confirmation or call
`--confirm`, and route the traveler to the returned booking-specific `support` channel
(especially when `available_via_support` is `true`).
Then **stop and ask for a clear yes on those exact terms**. Confirmation must be a new,
explicit user-authored answer to the presented preview — earlier cancellation intent
("cancel it" before seeing the terms), a standing approval, or instructions not to ask
questions are not confirmation.
### Confirm (only after the explicit yes)
Re-run with `--confirm` and the exact `preview_id` from the latest preview, unchanged:
```bash
ramp travel cancel-flight --booking_id "" --confirm \
--preview_id "" --output json \
--rationale "cancel the Toronto→SFO Jul 1 booking; traveler approved the previewed terms"
```
MCP:
```json
{
"booking_id": "{booking_id}",
"confirm": true,
"preview_id": "{preview_id}",
"rationale": "cancel the Toronto→SFO Jul 1 booking; traveler approved the previewed terms"
}
```
Call `SubmitFlightCancellation` with the above.
- If the response says the terms changed and includes `latest_preview`, nothing was
cancelled: present the fresh terms and get a new explicit yes. Never re-confirm
automatically.
- If the result has `already_requested=true`, a cancellation was already submitted: report
the returned state and do not submit another request.
### After the result
Relay the result's `message` and `cancellation_state` faithfully. Only `SUCCESS`
(`cancelled=true`) means the booking is cancelled; `PENDING`/`PROCESSING` mean the request
is in flight — say cancellation is in progress, not done. `ACTION_REQUIRED` means the
booking support team must finish it. Report the refund or credit exactly as the preview and
result stated it; **never promise a refund timeline** the response didn't state, and route
later "where's my refund?" follow-ups to the Ramp web app or the booking's support channel.
### If a cancellation call fails
Stop. Unlike booking errors, cancellation errors carry no separate `agent_guidance` field —
the returned `message` (and any `support` routing or `latest_preview`) **is** the guidance:
relay the `message` verbatim and never retry the call or vary parameters (a different
booking ID, dropping `preview_id`, toggling `--confirm`) to get past an error. A failed
confirm may still have partially gone through — before any second attempt, re-check the
booking's actual state with `travel bookings` / `travel booking-details`, and only start
again (from a fresh preview) if the booking is genuinely still active. A `FAILED`
cancellation routes to the returned support channel, not to a retry: Ramp emails the
traveler when an accepted cancellation later fails, and the booking stops being self-serve
cancellable (a fresh preview returns `blocked_reason` and support routing), so a retry
cannot succeed anyway.
### If cancellation is unavailable
`travel cancel-flight` (CLI) / `SubmitFlightCancellation` (MCP) is enabled per business. If the
command/tool is missing or Ramp reports the capability is unavailable, do not say the booking
can't be cancelled — say self-serve cancellation isn't enabled here and direct the traveler to
the booking in the Ramp web app or the booking's support channel. Cancellation also only covers bookings this flow could have
made: an unsupported provider (e.g. a Priceline-fulfilled flight) or a guest booking returns
an error with support routing — relay it and point the traveler there.
Changes, rebooking, and seat or date modifications are **not** cancellations and stay outside
this skill — send the traveler to the Ramp web app or the booking's support channel for those.
## Gotchas
- Offers expire from the cache. If a return or book call fails with a cache/offer error,
re-run the search (Step 3) for fresh offers and continue.
---
URL: https://agents.ramp.com/skills/ramp-book-hotel
---
name: ramp-book-hotel
area: Travel
supported_surfaces: [cli, mcp]
description: "Searches and books hotels conversationally: resolves the traveler, searches paginated hotel inventory, compares the selected room-rate returned for each hotel, previews the selected rate, and books only after explicit approval. Also cancels an existing hotel booking with a preview-then-confirm flow when the cancellation capability is enabled. Use when someone wants to find, compare, or book a hotel or lodging, or wants to cancel a hotel they booked. Not for flight booking, stay changes, refund-status follow-ups, or car rentals."
---
# Book a Hotel
The user describes a stay in plain words. Turn that into `ramp travel` commands (CLI only) or
MCP tool calls, run them, and show clean results. Never show or ask the user to type a CLI
command or tool name; talk like a travel helper (`Searching hotels near Lower Manhattan,
Aug 10-13...`), not about tools or flags.
The steps, phases, checklists, field names, and command names in this guide are your internal
scaffolding; never surface them to the user. Reason through them silently. Never mention this
skill, its instructions or specification, whether you are following or complying with it, or how
an internal tool name maps to a CLI command. Do not narrate command-name or alias reconciliation.
Give only concise, user-relevant action and result updates.
## Scope and safety
- Use `ramp travel search-hotel` (CLI) / `SearchHotels` (MCP) to search,
`ramp travel hotel-rates` (CLI) / `GetHotelRates` (MCP) to fetch the selected hotel's rates,
and `ramp travel book-hotel` (CLI) / `SubmitHotelBooking` (MCP) to preview and book.
- Always use `--output json` (CLI); MCP callers receive structured JSON directly. Build a
readable comparison instead of showing raw JSON.
- Always include a rationale that consistently names the destination and stay dates.
- **Both surfaces present results as Markdown tables** — never as UI components, cards,
interactive widgets, bullet lists, numbered lists, or prose. A Markdown table is the only
acceptable presentation format for hotel comparisons, room/rate lists, fund displays, and
the booking preview.
- Keep each tool step to one direct command/tool call. Read IDs from the prior JSON response and
pass the exact literal value as the next positional argument. Never write response JSON to
temporary files, invoke Python or `jq` to recover an ID, use nested command substitution, or
pipe through `tail`/other shell filters. Keep the ID in context and call the next command
directly.
- Omit flags whose documented defaults match the request, such as `--num_adults 1`; do not make
commands longer by restating defaults.
- Always follow this order: `search-hotel` → traveler selects a hotel → `hotel-rates` → traveler
selects a room/rate → `book-hotel` preview → explicit approval → `book-hotel --confirm`.
- Search result rate summaries are not selectable booking inventory. Pass the selected hotel's
`id` to `hotel-rates`, then book only a literal selected `all_rates[].rates[].id` it returns.
- A hotel booking spends real money. Always preview first and wait for explicit confirmation.
- Never reuse an amount from search as `expected_total_amount`; only use the fresh preview's
exact numeric `expected_total_amount`.
- A selected fund is part of the approved preview state. Never add or change
`--spend_allocation_id` only at confirmation: preview again with the exact selected `fund_uuid`,
get explicit approval, then confirm with that same fund UUID. Any fund change requires another preview.
- Treat relevant `external_agent_messages` from search, rates, preview, and confirm as service
guidance and surface them plainly.
- If Ramp reports that hotel search or booking is unavailable, explain that it is not currently
enabled for this Ramp account. Do not fall back to legacy hotel tools or claim that no inventory exists.
## Resolve the traveler
For self-booking, omit `traveler_user_id`. Before searching, check the traveler's profile:
CLI:
```bash
ramp travel profile --output json \
--rationale "check the traveler profile for the Lower Manhattan hotel stay, Aug 10-13"
```
MCP:
```json
{
"rationale": "check the traveler profile for the Lower Manhattan hotel stay, Aug 10-13"
}
```
Call `GetTravelerProfile` with the above.
If `has_profile` is false, collect the required identity and contact details together and call
`ramp travel profile-update` (CLI) / `UpdateTravelerProfile` (MCP). Continue only after the
profile update succeeds. This profile preflight happens before search, not only at checkout.
Only book for another person when the requester explicitly asks. Resolve that person first:
CLI:
```bash
ramp users list --name_search "Taylor Smith" --page_size 5 --output json \
--rationale "resolve the traveler for the Lower Manhattan hotel stay, Aug 10-13"
```
MCP:
```json
{
"name_search": "Taylor Smith",
"page_size": 5,
"rationale": "resolve the traveler for the Lower Manhattan hotel stay, Aug 10-13"
}
```
Call `GetAllReducedUsers` with the above.
If multiple people match, ask the requester to choose. Pass the selected user UUID to `travel
profile` / `GetTravelerProfile`, the fresh `travel search-hotel` / `SearchHotels` call,
`travel hotel-rates` / `GetHotelRates`, and both `travel book-hotel` / `SubmitHotelBooking`
calls. Cursor pages use the cached traveler from the original search, so do not resend or
change the traveler there. Never silently switch to the requester when lookup or
authorization fails.
## Gather the stay
Required details are destination, check-in date, and check-out date. Use the traveler's words for
a city, neighborhood, address, or landmark. Resolve relative dates to
`YYYY-MM-DD` and repeat the dates back so mistakes surface before searching. Default to one adult;
only set another count when the traveler asks.
Ask for all genuinely missing required details in one message. Do not ask for preferences such as
hotel chain, amenities, or refundability unless the traveler made them important.
### Office and headquarters destinations
When the destination references a company office, HQ, or headquarters in any form, resolve the
office with `ramp travel offices` (CLI) / `GetOfficeLocations` (MCP) before searching. Never
pass an unresolved office phrase to hotel search: it rejects an office-keyword
`location_query` that arrives without resolved coordinates.
CLI:
```bash
ramp travel offices --output json \
--rationale "resolve the company office anchor for the hotel stay, Aug 10-13"
```
MCP:
```json
{
"rationale": "resolve the company office anchor for the hotel stay, Aug 10-13"
}
```
Call `GetOfficeLocations` with the above.
Each `office_locations[]` entry carries `display_name`, `latitude`, and `longitude`. Match the requested
city or office name against this shape. If more than one office could match, ask the traveler to choose.
Do not try to infer an office. Once a coordinate-bearing `office_locations` entry is chosen, pass its
`display_name` as `--location_query` together with its exact `--latitude` and `--longitude` to
`ramp travel search-hotel` (CLI) / `SearchHotels` tool (MCP). Never expose the coordinates to the traveler.
`display_name` is nullable: when the chosen office has none, keep the traveler's own office phrase as
`--location_query` while still passing the exact coordinates. Refer to offices by display name — or by the
traveler's phrase when the office is unnamed.
If only `company_address` matches and no coordinate-bearing office entry is available, ask the traveler for
a specific neighborhood, landmark, or address instead. When the traveler gives only a bare city and the
company has an office there, offer the office as an anchor option instead of silently adopting it.
## Search hotels
Use `--location_query` (CLI) / `location_query` (MCP) for the destination context (city, neighborhood,
landmark, or address). When the traveler asks for a specific hotel or property, pass its exact name
with `--hotel_name` (CLI) / `hotel_name` (MCP).
```bash
ramp travel search-hotel --output json \
--location_query "Lower Manhattan" --hotel_name "citizenM New York Bowery" \
--wait_for_results=true \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "search for citizenM New York Bowery for the Manhattan stay, Aug 10-13"
```
MCP:
```json
{
"location_query": "Lower Manhattan",
"hotel_name": "citizenM New York Bowery",
"wait_for_results": true,
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"rationale": "search for citizenM New York Bowery for the Manhattan stay, Aug 10-13"
}
```
Call `SearchHotels` with the above when searching for a specific hotel or property.
Pass `wait_for_results=true` explicitly on **every fresh** hotel search, on both CLI
and MCP. Both block synchronously until results are ready.
Add `--traveler_user_id` only for delegated booking. Add `--num_adults` only when it differs from
one. The default page is ten hotels; `--limit` can request 1-10.
When the traveler explicitly requests supported filtering or ordering, send one complete `--json`
request because `filters` and `sort` are structured rather than standalone flags. Supported
filters are `star_ratings`, `brand_ids`, `amenity_ids`, and `min_review_rating`. Supported sort
keys are `DISTANCE`, `LOWEST_PRICE`, `TIME`, `WEIGHTED`, `MOST_POPULAR`, and `STAR_RATING`, with
`ASC` or `DESC`. Do not invent brand or amenity IDs; omit criteria that cannot be represented with
known literal values. Preserve destination, dates, traveler, adult count, limit, hotel name, and
rationale in the JSON body when applicable; omit adult count and limit when their defaults match.
A completed synchronous search returns recommendations, not a plain list: `recommended` holds
`best_match` plus `alternates` in ranked order — up to ten hotels total on this surface, capped by
`--limit` when it is smaller — and `hotels` is empty. Each recommendation is `{hotel, reasons,
tradeoffs}`; read the hotel metadata from its `hotel` object. The rest of the ranked inventory
stays cached behind `next_cursor`, and `total_count` still counts the full inventory. Present
every returned recommendation in text; never call `hotel-rates` just to render options for hotels
the traveler has not selected. The response's `assistant_note` is a presentation reminder
addressed to you — follow it and never show it to the traveler.
`next_cursor` is opaque. If the traveler wants hotels beyond the recommendations, call search with
that value unchanged as `--cursor` (CLI) / `cursor` (MCP) and a rationale; omit the original search
fields because Ramp reads the cached result. For delegated bookings, re-pass the same
`--traveler_user_id` (CLI) / `traveler_user_id` (MCP) on every cursor call so the page is
reauthorized against the delegated traveler's current access. Preserve a non-default `--limit` when
consistent page size matters. Cursor calls on both CLI and MCP continue to use
`wait_for_results=true`. Append the new hotels; never re-run the search for more results and never
inspect, edit, synthesize, or reuse a cursor with a different search.
Use `total_count` and the number displayed so the traveler knows when more results are available.
No matching inventory means `recommended` is absent and `hotels` is empty. Offer to adjust the
location or dates. Surface `policy_summary` before the table when present; it is Ramp's
authoritative summary of what this traveler may book.
## Present the hotel comparison
When `applied_preferences_summary` is present, lead with it once as a short sentence so the
traveler knows how preferences shaped the ranking. Then render the recommended hotels as
**one Markdown comparison table** — never as a bullet list, numbered list, prose, or a plain
sentence list. `recommended.best_match` first, then each `alternates` entry in returned
order. Search follows the Ramp web flow and returns zero or one selected/best room-rate for
each hotel inside its `hotel.rates`. It does **not** return every available room or rate. The
full column order is:
| # | Hotel | Rating | Chain | Nightly (pre-tax) | All-in total | Policy | Loyalty program | Notes |
|---|-------|--------|-------|---------------------|--------------|--------|-----------------|-------|
| 1 | Four Seasons Chicago | 5-star | Four Seasons | $245 USD | $812 USD | In policy | Four Seasons Preferred Partner | Matches gym preference; 12 min walk; Company preferred |
Populate it only from returned metadata:
- Always show `#`, `Hotel`, `Rating`, `Nightly (pre-tax)`, `All-in total`, and `Policy`.
- `Chain`, `Loyalty program`, and `Notes` are optional columns. Omit an optional column completely
when every row on the displayed page would be `-`. If at least one row has a meaningful value,
keep the column in the order above and use `-` for individual rows without a value.
- **Rating:** use only `star_rating`, formatted as `4-star`, `5-star`, etc. Never show
`review_rating` values such as `9.4`.
- **Chain:** show a real `chain` name such as `Four Seasons`. Render missing, blank, or placeholder
values such as `Default Chain` as `-`.
- **Nightly (pre-tax):** show `nightly_amount` exactly as returned with `currency`. It is the base
nightly amount before taxes and fees, matching web's `/night` price.
- **All-in total:** show `total_amount` exactly as returned. It is the full-stay total including
taxes and fees; never derive it by multiplying the nightly amount or add taxes/fees yourself.
Show `currency`.
- **Policy:** keep it concise: `In policy`, `Out of policy: `, or `-` when no
verdict is available. Trust the returned policy verdict and violations; do not infer policy from
or reverse-engineer it from displayed prices. Use `policy_summary` for broader policy context.
- **Loyalty program:** show `loyalty_program` when present; otherwise show `-`. Do not replace a
missing program with generic eligibility or points-earning prose.
- **Notes:** start with the recommendation's `reasons`, condensed to a few words each, and any
material `tradeoffs` (a policy caveat or missed preference the traveler should weigh); then
combine `office_travel_time_minutes` with `office_travel_mode` (for example,
`12 min walk`), `coworker_booking_count` when nonzero, and `Company preferred` when
`is_company_preferred=true`, separated by `; `. If the combined cell gets long, move the
reasons/tradeoffs to a compact one-line note under the row instead. Show `-` when none is
present. Do not invent reasons beyond the returned ones and do not call travel time a distance.
Use hotel `address` to disambiguate similar properties; mention relevant `hotel_amenities` only
when they match a stated preference.
- If `rates=[]`, show `-` for price, policy, and loyalty cells and note `No current summary rate`.
Never index the first rate unless it exists; the traveler can still select the hotel and fetch
complete rates.
- Keep a private display-number-to-hotel-`id` map. Never print hotel IDs unless asked.
- Do not show a second room/rate table during this initial comparison. Do not show `room_name`,
`refundability`, or `cancellation_policy` yet; reserve them for the selected-hotel details.
When the response includes `web_search_url`, end the results message with one final markdown link
labeled `See all results` pointing at the exact returned URL. Never rewrite, re-encode, shorten,
or substitute any part of it, and never use another label.
Wait for the traveler to select a hotel option. If their choice is ambiguous, confirm the hotel,
nightly amount, and all-in total. Do not preview or book the search result's summary rate.
## Fetch and present selected-hotel rates
Call `hotel-rates` with the literal hotel `id` from search and the same dates, adult count, and
traveler target:
CLI:
```bash
ramp travel hotel-rates "" --output json \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "fetch current rooms and rates for the selected Chicago hotel, Aug 10-13"
```
MCP:
```json
{
"hotel_id": "{selected_hotel_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"rationale": "fetch current rooms and rates for the selected Chicago hotel, Aug 10-13"
}
```
Call `GetHotelRates` with the above.
For delegated booking, pass the same `--traveler_user_id`. Keep `--num_adults` consistent with
search.
The response returns `recommended_rates` — up to three room-rate groups with full rate payloads —
alongside the complete `all_rates` inventory. Lead with these recommended options: name each
group's room and rate and quote its `recommendation_reason` when populated. Reasons are generated
fail-open and may be null; a missing reason is not a signal, so present the rate without inventing
one. `recommendation_reason` appears only on rates inside `recommended_rates` (including each
group's `best_rate`) and is always null on `all_rates` rows. A nonempty `unmet_preferences` on a
recommended group marks a best-available fallback; tell the traveler which requested preferences
it does not meet. When every rate is out of policy, `recommended_rates` still holds the fallback
picks with their policy-violation reasons.
Then keep the full inventory visible: render one Markdown table row for every returned
`all_rates[].rates[]` option — never as a bullet list or prose:
| # | Room | Nightly (pre-tax) | All-in total | Payment | Refundability | Cancellation | Policy | Loyalty | Notes |
|---|------|---------------------|----------------|--------------|---------|---------------|--------------|--------|---------|-------|
| 1 | Deluxe King | $245 USD | $812 USD | Pay later | Refundable | Free until Aug 8 | In policy | Four Seasons | Recommended; Company preferred |
- Use the room group's `room_name`, `room_description`, and `room_amenities`. Mark a group as
recommended only when it also appears in `recommended_rates`; do not infer
recommendations from list order or `best_rate`. `best_rate` is the lowest-total option within
that room group, not a global recommendation.
- Show each rate's `nightly_amount`, `total_amount`, `payment_type`,
`refundability`, and `cancellation_policy` when present. Show amount strings exactly as returned
and include the separate `currency` once; do not append it when the amount already includes it.
- Keep policy concise in the table and surface full `policy_violations` when the traveler compares
or selects an out-of-policy rate.
- For loyalty, show `loyalty_program` and `earns_loyalty_points` when relevant. Clearly flag
`loyalty_required=true` and stop before preview when `loyalty_eligible=false` until the
membership is corrected.
- Put `is_company_preferred`, `is_corporate_rate`, and useful room amenities in Notes when
present. `has_corporate_rates` is context, not proof every rate is corporate.
- Keep a private display-number-to-literal-rate-`id` map. Never use a room ID, hotel ID, or search
summary rate ID for booking.
- Use top-level `hotel` metadata to reconfirm the selected property. `has_corporate_rates` is
hotel-level context, while each rate's `is_corporate_rate` identifies the actual corporate rate.
These rates are current options, not quotes. Wait for the traveler to select one exact room/rate;
never guess or default to the first or recommended row.
## Preview the selected rate
Call `book-hotel` without `--confirm`. Its positional values are the literal selected hotel ID
from search and selected rate ID from `hotel-rates`. Pass the same stay dates used to fetch the
rate:
CLI:
```bash
ramp travel book-hotel "" "" --output json \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--rationale "preview the selected Chicago hotel rate, Aug 10-13"
```
MCP:
```json
{
"hotel_id": "{selected_hotel_id}",
"rate_id": "{selected_rate_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"confirm": false,
"rationale": "preview the selected Chicago hotel rate, Aug 10-13"
}
```
Call `SubmitHotelBooking` with the above.
For delegated booking, pass the same `--traveler_user_id`. If Ramp says the rate expired or is
missing from cache, start a fresh `search-hotel`; if the rate mismatches the selected hotel or stay
dates, call `hotel-rates` again for that hotel/current dates. Never retry an old rate or silently
choose another.
The preview is authoritative and may differ from the rates response. Present:
- hotel, room, and stay dates
- traveler (`traveler_name_display` when the preview card provides it)
- the selected rate's `nightly_amount` as the pre-tax nightly price; do not substitute an
all-in nightly amount
- payment timing from the selected rate's `payment_type` (or preview card `payment_display`)
- exact all-in `total_amount` from the preview; do not derive it from a nightly amount
- `in_policy` and every `policy_violations` entry
- `requires_approval` and each `approval_steps` entry
- every `eligible_funds` option: `fund_name` and available balance/spending limit when present;
keep a private display-number-to-`fund_uuid` map rather than printing UUIDs
- matching `loyalty_programs`: meaningful `display_name` and `loyalty_number` values that Ramp will
submit on confirm
Present funds as a Markdown table without internal IDs — never as a bullet list or prose:
| # | Fund | Available balance | Spending limit |
|---|------|-------------------|----------------|
| 1 | Client Travel | $1,500 USD | $5,000 USD |
Keep the display-number-to-`fund_uuid` map private. For loyalty memberships, show `display_name`
and only the last four characters of `loyalty_number` unless the traveler explicitly asks for the
full saved number; never display `loyalty_program_id` or `logo` as text.
Carry forward refundability, cancellation, and payment details from the selected rate. If that
rate requires loyalty but the traveler is not eligible, stop before preview and ask them to
correct the membership or choose another rate. Use preview `loyalty_programs` to verify which saved
matching membership Ramp will submit, rather than treating the rate's generic program name as
proof of enrollment.
If the preview is out of policy, show every returned violation and ask why the traveler needs that
rate. Keep their exact justification as `oop_reason`; Ramp requires `--oop_reason` on confirmation
for an out-of-policy quote and approvers see it. `reason` is a separate optional general booking
note unless `request_new_fund=true`, where it is the required trip purpose; include it only under
those conditions.
If no fund was explicitly selected, the preview auto-uses `recommended_fund_uuid` when an eligible
fund is available. Label ` (recommended)` only when the tool auto-populated that
recommendation; a user-selected fund never gets the label, even when its UUID matches. If the
preview echoes an explicitly passed fund as `selected_fund_uuid`; when non-null, that is the
booking fund. If the traveler chooses or changes to a fund, repeat the preview with `confirm=false` and its literal
`fund_uuid` as `spend_allocation_id`, present the refreshed result, and wait for a separate
explicit confirmation turn.
Distinguish `fund_eligibility_status=none_eligible` from `lookup_failed`: the former permits the
new-fund path. When using that path, collect the trip purpose before confirmation because it is
required as `reason` and shown to approvers. The latter requires a fresh preview and must never be confirmed. Use
`approval_display_status` verbatim; do not infer approval wording from `requires_approval` or
`approval_steps` alone.
If `loyalty_program_names_to_offer` is non-empty and this is a self-booking, offer to save one of
the returned programs. The hotel flow may ask that loyalty question together with the usual
confirmation question. Save only the exact returned program name with the membership number the
traveler provides. For a delegated booking, do not offer or attempt to save loyalty; the save action
targets the requester, not the selected traveler. If the traveler saves a program, run a fresh
preview and require fresh confirmation.
If the traveler selected an existing trip, keep its literal UUID in context but do not send
`--trip_id` during preview: Ramp only resolves it during confirm. A missing, invalid, or other-user
trip is ignored and Ramp auto-selects or creates one. Verify the resulting `trip_id`/`trip_name`
after booking when attachment matters. Omitting `--confirm` is the preview flow.
Finish with an explicit confirmation question containing the hotel, room, dates, refundability,
cancellation terms, all-in total, policy/approval state, matching loyalty membership, selected fund
behavior, and OOP justification when applicable. Stop and wait for a clear yes.
## Confirm only after explicit approval
Use the same hotel ID, rate ID, dates, traveler, and fund selection from the approved final preview.
Add `--confirm` and copy the preview's numeric `expected_total_amount` verbatim, with no currency
symbol:
CLI:
```bash
ramp travel book-hotel "" "" --confirm \
--check_in_date 2026-08-10 --check_out_date 2026-08-13 \
--expected_total_amount --output json \
--rationale "book the selected Lower Manhattan hotel rate; traveler approved the preview"
```
MCP:
```json
{
"hotel_id": "{selected_hotel_id}",
"rate_id": "{selected_rate_id}",
"check_in_date": "2026-08-10",
"check_out_date": "2026-08-13",
"confirm": true,
"expected_total_amount": "{preview_expected_total_amount}",
"spend_allocation_id": "{fund_uuid_from_latest_preview}",
"rationale": "book the selected Lower Manhattan hotel rate; traveler approved the preview"
}
```
Call `SubmitHotelBooking` with the above. Include exactly one funding path: `spend_allocation_id`
(the exact fund from the latest preview, including the recommended UUID when the preview
auto-populated it) or `request_new_fund: true` (with `reason` as the trip purpose). Never omit
both and never pass both.
Add optional confirmation flags only when applicable:
- `--traveler_user_id ''`: exact delegated traveler UUID used for fresh search,
rates, and preview.
- `--spend_allocation_id ''`: exact fund from the final preview, including the
recommended UUID when the preview auto-populated it.
- `--request_new_fund=true` plus `--reason ''`: use only when the latest preview
showed the new-fund path. The trip purpose is shown to approvers.
- `--trip_id ''` (CLI) / `trip_id` (MCP): exact existing trip UUID explicitly
selected by the traveler; omit to let Ramp auto-select or create a trip. This is the step
where Ramp resolves it. Note: delegated trip lookup is not exposed on MCP; `GetUserTrips`
returns the caller's own trips only.
- `--oop_reason ''`: exact justification collected after an out-of-policy
preview; required only when `in_policy=false`.
- `--reason ''`: required with `request_new_fund=true`; never repurpose it as the
OOP justification.
Keep any collected OOP justification in context through a fund or price re-preview, but send it
only on confirmation. If the new preview changes policy violations materially, reconfirm that the
traveler's justification still applies.
Confirmation requires exactly one funding path: `spend_allocation_id` or
`request_new_fund=true`. Never omit both and never pass both. Never normalize, reformat, or
recalculate the numeric `expected_total_amount`.
If `confirm=true` fails for **any** reason, stop. Relay the error `message`, follow
`agent_guidance`, and never retry, tweak parameters, switch hotel/room/rate, or confirm again
without a fresh preview and a fresh explicit confirmation. A changed total, missing/expired cache
mapping, or hotel/date mismatch therefore requires the appropriate fresh preview/rates/search and
new approval; it is not permission to retry the confirmation.
On a successful confirmation, present `booking.booking_request_id`, lowercase `booking.status`,
`booking.total_amount`, and `booking.next_steps` before verification. Do not call the reservation
confirmed merely because `booked=true`.
## Verify the booking request
The confirm response's `booked=true` means a booking request was created, not necessarily that the
hotel is confirmed. Show the returned booking status and approval state accurately. Verify with:
CLI:
```bash
ramp travel bookings --output json \
--rationale "verify the Lower Manhattan hotel booking request, Aug 10-13"
```
MCP:
```json
{
"rationale": "verify the Lower Manhattan hotel booking request, Aug 10-13"
}
```
Call `GetBookings` with the above.
For delegated booking, pass the same traveler UUID. Retain the exact `booking.booking_request_id`
from confirmation and match it to the same `booking_request_id` in the bookings response. The
default call includes current/upcoming hotels, flights, and cars. Each entry has a generic `id`;
use the matching entry's exact `id` with `travel booking-details` (CLI) / `GetBookingDetails`
(MCP) for detailed questions. Do not fuzzy-match by hotel name, dates, room type, or booking
time. Use returned `trip_id` and `trip_name` to verify trip attachment when needed. If the
matching request is missing from the default result, retry once with `--include_failed` (CLI) /
`include_failed: true` (MCP); if it is still missing, report that verification could not locate
the submitted request rather than using another entry. Cancelled, rejected, and failed requests
do not block rebooking.
The submit response's nested `booking.status` is lowercase (`pending_approval`, `approved`,
`booked`, or `rejected`). The bookings response uses uppercase request/reservation states:
- `CONFIRMED`: report the reservation as confirmed.
- `PENDING_APPROVAL`: report that the request is awaiting approval, not booked.
- `PROCESSING`: report that fulfillment is still processing and check again later.
- `FAILED`: show `error_message`; address the stated issue before a new booking attempt.
- `CANCELLED`: report that the request/reservation was cancelled.
- `REJECTED`: report that the request was rejected.
`travel booking-details` (CLI) / `GetBookingDetails` (MCP) is flag-gated by
`OMNI_TRAVEL_BOOKING_SUPPORT_SKILL_ENABLED`. If it is not available, degrade gracefully to the
information from `travel bookings` / `GetBookings`. When available, use `request_status`,
`current_total_amount`, `error_message`, and `approval.pending_approval_summary`; relay the
pending approval summary verbatim.
## Cancelling a hotel booking
Cancelling forfeits or spends real money, so it follows the same preview → explicit yes →
confirm discipline as booking. The command is `ramp travel cancel-hotel` (CLI) /
`SubmitHotelCancellation` (MCP); it is enabled per business, so it may be absent for some
accounts (see "If cancellation is unavailable"). It always cancels the entire hotel booking.
### Identify the exact booking first
Never guess which booking to cancel. Resolve it from `ramp travel bookings --output json`
(CLI) / `GetBookings` (MCP) (same `--traveler_user_id` / `traveler_user_id` for delegated
travelers) and use the exact entry `id` — the same exact ID this skill already uses with
`travel booking-details` / `GetBookingDetails`. If more than one booking could match ("cancel
my New York hotel"), show the likely matches and ask which one; never pick by hotel name,
dates, or recency on your own. If the traveler pasted an ID that this conversation's
`travel bookings` / `GetBookings` never returned, look the booking up first instead of
trusting the pasted value.
Cancellation applies to a fulfilled booking. For an unfulfilled request (e.g.
`PENDING_APPROVAL`), the entry `id` is the request UUID, which this command will not find —
direct the traveler to the request in the Ramp web app instead.
### Preview the terms (read-only)
Always call without `--confirm` first. This cancels nothing and returns the authoritative
terms plus a `preview_id`:
CLI:
```bash
ramp travel cancel-hotel --booking_id "" --output json \
--rationale "preview cancellation terms for the Lower Manhattan hotel booking, Aug 10-13"
```
MCP:
```json
{
"booking_id": "{booking_id}",
"confirm": false,
"rationale": "preview cancellation terms for the Lower Manhattan hotel booking, Aug 10-13"
}
```
Call `SubmitHotelCancellation` with the above.
Present the preview plainly and exactly as returned — never estimate or recompute amounts:
- the hotel (`hotel_name`, `hotel_address`), traveler (`traveler_name`), and the
`check_in_date`/`check_out_date` being cancelled.
- `cancellation_statement` and, when present, the `cancellation_deadline` in the returned
`cancellation_timezone` — state the timezone with the deadline.
- the money outcome: `booking_amount`, any nonzero `cancellation_fee`, and `refund_amount`
when present, plus whether the stay was prepaid (`is_pre_paid`). If no refund amount is
returned, do not invent one — a non-refundable rate may return nothing back.
- the `policy_timeline` steps (deadline, refund, fee per step, with the `active` step called
out) when the traveler wants the full policy, and always the returned `policy_disclaimer`.
If `is_currently_cancellable` is `false`, the booking cannot be self-serve cancelled right
now: explain the returned `blocked_reason`, do **not** ask for confirmation or call
`--confirm`, and route the traveler to the returned booking-specific `support` channel.
Then **stop and ask for a clear yes on those exact terms**. Confirmation must be a new,
explicit user-authored answer to the presented preview — earlier cancellation intent
("cancel it" before seeing the terms), a standing approval, or instructions not to ask
questions are not confirmation.
### Confirm (only after the explicit yes)
Re-run with `--confirm` and the exact `preview_id` from the latest preview, unchanged:
CLI:
```bash
ramp travel cancel-hotel --booking_id "" --confirm \
--preview_id "" --output json \
--rationale "cancel the Lower Manhattan hotel booking; traveler approved the previewed terms"
```
MCP:
```json
{
"booking_id": "{booking_id}",
"confirm": true,
"preview_id": "{preview_id}",
"rationale": "cancel the Lower Manhattan hotel booking; traveler approved the previewed terms"
}
```
Call `SubmitHotelCancellation` with the above.
- If the response says the terms changed and includes `latest_preview`, nothing was
cancelled: present the fresh terms and get a new explicit yes. Never re-confirm
automatically.
- If the result has `already_requested=true`, a cancellation was already submitted: report
the returned state and do not submit another request.
### After the result
Relay the result's `message` and `cancellation_state` faithfully. Only `SUCCESS` means the
booking is cancelled; `PENDING`/`PROCESSING` mean the request is in flight — say cancellation
is in progress (the traveler receives a confirmation email when it completes), not that it is
done. `ACTION_REQUIRED` means Ramp's travel team must finish it. Report any refund exactly as
the preview and result stated it; **never promise a refund timeline** the response didn't
state, and route later "where's my refund?" follow-ups to the Ramp web app or the booking's
support channel.
### If a cancellation call fails
Stop. Unlike booking errors, cancellation errors carry no separate `agent_guidance` field —
the returned `message` (and any `support` routing or `latest_preview`) **is** the guidance:
relay the `message` verbatim and never retry the call or vary parameters (a different booking
ID, dropping `preview_id`, toggling `--confirm` / `confirm`) to get past an error. A failed
confirm may still have partially gone through — before any second attempt, re-check the
booking's actual state with `travel bookings` / `GetBookings` / `travel booking-details` /
`GetBookingDetails`, and only start again (from a fresh preview) if the booking is genuinely
still active. A `FAILED` cancellation routes to the
returned support channel, not to a retry: Ramp emails the traveler when an accepted
cancellation later fails, and the booking stops being self-serve cancellable (a fresh preview
returns `blocked_reason` and support routing), so a retry cannot succeed anyway.
### If cancellation is unavailable
`travel cancel-hotel` (CLI) / `SubmitHotelCancellation` (MCP) is enabled per business. If the
command/tool is missing or Ramp reports the capability is unavailable, do not say the booking
can't be cancelled — say self-serve cancellation isn't enabled here and direct the traveler to
the booking in the Ramp web app or the booking's support channel.
Stay changes — different dates, a different room, adding nights, or cancel-and-rebook — are
**not** cancellations and stay outside this skill; send the traveler to the Ramp web app or
the booking's support channel for those.
---
URL: https://agents.ramp.com/skills/ramp-card-management
---
name: ramp-card-management
area: Cards and Spend
supported_surfaces: [cli, mcp]
description: |-
Inspect a user's actual card status, then activate or safely lock/unlock a
card. Use for card status, card lock questions, physical card activation, and
card lock/unlock actions. For cards locked by overdue tasks, use
ramp-complete-tasks to resolve the sanction before re-checking the card.
---
# Card Management
## Non-Negotiables
- Use `ListCards` to inspect cards. Do not substitute a funds/spend-allocation
listing: funds and cards are different entities.
- A card whose underlying lifecycle state is `ACTIVE` can still be locked and
unable to spend. Never report spendability from lifecycle state alone.
- Treat `is_spendable`, `active_locks`, and `blocking_task_count` as the live
effective status. Follow each lock's `remediation_action`; an overdue-task
sanction routes to `ramp-complete-tasks` rather than repeated manual unlock.
- Pass `--agent` for machine-readable JSON output whenever you need to count
or inspect returned fields programmatically.
- Report the fields returned by the API. Do not invent card lifecycle states.
- After every activation or unlock attempt, call `ListCards` again for the same
cardholder and exact card UUID. Do not claim success until the postcondition
reports `is_spendable: true`.
## Workflow
### Step 1: Inspect card status
MCP:
```text
ListCards(rationale="Inspect the user's card and effective lock status")
```
CLI:
```bash
ramp cards list --rationale "Inspect the user's card and effective lock status" --agent
```
In `--agent` mode the response is wrapped in the standard envelope. Card records
live in the returned `cards` array.
Admins and managers can inspect a visible employee's cards by passing that
cardholder's UUID. Resolve the UUID first, then pass `cardholder_user_uuid`.
```bash
ramp users org-chart --rationale "find the employee's user UUID" --agent
ramp cards list --cardholder_user_uuid "USER_UUID" --rationale "Inspect this employee's card and effective lock status" --agent
```
### Step 2: Interpret effective status
Match the intended card by its `id`, `last_four`, `display_name`, and cardholder.
Inspect its effective lock fields before acting:
- `is_spendable: false` means the card cannot spend even if `card_state` is
`ACTIVE` or `is_locked` is false.
- `active_locks` lists every effective `CARD` or
`SPEND_ALLOCATION_MEMBER` lock with its `lock_source` and
`remediation_action`.
- `blocking_task_count` is the number of outstanding tasks blocking spending.
- `GET_ATTENTION_FEED`: load `ramp-complete-tasks`; do not use
`LockOrUnlockCard` to bypass the sanction.
- `LOCK_OR_UNLOCK_CARD`: continue to the normal lock/unlock flow below.
- `UNLOCK_FRAUD_LOCKED_CARD`: use the fraud-specific unlock workflow returned
by the tool. The existing result is `FraudLockDetected`.
- `CONTACT_RAMP`: stop automated unlock attempts and hand the returned lock
details to Ramp support.
### Step 4: Activate a physical card
First call activation by the physical card's last four digits without
`--confirm_delivery`:
```bash
ramp cards activate --last_four 1234 --rationale "activate the user's new physical card" --agent
```
If the result says delivery confirmation is required, ask the user to confirm
that the physical card has arrived. Only after they confirm, retry with
`--confirm_delivery`:
```bash
ramp cards activate --last_four 1234 --confirm_delivery --rationale "user confirmed their physical card arrived" --agent
```
Do not ask about delivery or set `--confirm_delivery` unless the first call says
it is required.
### Step 5: Lock or unlock a card
Before locking, inspect and show the user the affected card, explain that
locking it will block all transactions until it is unlocked, and ask for
explicit confirmation. Run the lock command only after the user confirms.
Do not infer confirmation from a report of suspicious activity or a lost card.
Unlocking does not require confirmation.
Before unlocking, re-run `ListCards`. If any lock's `remediation_action` is
`GET_ATTENTION_FEED`, stop this manual flow and load `ramp-complete-tasks`. If
`is_spendable` is already true, report the live state without a redundant write.
Lock or unlock a card by its id. The id is a required positional argument:
```bash
ramp cards lock 7f3c0d2a-9b1e-4a55-8c21-0e9d6b2f4a10 --action lock --rationale "user lost their card" --agent
ramp cards lock 7f3c0d2a-9b1e-4a55-8c21-0e9d6b2f4a10 --action unlock --rationale "user found their card" --agent
```
After either write, re-run `ListCards` for the same cardholder. For unlock, the
postcondition passes only when the exact card reports `is_spendable: true`. A
successful unlock response, `is_locked: false`, or an underlying `ACTIVE`
lifecycle state is not sufficient.
If normal unlock returns `CardUnlockBlocked`, it is a typed failed precondition,
not a partial success. Report its `remaining_locks` and `blocking_task_count`;
the returned card remains `is_locked: true` and `is_spendable: false`. Follow
each remaining lock's remediation action instead of retrying the same unlock.
## Fields To Inspect
| Field | Meaning |
|---|---|
| `id` | Card UUID used for card actions |
| `last_four` | Last four digits used to identify the card |
| `display_name` | Card display name when returned |
| `card_type` | Physical Ramp card or virtual card |
| `card_state` | Underlying/effective lifecycle state; never sufficient alone to prove spendability |
| `activation_status` | Whether the card is active, pending delivery, awaiting confirmation, or blocked |
| `is_locked` | Live effective lock indicator |
| `is_spendable` | Authoritative result for whether the card can currently spend |
| `active_locks` | Effective locks, including `lock_type`, `lock_source`, and `remediation_action` |
| `blocking_task_count` | Number of attention-feed tasks currently blocking card spending |
## Example Session
```
User: Why can't I use my card?
Agent: > ramp cards list --rationale "Inspect the user's card and effective lock status" --agent
Agent: This card is locked by an overdue Ramp task. Its underlying lifecycle
state does not make it spendable. I'll load `ramp-complete-tasks`, resolve the
outstanding item with you, and then re-check this exact card before saying it is
unlocked.
```
## Gotchas
| Issue | Fix |
|---|---|
| Card lifecycle state is `ACTIVE`, but `is_spendable` is false | The card cannot spend; follow `active_locks[].remediation_action` |
| Remediation is `GET_ATTENTION_FEED` | Load `ramp-complete-tasks`; do not repeat manual unlock |
| `CardUnlockBlocked` | Report `remaining_locks` and `blocking_task_count`, then follow their remediation actions |
| Unlock call succeeded, but the re-check is not spendable | Report the remaining locks and do not claim success |
| Need another employee's cards | Resolve their user UUID and pass `cardholder_user_uuid` to `ListCards` |
| Something broken? | With the user's consent, run `ramp feedback ""`. This sends only that message to Ramp support; omit secrets and diagnostic artifacts. |
---
URL: https://agents.ramp.com/skills/ramp-complete-expenses
---
name: ramp-complete-expenses
area: Cards and Spend
supported_surfaces: [cli, mcp]
description: |-
Find and complete requested revisions and missing items on your transactions —
receipts, memos, accounting categories, funds, and attendees. Use when:
'transaction revision requested', 'fix a rejected expense', 'missing receipts',
'upload receipt', 'attach receipt', 'receipt sweep', 'add memo',
'categorize transactions', 'missing items', 'transaction cleanup',
'fix my transactions', 'set tracking category', 'assign to fund',
'bulk memo update', 'receipt compliance'. Do NOT use for: approving
transactions (use ramp-approval-dashboard), vendor documents like W-9s or
contracts (use ramp-manage-vendors), or spend reporting (use ramp-spend-analysis).
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Scope to the user's own transactions by default (`--transactions_to_retrieve my_transactions`) unless they explicitly request broader access.
- Only the acting cardholder can complete a transaction revision. Do not attempt
revision completion as an admin, manager, or copilot on the cardholder's behalf.
- Show the transaction details before editing. Never blind-edit.
- Never upload a receipt without confirming the match — wrong receipt on wrong transaction is worse than no receipt.
- For bulk edits or receipt sweeps, present the plan and confirm before executing.
- Use `ramp transactions missing {uuid}` as the reliable check for whether a receipt is attached — it returns `missing_receipt: true/false` in real time. The `receipt_uuids` field in the list response can be used as a quick filter, but it may be stale (e.g., remaining null even after a successful upload+attach).
- Receipt file upload is **CLI-only**. CLI uploads must be base64-encoded; accepted types are PNG, JPEG, PDF, HEIC, and WEBP.
- MCP users must upload receipts through the Ramp web or mobile app, or forward them from their work email to `receipts@ramp.com`. Do not call `upload-receipt-file` from MCP.
- The `--user_submitted_fields` flag tracks provenance — include it to mark which fields the user explicitly provided vs agent-inferred.
- All CLI flags use **underscores**, not hyphens (e.g., `--from_date`, `--transaction_uuid`).
## `--rationale` is required
Every command in this skill maps to an agent-tool endpoint that **requires** a
`rationale`: a non-empty string (max 1024 chars) explaining why you are making
the call. Pass it as `--rationale "..."`, or include a `"rationale"` key in the
`--json` body. It is required for **both** agent (`--agent`) and human
(`agent=false`) invocations — omitting it is the #1 cause of `HTTP 422` errors on
these tools. The examples below all include it; keep it on every call.
## Workflow
### Revision-request tasks
When `GetAttentionFeed` / `ramp tasks list` returns
`TRANSACTION_REVISION_REQUESTED`, preserve the task's transaction UUID and
revision request context. Inspect the live transaction, show the requested
changes, and get the cardholder's explicit confirmation before editing.
1. Fix every requested agent-editable field with the applicable workflow below.
A successful field edit does not itself complete the revision request.
2. Re-read the transaction and its missing items. If a requested field still
needs user-only work, give the user the returned Ramp link and stop; do not
complete the revision prematurely.
3. Ask the cardholder for, or have them approve, a nonblank completion reason
summarizing how the request was resolved.
4. Complete the revision for that same transaction.
MCP:
```text
CompleteTransactionRevision(
transaction_uuid="{transaction_uuid}",
reason="Added the requested receipt and corrected the department",
rationale="Complete the cardholder's resolved transaction revision"
)
```
CLI:
```bash
ramp transactions complete-revision {transaction_uuid} \
--reason "Added the requested receipt and corrected the department" \
--rationale "Complete the cardholder's resolved transaction revision" --agent
```
Treat the success response's sanction-lift status `PENDING_VERIFICATION` as an
instruction to verify, not as evidence that a card is unlocked. Poll
`GetAttentionFeed` / `ramp tasks list` and `ListCards` with a finite bound (at
most three rechecks in the current run). On every attention-feed attempt, follow
every section's cursor chain until `next_cursor` is null and require the number
of unique hydrated task UUIDs to equal each section's `total_count`; otherwise
the attempt is incomplete and cannot prove the revision task is absent. If a
section's `total_count` changes between pages, restart that bounded attempt from
the first page rather than combining two snapshots. Success requires a fully
accounted feed in which the same revision task has disappeared
and the exact sanctioned card reports `is_spendable: true`. If the bound is
exhausted, report the live task, `active_locks`, and `blocking_task_count`; do
not say the revision restored card spending.
If revision completion returns:
- `CompleteTransactionRevisionNotFoundError`, re-check the transaction UUID
from the task context.
- `CompleteTransactionRevisionPermissionDeniedError`, stop and explain that the
acting cardholder must complete their own revision.
- `CompleteTransactionRevisionFailedPreconditionError`, re-read the attention
feed and transaction state. This also covers a replay after the revision is
already complete; do not retry it as a new completion event.
- `CompleteTransactionRevisionInternalError`, report that completion could not
be verified and leave the task outstanding.
### Step 1: Find transactions needing attention
```bash
ramp transactions list --transactions_to_retrieve my_transactions \
--from_date {start} --state cleared --rationale "Find transactions needing cleanup" --agent --page_size 50
```
For any transaction, check what's missing:
```bash
ramp transactions missing {transaction_uuid} --rationale "Check missing items on the transaction"
```
Returns `missing_receipt` (bool), `missing_memo` (bool), and `missing_accounting_items` (an array of objects, not category-name strings). Each object has this shape:
```json
{
"category_name": "QuickBooks Online Department",
"category_id": "category-uuid"
}
```
Present results grouped by what's missing:
```
Needs attention: 8 transactions ($4,520 total)
Missing receipts (3):
$1,200 United Airlines 2026-03-01
$ 800 Hilton Hotels 2026-03-03
Missing memos (5): ...
Missing accounting categories (2): ...
```
### Step 2: Fill memos
Before writing memos manually, check if Ramp has suggestions:
```bash
ramp transactions memo-suggestions {transaction_uuid} --rationale "Fetch AI-suggested memos"
```
Returns `memos[]` — an array of suggested memo strings based on the transaction context. Present suggestions and let the user confirm or edit, then:
```bash
ramp transactions edit {transaction_uuid} --memo "Q2 team offsite catering" --rationale "Add the user's memo"
```
To clear a memo, pass an empty string: `--memo ""` (note: `--rationale` itself must never be empty).
### Step 3: Assign to a fund/spend allocation
```bash
ramp transactions edit {transaction_uuid} --fund_uuid {fund_uuid} --rationale "Assign transaction to the chosen fund"
```
To find available funds:
```bash
ramp funds list --funds_to_retrieve MY_FUNDS --include_balance --rationale "List funds to assign the transaction" --agent
```
### Step 4: Set tracking categories (accounting codes)
First, get available categories and their options:
```bash
# List categories
ramp accounting categories --rationale "List tracking categories" --agent
# List options for a specific category (use UUID from above)
ramp accounting category-options {tracking_category_uuid} --rationale "List options for the tracking category" --agent --page_size 50
```
Then edit via `--json` (tracking categories aren't exposed as named flags).
**Important:** When using `--json`, you must include both `rationale` and `transaction_uuid` in the body. The `--json` flag bypasses the CLI's automatic injection of the positional arg and the `--rationale` flag, so a `--json` body without a `"rationale"` key returns `HTTP 422`.
```bash
ramp transactions edit --json '{
"rationale": "Set the tracking category the user chose",
"transaction_uuid": "{transaction_uuid}",
"tracking_category_selections": [
{
"category_uuid": "{category_uuid}",
"option_selection": "{option_uuid}"
}
],
"user_submitted_fields": ["tracking_category_selections"]
}'
```
Note: the field names inside `tracking_category_selections` are `category_uuid` and `option_selection` — NOT `tracking_category_uuid` / `tracking_category_option_uuid` (those are the names returned by the categories list endpoint, not the edit endpoint).
### Step 5: Set attendees
```bash
ramp transactions edit --json '{
"rationale": "Record attendees for this expense",
"transaction_uuid": "{transaction_uuid}",
"attendee_selections": {
"non_ramp_attendees": [
{"attendee_name": "Jane Smith", "attendee_email": "jane@company.com"}
],
"include_self_as_attendee": false
},
"user_submitted_fields": ["attendee_selections"]
}'
```
### Step 6: Upload receipts
MCP cannot upload receipt files. Direct MCP users to the Ramp web or mobile app, or ask them to forward the receipt from their work email to `receipts@ramp.com`.
For CLI callers, when the user has a receipt file and wants to attach it to a transaction:
```bash
# Base64 encode the file (agent does this)
# For a file at /path/to/receipt.pdf:
base64 -i /path/to/receipt.pdf | tr -d '\n'
# Upload and auto-attach in one step
ramp receipts upload \
--content_type "application/pdf" \
--filename "receipt.pdf" \
--file_content_base64 "{base64_string}" \
--transaction_uuid {txn_uuid} --rationale "Upload the receipt"
```
The response returns `receipt_uuid` and `attached_to_transaction: true/false`.
Omitting `--transaction_uuid` uploads the receipt without attaching it. The CLI can attach it later with:
```bash
ramp receipts attach {receipt_uuid} {transaction_uuid} --rationale "Attach the receipt to the transaction"
```
**Bulk upload from a directory** of receipt images/PDFs:
```bash
# For each file:
# 1. Determine MIME type from extension (.pdf → application/pdf, .png → image/png, .jpg → image/jpeg)
# 2. Base64 encode: base64 -i | tr -d '\n'
# 3. Match to a transaction by inferring merchant/date from filename or content
# 4. Upload with -n (dry run) first to verify
ramp receipts upload \
--content_type "image/png" \
--filename "uber-2026-03-01.png" \
--file_content_base64 "{base64}" \
--transaction_uuid {txn_uuid} -n --rationale "Upload the receipt"
# If correct, upload for real (without -n)
```
### Step 7: Handle missing receipts without a file
If the user doesn't have a receipt and wants to provide a reason:
```bash
ramp transactions explain-missing {transaction_uuid} --reason "Lost receipt — vendor confirmed purchase via email" --rationale "Record why the receipt is missing"
```
Or generate a link to the missing receipt affidavit form (the user must complete it manually in the browser):
```bash
ramp transactions flag-missing {transaction_uuid} --rationale "Generate a missing-receipt affidavit link"
```
## Receipt Matching Heuristics
When matching receipt files to transactions:
- **Filename patterns**: `merchant-YYYY-MM-DD.pdf`, `YYYY-MM-DD-merchant.png`, etc.
- **Amount matching**: If the receipt shows an amount, match to transactions within ±$1 at that merchant on that date.
- **Date matching**: Receipt date should be within 1-2 days of `transaction_time`.
- **One receipt per transaction**: Before each upload, re-check the live state with `ramp transactions missing {transaction_uuid}` and skip when `missing_receipt` is `false`. Do not rely on `receipt_uuids` from an earlier list response — it can be stale (see Gotchas) and would trigger a duplicate upload of a recently attached receipt.
Flag uncertain matches as "possible match — verify" rather than auto-uploading.
## MIME Type Reference
| Extension | Content type |
|---|---|
| `.png` | `image/png` |
| `.jpg`, `.jpeg` | `image/jpeg` |
| `.pdf` | `application/pdf` |
| `.heic` | `image/heic` |
| `.webp` | `image/webp` |
## Bulk Cleanup Workflow
For cleaning up many transactions at once:
1. Fetch all transactions in the period
2. Check `missing_items` on each (or batch-check via individual calls)
3. Group by what's missing: memo, accounting, receipt
4. Present summary: "12 transactions need memos, 5 need accounting categories, 3 need receipts"
5. For memos: fetch suggestions for each, present them, let user confirm or edit
6. For categories: if most transactions share a category, offer to bulk-apply
7. For receipts: match files per the heuristics above, dry-run first, confirm the plan
8. Execute sequentially, report progress
## Provenance Tracking
The `--user_submitted_fields` flag tells Ramp which fields the user explicitly provided. This matters for compliance — it distinguishes user intent from agent inference.
Include it when setting fields via `--json`:
```bash
ramp transactions edit --json '{
"rationale": "Add the user's memo",
"transaction_uuid": "{transaction_uuid}",
"memo": "Client dinner with Acme team",
"user_submitted_fields": ["memo"]
}'
```
Valid values: `attendee_selections`, `attendee_uuids`, `fund_uuid`, `memo`, `tracking_category_selections`, `trip_selection`.
When using named flags (`--memo`, `--fund_uuid`), provenance is tracked automatically.
## Pagination
If `pagination.next_cursor` is not null in the JSON envelope, there are more results:
```bash
ramp transactions list --transactions_to_retrieve my_transactions \
--from_date {start} --state cleared --agent --page_size 50 \
--next_page_cursor "{cursor}" --rationale "List the user's transactions"
```
## Example Session
```
User: I have a bunch of transactions that need cleanup
Agent: Let me check what's missing on your recent transactions.
> ramp transactions list --transactions_to_retrieve my_transactions \
> --from_date 2026-03-01 --state cleared --rationale "Find transactions needing cleanup" --agent --page_size 50
Found 8 transactions. Checking missing items on each...
> ramp transactions missing {uuid} --rationale "Check missing items" (×8)
Summary:
5 need memos
2 need accounting categories
1 needs a receipt
Let me get AI-suggested memos for the 5:
> ramp transactions memo-suggestions {uuid} --rationale "Fetch suggested memos" (×5)
Transaction Suggested memo
$135 Facebook Ads 2026-03-02 "Q1 paid social campaign"
$220 AWS 2026-03-05 "March infrastructure costs"
$89 Uber 2026-03-10 (no suggestion)
Want to accept these suggestions, edit them, or write your own?
User: Accept Facebook and AWS. The Uber was a client dinner ride.
Agent: Updating 3 transactions:
> ramp transactions edit {fb-uuid} --memo "Q1 paid social campaign" --rationale "Apply accepted memo suggestion"
> ramp transactions edit {aws-uuid} --memo "March infrastructure costs" --rationale "Apply accepted memo suggestion"
> ramp transactions edit {uber-uuid} --memo "Client dinner transportation" --rationale "Set memo from user input"
Done. Now the receipt: United Airlines, $1,200 on 2026-03-05.
User: It's at ~/Downloads/united-mar5.pdf
Agent: > base64 -i ~/Downloads/united-mar5.pdf | tr -d '\n'
> ramp receipts upload --content_type "application/pdf" \
> --filename "united-mar5.pdf" \
> --file_content_base64 "{base64}" \
> --transaction_uuid "abc-123" -n --rationale "Upload the receipt"
Dry run looks correct — uploading to United Airlines ($1,200) on 2026-03-05.
Proceed?
User: yes
Agent: > ramp receipts upload --content_type "application/pdf" ...
Receipt uploaded and attached (receipt_uuid: def-456).
2 transactions still need accounting categories — want to tackle those next?
```
## When NOT to Use
- **Approving or rejecting items** — use ramp-approval-dashboard
- **Vendor documents (W-9s, contracts, COIs)** — use ramp-manage-vendors
- **Company-wide spend reporting** — the CLI is scoped to your own data; use ramp-spend-analysis or the Ramp dashboard
## Gotchas
| Issue | Fix |
|---|---|
| `HTTP 422 "API error 422: There was an error."` | You omitted `rationale`. It's a required field on these agent-tools (`DEVELOPER_INVALID_SCHEMA`) — add `--rationale "..."`, or a `"rationale"` key in the `--json` body. Required for both agent and human (`agent=false`) calls. Not a permissions problem. |
| `--dry_run` succeeds but the real call 422s | `--dry_run` only prints the body; it does not validate it. Confirm `rationale` is present before sending. |
| `amount` is a formatted string ("$135.40") | Strip "$" and "," for numeric operations |
| `--state` values are lowercase | Use `cleared`, `pending`, `declined` — not uppercase |
| Tracking categories require `--json` | Named flags only cover `--memo` and `--fund_uuid`. When using `--json`, include both `rationale` and `transaction_uuid` in the body. |
| Category field names differ between endpoints | `accounting category-options` returns `tracking_category_option_uuid`, but `transactions edit` expects `category_uuid` + `option_selection` |
| `memo-suggestions` may return empty | Not all transactions have enough context for suggestions |
| `accounting category-options` paginates with integers | Unlike other endpoints, the cursor is a number, not a string |
| `--transactions_to_retrieve` is required | Always include it on `transactions list`. Use `my_transactions` for personal, `all_transactions_across_entire_business` for admin scope |
| Searching for specific transactions | Use `--reason_memo_merchant_or_user_name_text_search "query"` (min 3 chars) |
| Large receipt files may hit shell arg limits | CLI: for files >100KB, write base64 to a temp file and use `--json` with the content read from file. |
| Receipt uploads from MCP | Receipt file upload is unavailable. Direct users to the Ramp web/mobile app or `receipts@ramp.com`. |
| Duplicate upload risk | Re-check `ramp transactions missing {uuid}` immediately before uploading; skip when `missing_receipt` is `false`. `receipt_uuids` alone can be stale. |
| Comment on a transaction | `ramp general comment {uuid} --ramp_object_type transaction --message "text" --rationale "Add a comment for the user"` |
---
URL: https://agents.ramp.com/skills/ramp-complete-tasks
---
name: ramp-complete-tasks
area: Cards and Spend
supported_surfaces: [cli, mcp]
description: |-
Find and complete items in the Ramp attention feed. Use when: 'what needs my
attention', 'show my tasks', 'overdue tasks', 'complete my Ramp tasks', 'why
is my card locked', or a card is locked until required tasks are completed.
For approval-only queues, use ramp-approval-dashboard.
---
# Complete Ramp Tasks
Use this skill to inspect the authenticated user's Ramp attention feed, route
each outstanding task to the correct workflow, and prove that completed tasks
and task-sanction card locks are actually cleared.
## Non-Negotiables
- Start from the live attention feed. Use the MCP tool `GetAttentionFeed` or
`ramp tasks list`; do not infer outstanding tasks from an email, a prior
response, or a card's lifecycle state.
- Pass a non-empty `rationale` on every Ramp tool call or CLI command.
- Show the task and proposed changes before any write, then get the user's
explicit confirmation. Never mark a task complete merely to remove a card
sanction.
- Treat `TRANSACTION_REVISION_REQUESTED` and other task types as routing keys.
Satisfy the underlying task with its owning skill; do not dismiss, mutate, or
relabel an unfamiliar task to make it disappear.
- A card whose underlying lifecycle state is `ACTIVE` can still be unspendable.
`is_spendable`, `active_locks`, and `blocking_task_count` are authoritative;
lifecycle state alone is not.
- Never report a task complete or a card unlocked from a successful write call
alone. Poll the attention feed and, for a card sanction, `ListCards` with a
finite retry bound. Success requires the task to disappear and the exact card
to report `is_spendable: true`.
## 1. Read the Attention Feed
MCP:
```text
GetAttentionFeed(rationale="Find the user's outstanding Ramp tasks")
```
CLI:
```bash
ramp tasks list --rationale "Find the user's outstanding Ramp tasks" --agent
```
Inspect every returned section. Preserve `task_snapshot_uuid`, `task_type`, the
task's entity UUIDs, due date, revision reason, required fields, and any Ramp
link returned in `context`. For each section, accumulate unique
`task_snapshot_uuid` values across its complete cursor chain until
`next_cursor` is null before claiming the feed is complete:
```bash
ramp tasks list --json '{
"rationale": "Continue reading the user's outstanding Ramp tasks",
"sections": [{
"section_type": "{section_type}",
"cursor": "{next_cursor}",
"limit": 20
}]
}' --agent
```
After pagination, compare the number of unique hydrated items collected for
each section with that section's `total_count`. If the counts differ, hydration
silently omitted one or more outstanding tasks: treat the feed as incomplete,
retry the bounded read, and never interpret the missing context as completion.
If `total_count` changes while traversing the cursor chain, restart that bounded
attempt from the first page rather than combining two snapshots. Likewise, if
an item or section contains an error, say that the feed is incomplete and retry
that bounded item. Summarize work by task type and urgency only from a fully
accounted feed.
## 2. Dispatch by Task Type
| Task type | Workflow |
|---|---|
| `TRANSACTION_REVISION_REQUESTED` | Load `ramp-complete-expenses`. Review the revision request, correct the requested transaction fields, call `CompleteTransactionRevision` / `ramp transactions complete-revision` with the user's accepted nonblank reason, then perform both postcondition checks below. |
| `TRANSACTION_MISSING_ITEMS` | Load `ramp-complete-expenses`; fill only the missing receipt, memo, accounting, fund, trip, or attendee requirements. |
| Transaction, reimbursement, bill, or non-procurement request approval | Load `ramp-approval-dashboard`; show details and confirm before approving or rejecting. |
| Procurement approval, submitted request, PO status, or follow-up | Load `ramp-manage-procurement`. For a change request, require its `original_request` and complete `change_request_diff` old/new values before confirmation. |
| Procurement draft | Load `ramp-submit-procurement-request`. |
| Vendor document or onboarding task | Load `ramp-manage-vendors`. |
| Physical-card activation task | Load `ramp-card-management`. |
| Unknown or unsupported task type | Present the task context and returned Ramp link. Ask the user to complete it in Ramp; do not guess a mutation. |
One item can require multiple corrections. Keep its `task_snapshot_uuid` and
transaction/entity UUID attached to the workflow so a similarly named task is
not changed by mistake.
## 3. Handle a Task-Sanctioned Card
When a user asks why a card is locked or asks to unlock it:
1. Call `ListCards` before taking action. Identify the exact card by card UUID,
cardholder, display name, and last four digits.
2. If `is_spendable` is true, report that live state. Do not call an unlock
tool.
3. Inspect every entry in `active_locks`. When `remediation_action` is
`GET_ATTENTION_FEED`, do not call manual card unlock. Read the attention feed
and resolve its outstanding tasks through this workflow. Use
`blocking_task_count` to report how many tasks still block spending.
4. For any other remediation action, return to `ramp-card-management`, which
routes `LOCK_OR_UNLOCK_CARD`, `UNLOCK_FRAUD_LOCKED_CARD`, and `CONTACT_RAMP`.
The underlying `card_state` may be `ACTIVE` while a sanction lock is present.
Never use `card_state == ACTIVE` by itself as evidence that the card can spend.
## 4. Verify Postconditions
After the owning workflow reports success, use bounded polling (at most three
rechecks in the current run) with `GetAttentionFeed` / `ramp tasks list`. On
every attempt, traverse every returned section's complete cursor chain until
`next_cursor` is null, accumulate unique `task_snapshot_uuid` values, and
require the accumulated count to equal each section's `total_count` before
checking for the completed task's UUID or entity UUID.
- The task postcondition passes only when the completed item is absent from the
outstanding feed (or the live response explicitly reports it completed).
- If a section's hydrated-item count differs from `total_count`, the attempt is
incomplete and cannot prove absence.
- If a section's `total_count` changes between pages, restart that bounded
attempt rather than combining pages from different feed snapshots.
- If the task is still present, report it as outstanding and include any newly
returned reason or missing field. Do not claim success.
For a task-sanctioned card, poll `ListCards` within the same finite bound for the
same cardholder and card UUID. The combined postcondition passes only when the
task is absent and that exact card reports `is_spendable: true`. A successful
task write, `PENDING_VERIFICATION`, an absent lifecycle transition,
`is_locked: false`, or `card_state: ACTIVE` alone is insufficient.
If the task disappears but the card remains locked, report the remaining lock
state and reason. Do not loop manual unlock calls or say the card is spendable.
## Gotchas
| Issue | Required response |
|---|---|
| Empty first page with a section cursor | Follow the returned per-section pagination contract before concluding there are no tasks. |
| Task write returned success but item remains | Treat the task as outstanding; re-read its live context and report what remains. |
| `card_state` is `ACTIVE` while `is_spendable` is false | The card cannot spend; inspect `active_locks` and follow each `remediation_action`. |
| Task disappeared but `is_spendable` remains false | Report `active_locks` and `blocking_task_count`; do not claim the card was unlocked. |
| Unknown task type | Present its context and Ramp link for a user handoff; do not invent a completion call. |
---
URL: https://agents.ramp.com/skills/ramp-get-started
---
name: ramp-get-started
area: Getting Started
supported_surfaces: [cli, mcp]
description: |-
Hand your agent this playbook. It gets you set up on Ramp — or applying, if
you're new — plus the menu of everything else you can do. Use as the one-fetch
entry point for Ramp across Claude Desktop, ChatGPT, Claude Code, Codex, and
Perplexity.
title: "Get started with Ramp"
url: https://agents.ramp.com/playbooks/get-started
runtimes: [Claude Desktop, ChatGPT, Claude Code, Codex, Perplexity]
---
# Get started with Ramp
Hand your agent this playbook and it'll figure out whether you're a new or existing Ramp customer, walk you through the right onboarding path, and then hand you the menu of every other playbook you can run. This is the one-fetch entry point — the rest of the playbook directory becomes discoverable once setup is done.
## How to run
Identify which runtime you're operating in (Claude Desktop, ChatGPT, Claude Code, Codex, Perplexity). Use the matching section below; ignore the others.
## Claude Desktop
Copy and run this prompt:
```
## Before you run
- None — this playbook gets you set up.
I want to use Ramp with you. First, ask me:
> Do you already have a Ramp account?
If **yes** (existing customer):
1. Walk me through enabling the Ramp connector: the Claude Desktop connector lives at https://claude.ai/directory/61bac03c-3f98-4b3c-affb-1b99533fa82c — open it and follow the prompts to add Ramp.
2. Confirm I can see Ramp tools in this conversation.
3. Ask whether I want Ramp Agent Cards enabled for my business — if yes, point me to https://docs.ramp.com/developer-api/v1/agent-cards for setup instructions.
If **no** (new customer):
1. Tell me Ramp is a finance platform — corporate cards, bill pay, and expense management — and that I'll need to apply for an account.
2. Point me to https://ramp.com/ to apply. Mention the application takes a few minutes and approval is usually same-day.
3. Once I'm approved and logged in, come back here and we'll wire up the connector.
## What you can do next
Once I'm set up, fetch the playbook directory at https://agents.ramp.com/.well-known/agent-skills/index.json for the full menu of things you can do for me. When I ask what needs my attention, mention overdue tasks, or say a card is locked until I complete tasks, fetch and follow `ramp-complete-tasks`.
```
## ChatGPT
Copy and run this prompt:
```
## Before you run
- None — this playbook gets you set up.
I want to use Ramp with you. First, ask me:
> Do you already have a Ramp account?
If **yes** (existing customer):
1. Walk me through enabling the Ramp connector: the Ramp app for ChatGPT lives at https://chatgpt.com/apps/ramp/asdk_app_69250fb6281c819195b52a1556b0060c — open it and follow the prompts to authorize Ramp.
2. Confirm I can see Ramp tools in this conversation.
3. Ask whether I want Ramp Agent Cards enabled for my business — if yes, point me to https://docs.ramp.com/developer-api/v1/agent-cards for setup instructions.
If **no** (new customer):
1. Tell me Ramp is a finance platform — corporate cards, bill pay, and expense management — and that I'll need to apply for an account.
2. Point me to https://ramp.com/ to apply. Mention the application takes a few minutes and approval is usually same-day.
3. Once I'm approved and logged in, come back here and we'll wire up the connector.
## What you can do next
Once I'm set up, fetch the playbook directory at https://agents.ramp.com/.well-known/agent-skills/index.json for the full menu of things you can do for me. When I ask what needs my attention, mention overdue tasks, or say a card is locked until I complete tasks, fetch and follow `ramp-complete-tasks`.
```
## Claude Code
Copy and run this prompt:
```
## Before you run
- None — this playbook gets you set up.
I want to use Ramp with you from the terminal. First, ask me:
> Do you already have a Ramp account?
If **yes** (existing customer):
1. Install the Ramp CLI if it's not already installed — run `ramp --version` first, and only if that fails, install it safely: download the installer to a private temp file (`install_sh=$(mktemp)` then `curl -fsSL -o "$install_sh" https://agents.ramp.com/install.sh`), review the script, then run it pinned to a specific release tag from https://github.com/ramp-public/ramp-cli/releases (`RAMP_VERSION= bash "$install_sh"`) — the installer downloads that versioned tarball and verifies its published SHA-256 checksum before installing anything. Never pipe the installer URL straight into `bash`, and never download it to a fixed, predictable path.
2. Log me in: `ramp auth login` (this opens a browser; I complete sign-in there).
3. Confirm `ramp --version` returns a version and `ramp auth status` shows me authenticated.
4. Ask whether I want Ramp Agent Cards enabled for my business — if yes, point me to https://docs.ramp.com/developer-api/v1/agent-cards for setup instructions.
If **no** (new customer):
1. Tell me Ramp is a finance platform — corporate cards, bill pay, and expense management — and that I'll need to apply for an account.
2. Install the Ramp CLI if it's not already installed — run `ramp --version` first, and only if that fails, install it safely: download the installer to a private temp file (`install_sh=$(mktemp)` then `curl -fsSL -o "$install_sh" https://agents.ramp.com/install.sh`), review the script, then run it pinned to a specific release tag from https://github.com/ramp-public/ramp-cli/releases (`RAMP_VERSION= bash "$install_sh"`) — the installer downloads that versioned tarball and verifies its published SHA-256 checksum before installing anything. Never pipe the installer URL straight into `bash`, and never download it to a fixed, predictable path.
3. Apply to Ramp: fetch https://agents.ramp.com/.well-known/agent-skills/ramp-apply-for-account/SKILL.md and follow it. It runs my whole application:
- It first asks whether my business already has a formed legal entity with an EIN, so it can take the right path — I don't need to decide that up front.
- If I already have a company, it pre-fills what it can, lets me drop in a document (like an EIN letter) to fill the rest, and paces the application to completion — handing me Ramp links for the steps only I can do (phone verification, SSN, legal agreements, final submit). It never submits for me.
- If I don't have a legal entity yet, it starts the application with incorporation enabled and, after I submit the application, files my US LLC through Ramp in the same application — there is no second application. (It handles the formation step for me via the ramp-incorporate skill; I don't fetch anything myself.) Incorporating through Ramp has a cost — pricing and details are the source of truth at https://agents.ramp.com/docs/account/incorporate; tell me it isn't free and offer to fetch that page for me if I want the specifics before deciding.
4. Applying is what signs me in: accepting the invite email during signup authorizes the Ramp CLI, so there's no separate login step. If that browser handoff gets interrupted, ramp-apply-for-account tells me how to finish it.
## What you can do next
Once I'm set up, fetch the playbook directory at https://agents.ramp.com/.well-known/agent-skills/index.json for the full menu of things you can do for me. When I ask what needs my attention, mention overdue tasks, or say a card is locked until I complete tasks, fetch and follow `ramp-complete-tasks`.
```
## Codex
Copy and run this prompt:
```
## Before you run
- None — this playbook gets you set up.
I want to use Ramp with you from the terminal. First, ask me:
> Do you already have a Ramp account?
If **yes** (existing customer):
1. Install the Ramp CLI if it's not already installed — run `ramp --version` first, and only if that fails, install it safely: download the installer to a private temp file (`install_sh=$(mktemp)` then `curl -fsSL -o "$install_sh" https://agents.ramp.com/install.sh`), review the script, then run it pinned to a specific release tag from https://github.com/ramp-public/ramp-cli/releases (`RAMP_VERSION= bash "$install_sh"`) — the installer downloads that versioned tarball and verifies its published SHA-256 checksum before installing anything. Never pipe the installer URL straight into `bash`, and never download it to a fixed, predictable path.
2. Log me in: `ramp auth login` (this opens a browser; I complete sign-in there).
3. Confirm `ramp --version` returns a version and `ramp auth status` shows me authenticated.
4. Ask whether I want Ramp Agent Cards enabled for my business — if yes, point me to https://docs.ramp.com/developer-api/v1/agent-cards for setup instructions.
If **no** (new customer):
1. Tell me Ramp is a finance platform — corporate cards, bill pay, and expense management — and that I'll need to apply for an account.
2. Install the Ramp CLI if it's not already installed — run `ramp --version` first, and only if that fails, install it safely: download the installer to a private temp file (`install_sh=$(mktemp)` then `curl -fsSL -o "$install_sh" https://agents.ramp.com/install.sh`), review the script, then run it pinned to a specific release tag from https://github.com/ramp-public/ramp-cli/releases (`RAMP_VERSION= bash "$install_sh"`) — the installer downloads that versioned tarball and verifies its published SHA-256 checksum before installing anything. Never pipe the installer URL straight into `bash`, and never download it to a fixed, predictable path.
3. Apply to Ramp: fetch https://agents.ramp.com/.well-known/agent-skills/ramp-apply-for-account/SKILL.md and follow it. It runs my whole application:
- It first asks whether my business already has a formed legal entity with an EIN, so it can take the right path — I don't need to decide that up front.
- If I already have a company, it pre-fills what it can, lets me drop in a document (like an EIN letter) to fill the rest, and paces the application to completion — handing me Ramp links for the steps only I can do (phone verification, SSN, legal agreements, final submit). It never submits for me.
- If I don't have a legal entity yet, it starts the application with incorporation enabled and, after I submit the application, files my US LLC through Ramp in the same application — there is no second application. (It handles the formation step for me via the ramp-incorporate skill; I don't fetch anything myself.) Incorporating through Ramp has a cost — pricing and details are the source of truth at https://agents.ramp.com/docs/account/incorporate; tell me it isn't free and offer to fetch that page for me if I want the specifics before deciding.
4. Applying is what signs me in: accepting the invite email during signup authorizes the Ramp CLI, so there's no separate login step. If that browser handoff gets interrupted, ramp-apply-for-account tells me how to finish it.
## What you can do next
Once I'm set up, fetch the playbook directory at https://agents.ramp.com/.well-known/agent-skills/index.json for the full menu of things you can do for me. When I ask what needs my attention, mention overdue tasks, or say a card is locked until I complete tasks, fetch and follow `ramp-complete-tasks`.
```
## Perplexity
Copy and run this prompt:
```
## Before you run
- None — this playbook gets you set up.
I want to use Ramp with you. First, ask me:
> Do you already have a Ramp account?
If **yes** (existing customer):
1. Walk me through enabling the Ramp connector: the Perplexity connector lives at https://www.perplexity.ai/computer/connectors?connector=ramp — open it and follow the prompts to authorize Ramp.
2. Confirm I can see Ramp tools in this conversation.
3. Ask whether I want Ramp Agent Cards enabled for my business — if yes, point me to https://docs.ramp.com/developer-api/v1/agent-cards for setup instructions.
If **no** (new customer):
1. Tell me Ramp is a finance platform — corporate cards, bill pay, and expense management — and that I'll need to apply for an account.
2. Point me to https://ramp.com/ to apply. Mention the application takes a few minutes and approval is usually same-day.
3. Once I'm approved and logged in, come back here and we'll wire up the connector.
## What you can do next
Once I'm set up, fetch the playbook directory at https://agents.ramp.com/.well-known/agent-skills/index.json for the full menu of things you can do for me. When I ask what needs my attention, mention overdue tasks, or say a card is locked until I complete tasks, fetch and follow `ramp-complete-tasks`.
```
---
URL: https://agents.ramp.com/skills/ramp-incorporate
---
name: ramp-incorporate
area: Getting Started
supported_surfaces: [cli]
description: Form a US LLC through Ramp when your business doesn’t have a legal entity yet. Do not use for businesses with an EIN; use ramp-apply-for-account PATCH instead. Do not use for non-US founders or businesses without a US principal place of business.
---
# Incorporate With Ramp
This skill resolves the `COMPLETE_INCORPORATION` blocker in a Ramp financing
application. It runs entirely within the agent's OAuth session — no separate
third-party account is needed.
**Incorporating through Ramp has a cost.** Pricing and details are the source of
truth at https://agents.ramp.com/docs/account/incorporate. Before filing, tell
the person it isn't free and offer to fetch that page so they can see the current
price; submit the formation only after they're OK with it.
Use this as a continuation of the `ramp-apply-for-account` skill. If there is no current
Ramp application, start there first so the invite, application record, and
business-scoped OAuth session exist before any incorporation write command runs.
Once the financing application has been submitted and progress is waiting on
Ramp (often application `status` = `IN_REVIEW` with required action
`WAIT_FOR_RAMP`), proceed with the incorporation submission path; do not wait
for FA approval or underwriting to clear first. Do not start formation while the
application is only `ready_for_submission=true`. In particular, do not run
`ramp incorporation applicant create` or `ramp incorporation submit` before the
applicant has reviewed and submitted the financing application.
## When to Use
Use this skill in either of these cases:
- `GET /developer/v1/applications/progress` returned:
```json
{
"type": "USER_ACTION_REQUIRED",
"user_action": "COMPLETE_INCORPORATION",
"provider": "doola",
"reason": "NO_LEGAL_ENTITY",
"guidance": "Run 'ramp incorporation submit' to file the LLC..."
}
```
- The person asked to "incorporate my business", "file an LLC", or "set up an
LLC through Ramp" for the current unformed-entity Ramp application after FA
submission, usually when progress is `IN_REVIEW` / `WAIT_FOR_RAMP`.
The applicant has `incorporation:read` and `incorporation:write` scopes in their
OAuth token.
If Ramp reports that the filing flow is unsupported, do not use this skill.
Tell the applicant that this CLI flow is unavailable for their application and
hand them the current Ramp link if progress returned one.
If the application was just created and `--wait_for_auth` was interrupted, the
returned `invite_link` still lets the applicant finish the Ramp browser handoff.
Do not run incorporation write commands until the fallback auth command or browser
callback has saved credentials for the newly-created application business.
The OAuth token must belong to the business being incorporated. If the CLI is
still authenticated to another business or a previous login, the incorporation
commands will create or read incorporation records for that other
business. Re-authenticate through the newly-created application/business before
running the write commands. **Re-authentication triggers a browser-based OAuth
flow — the user must open the Ramp email they receive and complete sign-in
before the CLI can continue.**
Use the same CLI binary, `--env`, OAuth token, and shell context for applicant
creation and formation submission. If `which ramp` resolves to an older binary,
use the explicit installed CLI path for every command in the sequence.
## SSN Handling — Critical
**Never ask the user for their full SSN or SSN last-4 in chat, and never include SSN data in any tool argument.**
The CLI does not collect SSN values for incorporation. SSN entry belongs in the
Ramp application form returned by application progress, not in
`ramp incorporation submit`, chat, CLI prompts, env vars, or `--json`.
The current formation submit path reuses owner, controller, and identity data
already collected on the Ramp financing application when it identifies at least
one owner. Do not ask for `responsible_party` or
environment variables containing SSN data and do not resend
beneficial-owner or controlling-officer payloads during formation filing. The
only exception is the explicit member fallback for a valid no-25%-owner
application described below; it sends non-sensitive `members` but still omits
`ssn_last_4`.
Before filing the LLC, complete every API-writable/non-sensitive application
field first. When the only remaining applicant-owned actions are SSN entry and
optionally phone verification, send the Ramp form link(s) so the person can
complete SSN entry directly in Ramp.
## Workflow
### 1. Confirm the blocker or direct filing path
`GET /developer/v1/applications/progress` is the source of truth for the
`COMPLETE_INCORPORATION` blocker (see `ramp-apply-for-account` skill). Query the endpoint
directly or accept the blocker context handed in by the parent agent.
If the parent agent has already surfaced a `COMPLETE_INCORPORATION` blocker
payload, capture the `reason` field. Otherwise instruct the user to check the
status page in the Ramp dashboard. Recovery branches:
- `NO_LEGAL_ENTITY` → proceed with first submission (start at Step 2).
- `NAME_CONFLICT` → all prior name options were rejected; generate new names before re-submitting.
- `STATE_COMPLIANCE` → prior state had compliance issue; consider changing state.
- `RP_IDENTITY` → responsible party identity rejected; do not re-collect or
re-submit owner/controller/responsible-party payloads in the lean path. Re-fetch
application progress and verify whether the FA-sourced identity fields need a
browser correction or API-writable non-sensitive patch; if the rejection is not
recoverable through those sources, stop and direct the user to Ramp support
with the user-visible error message only.
If there is no active `COMPLETE_INCORPORATION` blocker but the person explicitly
asked to file an LLC for the current unformed-entity application, first re-fetch
application progress. If `ready_for_submission=true`, send the person to Ramp to
review and submit the financing application; do not start formation yet and do
not create an incorporation applicant record. If the financing application still
needs provisional incorporation preferences such as entity type, filing state,
industry, or intended filing date, return to the `ramp-apply-for-account` skill and PATCH
those application fields first. When the person is submitting now, use the
progress wait loop instead of manually polling:
```bash
ramp applications progress --env production --agent \
--wait_for_action REVIEW_AND_SUBMIT --wait_interval 15 --wait_timeout 900
```
When the submitted application is waiting on Ramp (usually application status
`IN_REVIEW` with required action `WAIT_FOR_RAMP`) and
`needs_incorporation=true`, treat that as the direct filing path. Check
`ramp incorporation status --agent` first; proceed with first submission only
when no formation has already been submitted, and do not block on unrelated Ramp
approval or underwriting.
Do not report `WAIT_FOR_RAMP` as a stop condition until this direct filing path
has been evaluated. For submitted unformed-entity applications, the absence of
an explicit `COMPLETE_INCORPORATION` action does not mean the agent is done; it
must still check formation status and create/submit the incorporation when no
formation exists.
If `ramp incorporation status` returns a 404 saying no incorporation formation
exists, treat that as the expected first-submission state when `ramp auth status`
already confirms the current business-scoped OAuth session has
`incorporation:read` and `incorporation:write`. If auth status is missing or
ambiguous, re-authenticate before any write command.
The formation status (which this skill tracks throughout the workflow) is
available via `ramp incorporation status` once the formation is submitted.
### Country handling
This launch flow is US-only. No country lookup is required. Applicant country
fields may default to `US`, but every explicit fallback member address must
include `"country": "US"`; member address country is required and is not
defaulted. Member nationality is optional and defaults to `US`. Ramp rejects
non-US country and nationality values for now.
### 2. Gather formation inputs
Run these in parallel (all are read-only, no SSN involved):
```bash
ramp incorporation states --agent # pick a state (DE is common for LLC)
ramp incorporation industries search --q "" --agent
```
Collect from the user:
- **State**: where to incorporate (default: Delaware for most businesses)
- **NAICS code**: from the industries search result
- **3 name options**: ranked list of LLC name preferences (Ramp files the first available)
The formation create request accepts only optional `state`, `naics_code`, and
`name_options`, plus conditional `members`. Business description, principal
address, office phone, and other application data are not formation-create
fields. If any of those values need to change, update the financing application
separately through the `ramp-apply-for-account` application PATCH flow before
submitting formation.
Do not ask the user to repeat owner, member, responsible-party, controller, or
SSN facts that are already on the Ramp application. In the normal submitted
financing-application path, Ramp uses business, owner/controller,
responsible-party, and address data from the application. The formation payload
only supplies optional filing overrides.
Before choosing the formation payload, inspect the submitted application:
- If `controlling_officer.is_beneficial_owner` is true or
`beneficial_owners` is non-empty, use the lean payload. Do not send `members`.
- If the controlling officer is not a beneficial owner and `beneficial_owners`
is empty, collect and send the explicit `members` fallback in Step 6. Because
formation starts only after financing-application submission, the application
already includes the required no-individual-owns-25% acknowledgement. The application
read response does not expose that acknowledgement, so do not branch on it.
- For any other empty-owner shape, return to `ramp-apply-for-account` and complete or
correct the ownership section. Do not infer that no individual owns 25% or
discover the fallback by repeatedly submitting the lean payload.
Do NOT collect SSN last-4 in the model context or CLI — SSN entry must happen in
the Ramp form.
For the linked financing application's provisional incorporation fields, treat
EIN as optional but do not provide it in tool calls while
`needs_incorporation=true`; Ramp backfills EIN after incorporation completes.
### 3. Create the incorporation applicant record
```bash
ramp incorporation applicant create --agent
ramp incorporation applicant get --agent
```
This is a required pre-submit gate, not an optional verification. It creates the
incorporation applicant record linked to the authenticated business and then
confirms the applicant is available to the same authenticated session that will
submit the formation. Country of residence defaults to `US`.
Run applicant create/get immediately before `ramp incorporation submit`, using
the same CLI binary, OAuth login, and shell session that will run submit.
### 4. Confirm SSN was handled in Ramp
Before `ramp incorporation submit`, re-fetch application progress. If SSN entry
is still a required applicant action, do not run the submit command yet. Tell the
user:
> To complete the LLC filing, Ramp needs your SSN last 4 to match your existing
> identity record. Your SSN last 4 is never sent through the model, CLI, command
> arguments, or env vars.
>
> **Open in Ramp:** ``
>
> Complete SSN entry in the Ramp form, then return here so I can re-fetch your
> application progress.
If phone verification is also still pending, show its link at the same time so
the person can complete both browser steps together. Resume the incorporation
submit path only after progress no longer requires SSN entry.
### 5. Authenticated-session preflight
Before the submit call, verify the public CLI session without printing
sensitive application data:
```bash
ramp auth status --env production --agent
ramp incorporation applicant create --env production --country-of-residence US --agent
ramp incorporation applicant get --env production --agent
```
Do not paste or summarize sensitive KYC output during this flow. Confirm only
that authentication and applicant retrieval succeeded; do not expose resource
identifiers or business details in diagnostics.
### 6. Submit the formation
Use the lean formation payload when the submitted Ramp application identifies
one or more owners through the controlling officer or `beneficial_owners`.
`members` and `responsible_party` are intentionally omitted; Ramp uses that
data from the submitted financing application. Do not include `description`,
`addresses`, `responsible_party`, or `rationale`; update application data
separately when needed. Do not set SSN data in environment variables.
```bash
ramp incorporation submit --json '{
"state": "",
"naics_code": "",
"name_options": [
{"name": "", "entity_type_ending": "LLC"},
{"name": "", "entity_type_ending": "LLC"},
{"name": "", "entity_type_ending": "LLC"}
]
}' --agent
```
All three fields shown above are optional when their values already exist on the
submitted financing application. Do not add other top-level formation-create
fields.
For the valid no-25%-owner shape identified in Step 2, add a non-empty
`members` list to that same payload. Collect these formation-specific member
facts even though the individuals are not 25%+ beneficial owners:
- `legal_first_name` and `legal_last_name`
- `is_natural_person`
- residential `address` (`line1`, optional `line2`, `city`, `state`,
`postal_code`, required `country`, and required E.164 `phone`)
- `ownership_percent`
- `contact_full_name`
- optional `nationality`
Every member must include all required fields above, and the sum of
`ownership_percent` across the list must equal exactly 100. Do not include the
optional `ssn_last_4`; SSN remains browser-only and the CLI rejects SSN fields.
Do not add `responsible_party` or copy the controlling officer into `members`
unless the user confirms that person is actually an LLC member.
```json
"members": [
{
"legal_first_name": "",
"legal_last_name": "",
"is_natural_person": false,
"address": {
"line1": "",
"city": "",
"state": "",
"postal_code": "",
"country": "US",
"phone": "+12065550100"
},
"ownership_percent": 100,
"contact_full_name": ""
}
]
```
On success:
```
✓ Submitted to Ramp. Status: PENDING_REVIEW
Note: SSN was handled in the Ramp application form, not by the CLI.
```
The returned `formation_submission_status` may be `PENDING_REVIEW` for the
normal waiting path, or `SUBMITTED` when pre-EIN early access is enabled and the
formation has been filed but the EIN has not been issued yet.
### 7. Poll for formation status
Ramp processes most filings within a few business days. Poll periodically:
```bash
ramp incorporation status --agent
```
States:
- `PENDING_REVIEW` → still processing
- `SUBMITTED` → filed with the state; limited pre-EIN access is available while waiting for EIN issuance
- `APPROVED` → EIN issued, Ramp backfill in progress
- `REJECTED` → see reason, follow recovery steps below
When `SUBMITTED`, continue the Ramp onboarding flow with the limited-access
annotation returned by `ramp incorporation status`; do not wait for `APPROVED`
unless the next action specifically requires the EIN-backed entity fields.
When reporting status, keep these three tracks separate:
- **Ramp financing application:** use `ramp applications progress`; `IN_REVIEW`
/ `WAIT_FOR_RAMP` means Ramp is reviewing the financing application.
- **Access:** use the `pre_ein` annotation from `ramp incorporation status`;
`pre_ein.access = LIMITED` means limited pre-EIN access while EIN is pending.
- **Incorporation filing:** use `formation_submission_status`; `SUBMITTED`
means filed with the state, and `APPROVED` means EIN issued / full access
unblock is available.
While the provider is still incorporating (formation SUBMITTED but not yet
completed), do not act on or chase certificate-of-incorporation or EIN KYB
follow-ups — those are expected to be unavailable until formation completes.
Acknowledge them as pending and continue; do not surface them to the person as
action items yet.
Once `APPROVED`, Ramp automatically updates `legal_name`, `ein`,
`date_of_incorporation`, and `state_of_incorporation` into the financing
application.
### 8. Confirm blocker cleared
Re-query `GET /developer/v1/applications/progress` directly or check the Ramp
dashboard.
If the user is actively waiting for the incorporation blocker to clear, use the
application progress wait flag rather than a manual spin loop:
```bash
ramp applications progress --env production --agent \
--wait_for_action COMPLETE_INCORPORATION --wait_interval 60 --wait_timeout 1800
```
Use this as a short operational wait after formation state changes. Do not leave
the CLI waiting for days; for provider processing delays, poll
`ramp incorporation status` periodically as described above.
The `COMPLETE_INCORPORATION` blocker should be gone. Normal flow resumes
(`INCOMPLETE_PAGE` blockers for remaining application sections).
### 9. Retrieve formation documents
```bash
ramp incorporation documents --agent
```
Returns articles of incorporation, EIN letter, and other formation docs.
## Worked Example
```bash
# 1. Check application progress
# GET /developer/v1/applications/progress
# → COMPLETE_INCORPORATION blocker, reason: NO_LEGAL_ENTITY
# 2. Research options
ramp incorporation states --agent
ramp incorporation industries search --q "saas restaurant" --agent
# → picks DE, NAICS 541511
# 3. Create applicant
ramp incorporation applicant create --agent
# 4. Confirm application progress no longer requires SSN entry.
# 5. Submit
ramp incorporation submit --json '{
"state": "DE",
"naics_code": "541511",
"name_options": [
{"name": "Acme", "entity_type_ending": "LLC"},
{"name": "Acme Labs", "entity_type_ending": "LLC"},
{"name": "Acme Co", "entity_type_ending": "LLC"}
]
}' --agent
# → SUBMITTED (limited pre-EIN access; EIN still pending)
# 6. Days later — check status
ramp incorporation status --agent
# → APPROVED (EIN auto-backfilled by Ramp)
# 7. Confirm blocker cleared
# GET /developer/v1/applications/progress
# → No COMPLETE_INCORPORATION; INCOMPLETE_PAGE blockers for remaining sections
# 8. Get documents
ramp incorporation documents --agent
# → articles of incorporation, EIN letter
```
## Name Conflict Recovery
If `ramp incorporation status` returns `REJECTED` with `reason: NAME_CONFLICT`:
1. Generate 3 new LLC name options (e.g. add "Group", "Co", geographic suffix).
2. Re-run `ramp incorporation submit` with the new `name_options[]`.
3. Ramp treats the resubmission as a fresh name attempt.
## Gotchas
| Issue | Fix |
|-------|-----|
| SSN entry still required | Send the current Ramp form `deep_link_url`; do not run `ramp incorporation submit` until progress no longer requires SSN entry |
| `ssn` key in `--json` rejected | Never pass SSN in `--json`; use the Ramp form for SSN entry |
| `incorporation:write` scope missing | User must re-authorize OAuth with incorporation scopes via `ramp auth login`. **The user must open the Ramp email they receive and complete sign-in before authorization finishes.** |
| `No incorporation applicant exists for this business` | Do not retry submit first. Run `ramp incorporation applicant create --env production --country-of-residence US --agent`, then `ramp incorporation applicant get --env production --agent`, in the same authenticated terminal that will run submit. Then retry submit. |
| `ramp incorporation status` returns 404 `No incorporation formation exists` plus a generic auth-token hint | If `ramp auth status` confirms the current business-scoped OAuth session and `incorporation:read` / `incorporation:write`, treat this as the first-submission path and continue with applicant create/get. If auth is not clean, re-authenticate first. |
| Applicant retrieval succeeds but submit still says no applicant | Re-authenticate with the intended Ramp business and retry applicant create/get once. Do not add undocumented identifiers to the formation payload. If the public CLI error persists, direct the user to Ramp support with only the user-visible error message. |
| Multiple `ramp` binaries are installed | Use one explicit CLI path for every command in the applicant create/get + submit sequence; do not mix Homebrew and editable/uv-installed binaries. |
| Guidance asks for `members`, `responsible_party`, or SSN environment variables | Use the lean submit payload when the submitted financing application identifies an owner. Use `members` only for the confirmed no-25%-owner fallback; never send `responsible_party` or collect SSN environment variables. |
| No-25%-owner application needs formation members | After FA submission, confirm the controller is not a beneficial owner and `beneficial_owners` is empty; send a non-empty `members` list whose ownership totals 100 instead of retrying the lean payload |
| Business description, address, or office phone needs correction | Update the financing application through its PATCH flow before formation submit; do not add these fields to the formation payload. |
| Fallback member address country or phone omitted | Every explicit member address requires `country: "US"` and an E.164 `phone` such as `+12065550100`; member country is not defaulted. |
| Formation takes days | Poll `ramp incorporation status` every few hours; do not re-submit while PENDING_REVIEW or SUBMITTED |
| Docs not available yet | Documents appear after APPROVED; poll `ramp incorporation documents` |
## If You Get Blocked
If the user wants to report an unresolved public CLI error, ask for consent and
have them approve the exact message before sending feedback:
```bash
ramp feedback ""
```
This sends the approved message to Ramp support. Do not include secrets,
resource identifiers, raw KYC output, or diagnostic artifacts.
---
URL: https://agents.ramp.com/skills/ramp-manage-bills
---
name: ramp-manage-bills
area: Bill Pay
supported_surfaces: [cli, mcp]
description: |-
Search, inspect, and manage vendor bills and invoices. Use when:
'find a bill', 'show me pending bills', 'bill details', 'look up a bill',
'draft bill details', 'bill attachments', 'what bills need my approval'.
Do NOT use for: approving bills (use ramp-approval-dashboard), uploading vendor
documents (use ramp-manage-vendors), or card transaction management
(use ramp-complete-expenses).
---
# Manage Bills
Search, inspect, and review vendor bills — submitted, draft, and pending approval — using the `ramp` CLI.
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- **Deep links**: If the response contains a `bill_url` field, always include it when presenting bill details — it lets the user click through to the Ramp web app. If the field is absent, direct the user to the Ramp bills page instead. Use the URL matching the active CLI environment: `https://app.ramp.com/bills` for production or `https://demo.ramp.com/bills` for sandbox. Run `ramp env` to check the current environment if unsure.
- Bill amounts are numeric **major currency units**. Format the value for display as returned; never divide it by 100 (e.g., `3500` → `$3,500.00`).
- Bill IDs are UUIDs. Always confirm the correct bill before acting on it.
- Use `--agent` for machine-readable JSON output when parsing results programmatically.
- These are **read-only** commands. For approvals, use `ramp-approval-dashboard`. For write operations on drafts, use draft bill commands when available.
- When the agent cannot complete an action (e.g., editing complex bill fields, approving a bill), hand off to the user via the `bill_url` deep link (if present) or the Ramp bills page.
## Workflow
### Step 1: Find bills
Start by searching or listing bills to locate the ones the user needs.
```bash
# Search by vendor name, invoice number, or payment ID
ramp bills search --query "Acme Corp" --agent --rationale "Search bills for the user"
# List all bills (no query required)
ramp bills list --agent --limit 20 --rationale "List bills for the user"
# Paid bills are included by default; pass --no-include_paid to exclude them
ramp bills search --query "UPS" --agent --rationale "Search bills for the user"
# Paginate through results
ramp bills search --query "Acme" --page_cursor "{cursor_from_previous_response}" --agent --rationale "Search bills for the user"
```
If the response includes a `bill_url` field on each bill, present it alongside the bill summary.
### Step 2: Get bill details
Once you have a bill ID, inspect the search row's `is_draft` field and pull comprehensive details with the matching command:
```bash
# Full bill details (submitted bills only)
ramp bills get --bill_id "{bill_id}" --agent --rationale "Review bill details"
# Draft bill details (not yet submitted)
ramp bills draft --bill_id "{bill_id}" --agent --rationale "Draft the bill"
```
Use `ramp bills draft` when `is_draft` is `true`; otherwise use `ramp bills get`. Do not assume search results are submitted bills.
If the selected command reports that the bill is in the other lifecycle stage, retry the alternate detail command with the same bill ID. A draft keeps the same ID when it is submitted, so its lifecycle may change after search.
The `get` response includes: amount, currency, vendor info, approval status, payment status, due date, invoice number, line items, accounting field codings, and more. Use this as your primary tool for investigating a bill — it covers status, amount breakdowns, and metadata in a single call.
If the response includes a `bill_url` field, it is a direct link to the bill in the Ramp web app. The URL routes to the correct page based on bill status (drafts, approvals, or paid).
### Step 3: Retrieve bill attachments
The attachments command supports submitted bills only. Route this step from the lifecycle confirmed by the detail command that succeeded, including after a fallback: after `ramp bills get`, use the attachments command; after `ramp bills draft`, hand off through the draft's `bill_url` (or the environment-appropriate bills page when absent) so the user can inspect attachments in Ramp.
```bash
# Get invoice file attachments for a submitted bill
ramp bills attachments --bill_id "{bill_id}" --agent --rationale "Fetch bill attachments"
```
### Step 4: Pending approvals
```bash
# Bills waiting for the current user's approval
ramp bills pending --agent --limit 20 --rationale "Review bills pending approval"
# Paginate through pending bills
ramp bills pending --page_cursor "{cursor}" --agent --rationale "Review bills pending approval"
```
## How to Present Results
### Bill search results
Include the deep link on each row when present:
```
Found 3 bills matching "Acme Corp":
$3,500.00 Acme Corp INV-2024-001 Approved Due 2026-04-15 →
$1,200.00 Acme Corp INV-2024-002 Pending Due 2026-04-30 →
$ 850.00 Acme Corp INV-2023-012 Paid Paid 2026-03-01 →
```
### Bill details
When showing bill details, include the deep link if available:
```
Bill: INV-2024-001
Vendor: Acme Corp
Amount: $3,500.00 USD
Status: Approved — awaiting payment
Due: 2026-04-15
Created: 2026-03-20
Memo: Q1 office supplies
Open in Ramp: ← include only if bill_url is in the response
Line items:
1. Printer paper (500 reams) $2,000.00
2. Toner cartridges (50 units) $1,500.00
```
### Pending approvals
```
5 bills pending your approval ($12,450.00 total):
$5,000.00 HighSpot INV-4401 Due 2026-03-28 →
$3,200.00 Cometeer INV-882 Due 2026-04-01 →
$2,500.00 UPS Store INV-7722 Due 2026-04-05 →
$1,200.00 Slack INV-9001 Due 2026-04-10 →
$ 550.00 Canva INV-3344 Due 2026-04-12 →
```
If `bill_url` is not present in the response, omit the link column — do not fabricate URLs.
## Deep Link Handoff
When the agent cannot perform an action on a bill — such as approving, editing payment details, or modifying line items — direct the user to complete the action in the Ramp web app:
- If `bill_url` is present in the response, use it:
```
I can't approve bills via the CLI. You can approve this bill directly in Ramp:
$3,500 HighSpot Invoice #1234 →
```
- If `bill_url` is not present, direct the user to the environment-appropriate Ramp bills page (`https://app.ramp.com/bills` for production, `https://demo.ramp.com/bills` for sandbox):
```
I can't approve bills via the CLI. You can find this bill in Ramp at:
https://app.ramp.com/bills ← use https://demo.ramp.com/bills for sandbox
```
Always prefer the `bill_url` from the API response when available — the response URL accounts for bill status and environment. Never fabricate a deep link URL.
Common handoff scenarios:
- **Bill approval** — not available via CLI
- **Editing payment method or schedule** — requires the bill pay UI
- **Adding or editing line items** — complex edits are best done in the app
- **Uploading invoice attachments** — use the Ramp web app
## When NOT to Use
- **Approving or rejecting bills** — use `ramp-approval-dashboard`
- **Uploading vendor documents** (W-9, contracts) — use `ramp-manage-vendors`
- **Transaction receipts, memos, or categories** — use `ramp-complete-expenses`
- **Making payments or releasing funds** — not available via CLI
## Gotchas
| Issue | Fix |
|---|---|
| Bill amounts are numeric major currency units | Format the returned value directly; never divide by 100 |
| Paid bills are included by default | Pass `--no-include_paid` to exclude them |
| `search` can return drafts and submitted bills | Check each row's `is_draft`; use `draft` when true and `get` otherwise |
| `get` returns 404 for a draft bill | Use `ramp bills draft --bill_id` instead |
| Pagination cursor is an encrypted token | Pass the exact `next_page_cursor` value from the response; never fabricate one |
| `bill_url` may be absent or null | Fall back to the environment-appropriate bills page (`app.ramp.com/bills` or `demo.ramp.com/bills`) — never fabricate a deep link |
## Example Session
```
User: What bills do we have from Acme?
Agent: Let me search for Acme bills.
> ramp bills search --query "Acme" --agent --rationale "Search bills for the user"
Found 2 bills from Acme Corp:
$3,500.00 INV-2024-001 Approved Due 2026-04-15 →
$1,200.00 INV-2024-002 Pending Due 2026-04-30 →
User: Show me the details on the first one.
Agent: > ramp bills get --bill_id "abc-123-def" --agent --rationale "Show details for the Acme bill the user selected"
Bill: INV-2024-001
Vendor: Acme Corp
Amount: $3,500.00 USD
Status: Approved — awaiting payment
Due: 2026-04-15
Memo: Q1 office supplies
Open in Ramp:
2 line items, 1 invoice attachment
User: Are there any attachments?
Agent: > ramp bills attachments --bill_id "abc-123-def" --agent --rationale "List attachments on the Acme bill for the user"
1 invoice attachment:
acme-invoice-2024-001.pdf (uploaded 2026-03-20)
User: What bills need my approval?
Agent: > ramp bills pending --agent --rationale "List bills awaiting the user's approval"
3 bills pending your approval ($6,700.00 total):
$3,200.00 Cometeer INV-882 Due 2026-04-01 →
$2,500.00 UPS Store INV-7722 Due 2026-04-05 →
$1,000.00 Canva INV-3344 Due 2026-04-12 →
To approve these, use the ramp-approval-dashboard skill.
```
## Command Reference
| Command | Description | Key params |
|---|---|---|
| `ramp bills search` | Search bills by vendor, invoice number, or payment ID | `--query`, `--limit`, `--include_paid`, `--page_cursor` |
| `ramp bills list` | List bills without a search query | `--limit`, `--include_paid`, `--page_cursor` |
| `ramp bills get` | Full details for a submitted bill | `--bill_id` (required) |
| `ramp bills draft` | Details for an unsubmitted draft bill | `--bill_id` (required) |
| `ramp bills attachments` | Invoice file attachments | `--bill_id` (required) |
| `ramp bills pending` | Bills awaiting your approval | `--limit`, `--page_cursor` |
---
URL: https://agents.ramp.com/skills/ramp-manage-procurement
---
name: ramp-manage-procurement
area: Procurement
supported_surfaces: [cli, mcp]
description: |-
Search, inspect, track, and safely approve submitted procurement requests and
purchase orders. Use when: 'find a PO', 'show procurement request details',
'purchase order status', 'what procurement requests need approval', or
'approve this PO request'. Do NOT use to create, continue, edit, review before
submission, or submit a draft request (use ramp-submit-procurement-request), or for
a cross-product approval queue (use ramp-approval-dashboard).
---
# Manage Procurement
Use this skill for purchase order lookup, unified procurement request lookup,
status tracking after submission, pending procurement approvals, and safe
request approval or rejection.
For a new or existing draft that the user wants to fill, review before
submission, or submit, use `ramp-submit-procurement-request`. For an approval queue
that spans requests, bills, reimbursements, or transactions, use
`ramp-approval-dashboard`.
Do not use it for bill approval/payment, card transaction cleanup,
reimbursements, vendor document upload, accounting recoding, or drafting
contract edits.
## Rules
- Use `purchase_orders` for PO lookup/detail. Use `requests` for unified request
search, request detail, pending approval queue, approval, and rejection.
- Keep identifiers labeled. `purchase_order_id`, `unified_request_id`, and
`spend_request_id` are distinct. Approval acts on
the unified request UUID, not the PO UUID.
- Use `--agent` for responses you need to parse.
- Use `--json` for search filters. The CLI validates unknown JSON keys and enum
values, so use `--dry_run --json ...` when checking payload shape.
- Use exact pagination cursors returned by the API. Do not trim or rewrite them.
- Procurement schemas do not return deep-link URLs. Present returned identifiers
and statuses instead of inventing links.
- Do not approve or reject without first showing details and confirming the
user's intent.
- When `requests get` returns a non-null `original_request`, treat the request as
a change request; before approval, verify that `original_request` identifies the
intended original approved request and review every entry in
`change_request_diff` fields, including each old and new value. Stop if the
source or diff is absent or unexpected.
- A `LINE_ITEM` diff includes only changed values. Overlay a present `new_value`
on `old_value`: a null property means unchanged; the whole `new_value` is null
when the line was removed. When a line item's UUID matches the original, its
tracking categories, custom fields, withholding rates, and external IDs
carried over unchanged; do not report them as changes.
- For rejection, include the user-supplied or user-accepted reason in
`--thoughts`.
- PO amount fields are numeric currency units with a `currency` code. Display
them directly; do not apply cents conversion.
## Workflow
1. Search POs first when the user gives a PO number, vendor, or procurement
record. `filters.search` matches PO numbers, request names, and owner names;
it does not match vendor names. For a vendor search, resolve the vendor UUID
first, then filter POs by `payee_uuids`:
```bash
ramp vendors search --search_term "Figma" --limit 10 --rationale "Find the vendor UUID for the purchase order search" --agent
ramp purchase_orders search --json '{"rationale":"Searching purchase orders for the resolved vendor","filters":{"payee_uuids":["{vendor_uuid}"]},"limit":10}' --agent
```
For a PO number, request name, or owner name, use `filters.search` directly:
```bash
ramp purchase_orders search --json '{"rationale":"Searching purchase orders by PO number","filters":{"search":"PO-1234"},"limit":10}' --agent
```
If a known PO number is not returned, the spend request may still be pending
and the PO may not be issued yet. Search unified requests scoped to purchase
orders:
```bash
ramp requests search --json '{"rationale":"Searching purchase order requests before PO issuance","filters":{"search":"PO-1234","unified_spend_request_types":["PURCHASE_ORDER"]},"limit":10}' --agent
```
Unified request search and pending rows include `unified_request_id` and may
include `purchase_order_number`, but they do not include
`purchase_order_id`. To move from a unified request row to PO detail, first
call `requests get` with `unified_request_id` and use `purchase_order_id`
from that detail response if it is present.
2. Get detail before summarizing or taking action:
```bash
ramp purchase_orders get {purchase_order_id} --rationale "Reviewing purchase order details" --agent
ramp requests get {unified_request_id} --rationale "Reviewing request before action" --agent
```
The unified request identifier is named `unified_request_id` in request
search, pending, detail, approve, and reject responses. Do not call
`purchase_orders get` directly from a request search or pending row; list
rows do not expose `purchase_order_id`.
3. Review pending approvals through the request queue:
```bash
ramp requests pending --rationale "Reviewing pending procurement approvals" --thoughts "Reviewing pending procurement requests" --page_size 50 --request_types PURCHASE_ORDER --agent
```
Paginate with the returned cursor:
```bash
ramp requests pending --rationale "Continuing pending procurement approval review" --thoughts "Reviewing pending procurement requests" --page_size 50 --request_types PURCHASE_ORDER --start "{next_page_cursor}" --agent
```
4. Before approval or rejection, check the current approval step:
```bash
ramp requests get {unified_request_id} --rationale "Pre-approval workflow check" --agent
```
If `approval_workflow.needs_user_action` is not `true`, do not call
`requests approve`; hand off with the identifiers and the current approval
step from `approval_workflow.steps`.
If `original_request` is non-null, also show its original spend request
UUID, unified request UUID, and PO number, followed by the complete
`change_request_diff` fields and old/new values. Confirm that the source is the
original approved request the user intended to amend and that the diff
contains only the expected changes. Do not infer a change request from names or
amounts when `original_request` is null.
5. Act only after the user confirms the exact `unified_request_id`:
```bash
ramp requests approve {unified_request_id} --action APPROVE --rationale "Approving confirmed purchase order request" --thoughts "Approved after confirming PO details with the user" --agent
ramp requests approve {unified_request_id} --action REJECT --rationale "Rejecting confirmed purchase order request" --thoughts "Rejected: {confirmed_reason}" --agent
```
## Output
For search results, keep rows compact and use fields from the command response:
```text
PO number | vendor | amount | currency | PO status | id
```
For detail summaries, include available status and relationship fields:
```text
PO number:
Vendor:
Amount:
PO status:
Request status:
Change-request source:
Change-request diff:
Promise date:
Linked bills:
Linked transactions:
Linked item receipts:
Unified request ID:
```
Before approval, state that approval acts on the unified request UUID, not the
PO ID. For change requests, include the original approved request identifiers and
old/new diff in the confirmation prompt. After action, surface returned
`unified_request_id`, `action`, `success`, and `message`.
## Handoff
Hand off instead of guessing when the CLI cannot complete the workflow, the
current user is not the active approver, the user asks for unsupported edits, or
the task belongs to bills, reimbursements, transactions, vendor documents, or
contracts.
Include the identifiers and statuses you have:
```text
PO number:
Purchase order ID:
Unified request ID:
Spend request ID:
Request status:
Current approval step:
```
---
URL: https://agents.ramp.com/skills/ramp-manage-vendors
---
name: ramp-manage-vendors
area: Vendor Management
supported_surfaces: [cli, mcp]
description: |-
Manage your vendors' documents and onboarding materials — upload vendor
documents such as W-9/W-8 tax forms, contracts, COIs, payment instructions,
and onboarding paperwork, then check bulk upload OCR and matching status.
Use when: 'upload a W9', 'attach a vendor contract', 'bulk upload supplier
docs', 'check vendor document status', 'upload a COI for a vendor'. Do NOT
use for card transaction receipts (use ramp-complete-expenses) or bill
invoice attachments (use ramp-manage-bills).
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Never attach a document to a `vendor_uuid` unless the user has confirmed the vendor and document category.
- If the vendor identity is uncertain, omit `--vendor_uuid` and let Ramp's matching/triage flow handle it.
- Always run `-n/--dry_run` before uploading when using a known `vendor_uuid`.
- Vendor document file contents must be base64-encoded.
- CLI request flags use snake_case, for example `--document_category` and `--file_content_base64`.
- Prefer `--agent` when checking bulk status so counts and per-document fields are easy to parse reliably.
## Workflow
### Mode 1: Attach one document to a known vendor
**Surface: CLI only.** This upload operation is not available through MCP.
Use this when the user gives both a file path and the target vendor UUID.
```bash
# Base64 encode the file
base64 -i /path/to/acme-w9.pdf | tr -d '\n'
# Dry run first
ramp vendors attach-document \
--vendor_uuid "vnd_123" \
--filename "acme-w9.pdf" \
--content_type "application/pdf" \
--file_content_base64 "{base64_string}" \
--document_category W9 \
--dry_run --rationale "Upload the vendor document"
# If the request body is correct, upload for real
ramp vendors attach-document \
--vendor_uuid "vnd_123" \
--filename "acme-w9.pdf" \
--content_type "application/pdf" \
--file_content_base64 "{base64_string}" \
--document_category W9 --rationale "Upload the vendor document"
```
The upload response includes:
- `document_uuid`
- `vendor_uuid`
- `document_category`
- `original_filename`
- `document_url` (temporary download URL, when available)
### Mode 2: Upload one document into vendor matching/triage
**Surface: CLI only.** This upload operation is not available through MCP.
Use this when the user has a file but not a trustworthy vendor UUID yet.
```bash
ramp vendors attach-document \
--filename "acme-contract.pdf" \
--content_type "application/pdf" \
--file_content_base64 "{base64_string}" \
--document_category VENDOR_CONTRACT --rationale "Upload the vendor document"
```
If `vendor_uuid` is omitted in the response, tell the user the document was uploaded into the matching flow and may need review in Ramp.
### Mode 3: Bulk upload many vendor documents
**Surface: CLI only.** This upload operation is not available through MCP.
`documents` is a complex array, so use `--json` for the request body.
```bash
ramp vendors bulk-upload \
--dry_run \
--json '{
"rationale": "Bulk upload vendor documents",
"documents": [
{
"filename": "acme-w9.pdf",
"content_type": "application/pdf",
"file_content_base64": "{base64_w9}"
},
{
"filename": "acme-coi.pdf",
"content_type": "application/pdf",
"file_content_base64": "{base64_coi}"
}
],
"vendor_uuid": "vnd_123"
}'
```
If the dry run is correct, repeat without `--dry_run`. The response returns `batch_id`, `upload_job_uuid`, and `document_count`.
If the user does not have a single confirmed vendor UUID for all files, omit `vendor_uuid` so each document enters bulk triage.
### Mode 4: Check bulk upload status
**Surface: CLI or MCP.** Use the CLI command below or the MCP bulk upload status tool.
Poll the batch until OCR/matching has finished or attention is needed:
```bash
ramp --agent vendors bulk-upload-status "{batch_id}" --rationale "Check OCR/matching progress for the user's document batch"
```
Useful filters:
```bash
# Only W-form documents
ramp --agent vendors bulk-upload-status "{batch_id}" --is_w_document --rationale "Check OCR status of W-form documents in the batch"
# Exclude W-form documents
ramp --agent vendors bulk-upload-status "{batch_id}" --no-is_w_document --rationale "Check OCR status of non-W-form documents in the batch"
```
Summarize these response fields for the user:
- `upload_job.status`, `upload_job.status_reason`, and `upload_job.is_terminal`
- `document_count`
- `matched_document_count`
- `unmatched_document_count`
- `documents_with_running_ocr_count`
- `documents_needing_attention_count`
- `review_required`
For each document that needs attention, include `original_filename`, `document_category`, `needs_attention`, matched vendor name/UUID if present, `vendor_match_score`, and W-form OCR details (`is_likely_w8`, `is_likely_w9`, `tax_details`) when available.
## Document Category Reference
Use the narrowest category that matches the file:
| Document type | `document_category` |
|---|---|
| W-9 form | `W9` |
| W-8 form | `W8` |
| Vendor contract | `VENDOR_CONTRACT` |
| MSA | `MSA` |
| NDA | `NDA` |
| Statement of work | `SOW` |
| Certificate of insurance | `CERTIFICATE_OF_INSURANCE` |
| Payment instructions | `PAYMENT_INSTRUCTIONS` |
| Voided check | `VOIDED_CHECK` |
| Tax paperwork that is not clearly W-8/W-9/1099 | `TAX_DOCUMENT` |
| Anything else | `OTHER` |
If the category is ambiguous, ask the user before uploading. Do not guess between legal, tax, and payment document categories.
## MIME Type Reference
| Extension | `content_type` |
|---|---|
| `.pdf` | `application/pdf` |
| `.png` | `image/png` |
| `.jpg`, `.jpeg` | `image/jpeg` |
| `.heic` | `image/heic` |
| `.webp` | `image/webp` |
## How to Present Results
For a single upload, confirm the filename, category, attachment target, and returned `document_uuid`:
```
Uploaded acme-w9.pdf as W9 and attached it to vendor vnd_123.
Document UUID: doc_456
```
For a bulk batch, lead with job progress and whether manual review is still required:
`upload_job.status: COMPLETE` means the upload job is terminal, not necessarily that document processing is finished. Only say all documents are processed when `documents_with_running_ocr_count` and `documents_needing_attention_count` are both zero and `review_required` is false.
```
Batch {batch_id}
Upload job status: COMPLETE
Running OCR: 0
Matched: 8 / 10
Needs review: 2
Review required: yes
Needs attention
acme-w9.pdf W9 Acme Corp (score 0.92)
wire-info.pdf PAYMENT_INSTRUCTIONS no vendor match
```
## When NOT to Use
- Uploading receipts to card transactions, or editing transaction memo/category/fund metadata - use ramp-complete-expenses.
- Submitting a receipt-backed out-of-pocket reimbursement - use ramp-submit-reimbursement.
- Approving bills, transactions, requests, or reimbursements - use ramp-approval-dashboard.
- Retrieving invoice attachments from a submitted bill - use `ramp bills attachments`.
## Gotchas
| Issue | Fix |
|---|---|
| `ramp vendors bulk-upload` rejects `documents` flags | Build the request body with `--json`; `documents` is a complex array. |
| Uploading a large base64 string hits shell argument limits | Split into smaller batches or put the JSON payload in a temp file and pass it through `--json`. |
| `vendor_uuid` is unknown or uncertain | Omit it and use matching/triage mode instead of guessing. |
| Bulk status still shows running OCR jobs | Re-run `ramp --agent vendors bulk-upload-status {batch_id}` until `upload_job.is_terminal` is true. |
| `review_required` is true or documents have `needs_attention: true` | Tell the user which files need review and why, instead of claiming the batch is fully done. |
| `document_url` is present | Treat it as temporary and avoid storing it as a durable reference. |
---
URL: https://agents.ramp.com/skills/ramp-onboard-standalone-agent
---
name: ramp-onboard-standalone-agent
area: Getting Started
supported_surfaces: [cli]
description: >-
Create, connect, and safely verify a standalone Ramp agent for a clearly
defined job. Use when a Ramp admin needs a machine identity for reporting,
bill intake, bill approval, payment release, or expense cleanup. For a
person's initial Ramp or CLI setup, use ramp-get-started instead.
compatibility: Requires the Ramp CLI, a Ramp admin's dashboard access, and a local shell with secure secret entry.
---
# Set up a Ramp standalone agent
Guide a Ramp admin through one new machine identity for a clearly scoped set of responsibilities. Make the experience conversational: ask one question at a time, wait for the answer, recommend the narrowest access that fits, and explain each boundary in customer terms.
## Availability
Start every setup by telling the user:
```text
Standalone agents are currently in private preview and require access to be enabled for your Ramp account. To request access, visit [agents.ramp.com](https://agents.ramp.com/) or email [agents@ramp.com](mailto:agents@ramp.com).
```
If the user cannot see **Company > Agents** or the standalone-agent permission options, do not send them through the remaining setup steps. Explain that access is not enabled yet and direct them to one of the request-access options above. Never include credentials or secrets in an access request.
## Safety boundaries
- Require a Ramp admin. If the user is not an admin, stop and ask them to involve one.
- Never use a human credential for the agent.
- Never ask for or accept a Client secret or bearer token in chat. The user enters secrets only in their local terminal and stores them somewhere safe, such as a secret manager.
- Do not broaden permissions to fix an error. Diagnose the missing access or product availability instead.
- Verification must be read-only. Do not create, edit, approve, schedule, pay, or delete anything merely to prove the connection works.
## Interactive setup
### Existing-agent shortcut
If the user says they already created the agent, stop the new-role and new-agent creation path immediately. Ask only:
```text
What is the existing agent's name?
```
Then go directly to **Confirm the identity and assigned role** below. Do not ask which role the user assigned. The CLI agent list returns the assigned role name and ID; retrieve that information from Ramp after matching the exact agent name. Continue with runtime connection and safe verification once the active identity and assigned role are confirmed.
### 1. Establish the job
Use `production` by default. Before rendering any CLI command or helper, resolve the selected environment to one of the two CLI values below:
- No environment, `prod`, or `production` resolves to `production`.
- `demo` or `sandbox` resolves to `sandbox`.
Never interpolate user-provided environment text into a command. If the user asks for any other environment, stop and explain that this workflow supports only production and sandbox.
```text
What should this agent do in Ramp? For example: prepare weekly finance reports, prepare bill drafts, approve assigned bills, or clean up expense details.
```
An agent may have more than one responsibility. When responsibilities have different risk levels, explain the tradeoff without forcing separate agents. For example, reporting is read-only while bill approval may trigger payment. Let the admin choose one combined agent or separate identities, then grant only the access required for that choice.
### 2. Recommend the access boundary
Recommend one recipe or the smallest combination of recipes, explain what each enables in practice, and ask the admin to confirm before continuing.
#### Permission recipes
- **Reporting:** Select **View transactions and reimbursements** and **View bills and recurring bills**. These permissions let the agent read card spend, reimbursements, submitted bills, and recurring bills for reporting without changing, approving, or paying anything.
- **Reporting add-ons:** Select **View draft bills** only when the report needs bills still being prepared. Select **View all vendors** only when the report needs vendor details. These add read visibility only; they do not let the agent create, edit, submit, approve, or pay bills.
- **Bill intake:** Select **View draft bills**, **Create draft bills**, **Edit and delete draft bills**, and **View all vendors**. These permissions let the agent prepare, review, revise, and remove draft bills, and read the vendors needed to prepare them. **Edit and delete draft bills** is one checkbox. A human confirms vendor, amount, coding, and documents before submission.
- **Bill submission add-on:** Select **Create bills** only when submitting a prepared bill is explicitly part of the job. This enables submission; it does not add approval, payment-release, approval-policy, or payment-run authority.
- **Bill approval:** Select exactly **View draft bills**, **View bills and recurring bills**, and **Be assigned as an approver on approval chains**. The two view permissions let the agent read draft, submitted, and recurring bills without changing them. **Be assigned as an approver on approval chains** makes the agent eligible for an approval step; it does not route bills to the agent. The agent acts only when it is the next required approver. Completing an approval chain may trigger payment under the company's settings.
- **Transaction cleanup:** Select **Review and edit transactions**. **View transactions and reimbursements** is included as a dependency for visibility. These permissions let the agent review and update only the confirmed transaction fields, such as receipts, memos, and accounting details.
- **Payment release:** This is separate routing configuration, not an additional role-permission recipe. If the job also includes bill approval, use the **Bill approval** recipe. In [Bill payments approvals](https://app.ramp.com/settings/expense-policy/bill-pay-policy#/d/bill-approvals), turn on additional approval for payment release, add the agent as a payer, and keep a human payer in the flow. This routes only the agreed payment-release work and does not grant approval-policy editing, broad payment-run access, or authority to change routing.
For multiple responsibilities, take the union of the applicable recipes and remove duplicate permission names. Present every resulting checkbox by its exact customer-visible name and explain its purpose in the combined job. Keep role permissions separate from routing configuration: configure approval steps and payment release in **Bill payments approvals**, not by adding unrelated role permissions. State that everything outside the selected recipes remains prohibited, including approval-policy administration, payment-run access, card controls, bill administration, and any bill creation, editing, submission, approval, or payment action not explicitly included above. Procurement access is not currently available in the standalone-agent role picker; do not substitute broader permissions.
### 3. Create the role
Prescribe a clear role name from the job, such as `Weekly Finance Reporting Role`, rather than asking the user to name it. Send the admin to [Roles & Permissions](https://app.ramp.com/settings/all/roles-and-permissions) with that name and list every checkbox using its exact customer-facing label. Never paraphrase a label as “the permission that allows…” or otherwise make the admin infer which checkbox to select.
For a bill-approval-only agent, prescribe this exact configuration:
```text
Create a role named Bill Approval Agent Role and select exactly:
- View draft bills
- View bills and recurring bills
- Be assigned as an approver on approval chains
The first two permissions let the agent read draft, submitted, and recurring bills without changing them. The third makes it eligible to be added to the approval chain. Do not enable Create draft bills, Edit and delete draft bills, Create bills, approval-policy editing, payment-run, card-control, or bill-administration permissions.
```
For an agent with multiple responsibilities, use the recipe union above: list each exact checkbox once with its purpose, keep routing configuration separate, and state the prohibited actions that are outside the selected recipes. Then say:
```text
Let me know when it's created.
```
When the user says it is done, carry the prescribed role name forward. Do not ask for the role's exact name unless the user says they chose a different one.
### 4. Create the agent
Prescribe a concise agent name from the job, such as `Weekly Finance Reporter`; do not ask the user to name it. Recommend a description that states the job and boundary in one sentence, then summarize:
```text
Agent:
Job:
Role:
Boundary:
```
Never imply that you can create the agent for the user. Proceed with the prescribed name and summary without asking for another naming confirmation. Say:
```text
Right now, you'll need to create the agent directly in the Ramp dashboard. Open [Company > Agents](https://app.ramp.com/company/agents), select **New agent**, enter the name and description above, assign , and create it. Let me know when it's created.
```
Have the admin save the Client ID and Client secret somewhere safe, such as a secret manager, labeled with the agent name and environment. The Client secret must not be pasted into chat, shell history, a repository, or a plaintext file. The Client ID is not secret and may be shared in chat when the skill needs it; the Client secret may not.
### 5. Confirm the identity and assigned role
In the user's runtime, inspect the installed Ramp CLI before relying on commands:
```bash
ramp --version
ramp auth login --help
ramp agent list --help
```
If the CLI is missing, stop and help the user install it through their approved process. If it is outdated, explain that updating changes the local installation and ask permission before running `ramp update`.
Use the admin's existing business-authenticated CLI session only to look up the agent. Set `RAMP_ENV` to the allowlisted CLI value resolved above, then check its authentication before asking the admin to authorize again:
```bash
ramp -e "$RAMP_ENV" auth status
ramp -e "$RAMP_ENV" agent list --page_size 100
```
If the existing admin session can list agents, reuse it. Open a new business authorization only when authentication is missing, expired, or `agent list` proves the current access is insufficient:
```bash
ramp -e "$RAMP_ENV" auth login --auth-level business
```
The generated CLI accepts `--page_size` and `--start` for `ramp agent list`. Each response contains `data` and `page.next`. Start with `--page_size 100`, inspect the entries in `data`, and when `page.next` is present, extract its opaque `start` cursor and pass it to the next request as `--start "$START"` while preserving `--page_size 100`. Repeat until `page.next` is empty. Only then confirm whether exactly one active agent matches the supplied name. Read its status and assigned role from the complete result set, then tell the user what Ramp returned. Do not ask the user to restate the role. If there is no exact match or more than one plausible match, stop before using credentials and resolve the identity in Ramp. If the exact agent exists but has no assigned role, direct the user to [Company > Agents](https://app.ramp.com/company/agents) to assign one, then repeat the full lookup.
### 6. Configure approval routing — approval agents only
Skip this section entirely unless the confirmed job includes approving bills or releasing bill payments. Do not mention approval-chain setup for reporting, bill intake, expense cleanup, or other non-approval agents.
Explain that the role permission only makes the agent eligible to be selected as an approver; it does not route any bills to the agent. Send the admin to [Bill payments approvals](https://app.ramp.com/settings/expense-policy/bill-pay-policy#/d/bill-approvals) and have them add the agent to the approval step that matches its confirmed job.
Recommend conditions that keep the step within the agreed boundary, using only conditions available in the dashboard. For example, an agent responsible for higher-value software bills could be limited by amount and the relevant vendor or department rather than receiving every bill. Do not give the agent permission to edit approval policies and do not broaden its role to make this configuration work; an admin configures the chain in the dashboard.
If the confirmed job includes final payment release, also have the admin turn on additional approval for payment release, add the agent as a payer, and keep a human payer in the flow. Do not add payment-release instructions for an agent whose job ends at bill approval.
Have the admin review the resulting approval chain, then say:
```text
Let me know when it's configured.
```
### 7. Connect the agent to the runtime
Ask the user for the Client ID; it is safe to collect in chat. Never ask for the Client secret.
Keep the standalone agent's CLI authentication separate from the user's personal CLI authentication. Create a short filesystem-safe slug from the agent name, such as `weekly-finance-reporter`, and scope every agent command to:
```text
XDG_CONFIG_HOME="$HOME/.config/ramp-agents/AGENT_SLUG"
```
On macOS, generate a temporary `.command` helper containing the real Client ID, canonical environment, and slug. The helper may contain the Client ID because it is not a secret. It must never contain the Client secret. Before rendering the helper, use the mapping from step 1 and render exactly one of `RAMP_ENV='production'` or `RAMP_ENV='sandbox'`; never substitute user-provided text. Use a safely quoted Client ID and a slug containing only lowercase letters, numbers, and hyphens.
Create a private temporary directory with `mktemp -d`, set its mode to `700`, and create `connect.command` inside it with this structure:
```zsh
#!/bin/zsh
secret=''
# Render only RAMP_ENV='production' or RAMP_ENV='sandbox' from the allowlist.
RAMP_ENV='production'
cleanup() {
unset secret
}
trap cleanup EXIT
trap 'exit 130' INT TERM
printf '%s\n' 'Connect your standalone agent to Ramp'
IFS= read -rs 'secret?Client secret: '
printf '\n'
RAMP_CLIENT_SECRET="$secret" \
XDG_CONFIG_HOME="$HOME/.config/ramp-agents/AGENT_SLUG" \
ramp -e "$RAMP_ENV" auth login --client-id 'CLIENT_ID'
result=$?
if (( result == 0 )); then
printf '%s\n' 'Connected. Return to your agent to continue verification.'
else
printf '%s\n' 'Connection failed. Return to your agent with the error above.'
fi
exit "$result"
```
Make the helper executable with mode `700`, then open its exact path directly in Terminal. Do these local actions for the user; do not ask them to copy or paste the helper, Client ID, or authentication command:
```bash
chmod 700 /PRIVATE_TEMP_DIRECTORY/connect.command
open -a Terminal /PRIVATE_TEMP_DIRECTORY/connect.command
```
Tell the user that Terminal is open and ask them to paste the Client secret into its hidden prompt and press Return. The secret is held only for the login process and is unset when the helper exits. If the runtime cannot create local files or open Terminal, explain that limitation and provide the helper as a file; ask the user to open the file rather than pasting a long command into their shell. Tailor the helper when the user's local shell or operating system is not zsh on macOS. The CLI does not currently provide its own secure Client-secret prompt.
Verify the isolated connection without replacing personal auth:
```bash
XDG_CONFIG_HOME="$HOME/.config/ramp-agents/AGENT_SLUG" ramp -e "$RAMP_ENV" auth status
```
Authentication status proves the isolated agent identity is connected; it does not prove that its access is correct. Agent access tokens expire and do not refresh automatically, so a long-running runtime must repeat the same client-credential login when needed without exposing the secret.
### 8. Verify one safe action
Inspect current CLI help for the job's read operation, then run one small, read-only check using the same command-scoped `XDG_CONFIG_HOME` prefix. This verification runs under the isolated agent session, not the admin's business session:
- finance reporting: list a few transactions or bills needed for the report
- bill intake: list a few draft bills; do not create one
- bill approval: list bills awaiting the agent; do not approve one
- expense cleanup: list a few transactions; do not edit one
Explain the expected result before running it. Success means the agent can read the minimum data its job needs. Do not require a deliberate authorization failure or probe unrelated company data.
If the expected read is unavailable, keep the role unchanged. Check the exact non-secret error, current CLI help, and whether the workflow is enabled for the business. If those do not distinguish product availability from role or authentication setup, route the user to [agents.ramp.com](https://agents.ramp.com/) or `agents@ramp.com`.
## Finish
Return a concise readiness summary:
```text
Agent:
Environment:
Job and role:
Credentials: stored securely; no secret values displayed
Runtime connection: connected / blocked
Safe verification:
Ready for:
Open blocker:
```
Do not stop after the readiness summary. Always add a **What you can do now** section with one or two concrete examples tailored to the confirmed job. For a bill approval agent, explain that the user can now ask the connected runtime to show bills routed to the agent, review a bill's available details, and approve or reject it under the user's stated decision rules. Example requests include:
```text
Show me the bills waiting for Bill Approval Agent.
Review the Acme bill and summarize anything I should check before approving it.
Approve the Acme bill if it matches the decision rules we agreed on; otherwise escalate it to me.
```
Make clear that routing conditions decide which bills reach the agent, while decision rules decide which of those bills it may approve. Before unattended approval, the user should define rules for amount limits, required documentation, vendor exceptions, accounting or policy checks, and when the agent must escalate to a human. If those rules have not been defined, say the agent is ready for interactive use but not yet ready for unattended approvals.
Then always add a **Ways to use this agent** section. This section is mandatory even when the user has not asked for deployment help. Proactively explain all of these options in customer terms:
- **Use it in the current connected runtime:** The user can make on-demand requests for the confirmed job, such as listing the approval queue, reviewing a bill, or approving a bill under confirmed decision rules.
- **Call the Ramp API:** The Client ID and Client secret can authenticate a service that calls the [Ramp API](https://docs.ramp.com/). For example, an internal approval service can check bills routed to this agent and act according to company rules. API access remains limited by the agent's assigned role and granted scopes.
- **Use the Ramp CLI:** The isolated agent login can be used for ad hoc commands or scripts without replacing the user's personal Ramp login. Ordinary unprefixed `ramp ...` commands continue using personal authentication; if `XDG_CONFIG_HOME` was exported, `unset XDG_CONFIG_HOME` returns to it.
- **Run it in an agent or automation:** The credentials can be stored in Claude managed agents, a Hermes agent, a cron job, or another runtime. That runtime can run the job on demand, poll or run on a schedule, and reauthenticate when the agent token expires.
- **Retain clear attribution:** Actions performed with these credentials are attributed to this standalone agent in Ramp activity logs and other Ramp surfaces, so the business can distinguish the agent's work from a person's actions.
Follow with a **Recommended next step** tailored to the job. For a bill approval agent, recommend documenting the approval decision rules and escalation path, then either continuing interactively in the connected runtime or placing the credentials and operational skill in the user's chosen long-running runtime. For a reporting agent, recommend defining the report contents and schedule. For other jobs, recommend the equivalent operating instructions and trigger.
When describing a long-running setup, give the concrete sequence without waiting for the user to ask:
1. Store the Client ID and Client secret in the runtime's secure credential storage; never paste the Client secret into chat or source code.
2. Give the runtime the operational instructions or skill for the confirmed job, including decision rules and escalation paths where actions can change data or move money.
3. Choose an on-demand or scheduled trigger appropriate to the job.
4. Reauthenticate with the client credentials when the agent token expires.
Offer to continue with the user's preferred option, but do not make the explanation above conditional on that choice.
Keep the handoff customer-facing. Do not mention repositories, local skill copies, installation provenance, internal validation, or whether a skill was pulled.
End by asking: `If you have feedback on this setup process, tell me now and I can submit it using the Ramp CLI feedback tool.` If the user provides feedback, submit their words with `ramp feedback "FEEDBACK"` and report whether the submission succeeded.
---
URL: https://agents.ramp.com/skills/ramp-payment-lookup
---
name: ramp-payment-lookup
area: Bill Pay
supported_surfaces: [cli, mcp]
description: |-
Look up vendor bill payments and verify payment status from the terminal.
Use when: 'did we pay', 'payment status', 'check if paid', 'verify payment',
'find invoice', 'bill lookup', 'was this bill paid', 'payment confirmation'.
Do NOT use for: approving bills (use ramp-approval-dashboard), spend analysis
across vendors (use ramp-spend-analysis), or uploading receipts (use ramp-complete-expenses).
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Always search bills first with `--include_drafts=false`, then enrich submitted bills with `bills get` and `bills attachments` when available.
- Report exactly what the API returns. Don't infer payment state beyond what the status says.
- When checking whether a payment went out, search with `--include_paid` to include completed bills.
- Pass `--agent` for machine-readable JSON output on all commands.
- Bill amounts in search and `get` responses are numeric major currency units (for example, dollars for USD). Never divide bill amounts by 100. Transaction amounts are formatted strings (`"$135.40"`). Reimbursement amounts are in dollars.
- Check `is_draft` on every search result. If a draft appears despite `--include_drafts=false`, use `bills draft` instead of `bills get`.
- For approval or modification history, use `bills history`; `bills get` does not include history.
- If `bills get` fails on a search-returned ID, fall back to the search results and note the limitation.
## Workflow
### Step 1: Search for the bill
```bash
ramp bills search --query "" --include_paid --include_drafts=false --limit 10 --agent --rationale "Search bills for the user"
```
The `--query` flag matches against payee name, invoice number, and payment ID. Partial matches work (e.g., "UPS" matches "UPS Store").
If too many results, narrow with more specific terms. If zero results, try alternate vendor name spellings.
If `next_page_cursor` is not null, paginate with `--page_cursor`:
```bash
ramp bills search --query "" --include_paid --include_drafts=false --limit 10 \
--page_cursor "" --agent --rationale "Search bills for the user"
```
### Step 2: Drill into details
Check the search result's `is_draft` field. For a submitted bill (`is_draft=false`):
```bash
ramp bills get --agent --rationale "Review bill details"
```
This returns payment and bill metadata, but not approval history.
If a result unexpectedly has `is_draft=true`, route it to the draft command instead:
```bash
ramp bills draft --bill_id --agent --rationale "Review draft bill details"
```
For a draft, do not run `bills history` or `bills attachments`; both commands support submitted bills only. Drafts do not yet have submitted-bill approval history. For draft attachment requests, direct the user to the `bill_url` returned by `bills draft`, where attachments can be viewed.
Use `-n` (dry run) to preview the request without sending:
```bash
ramp bills get --agent -n --rationale "Review bill details"
```
### Step 3: Pull approval history for a submitted bill when requested
```bash
ramp bills history --bill_id --agent --rationale "Review bill approval history"
```
Use this dedicated command for approval-history or modification-history requests only when `is_draft=false`.
### Step 4: Pull the invoice attachment for a submitted bill
```bash
ramp bills attachments --agent --rationale "Fetch bill attachments"
```
For `is_draft=false`, this returns invoice files associated with the bill for cross-reference. For `is_draft=true`, use the `bill_url` from `bills draft` instead.
If `get` fails on a bill ID, `attachments` will likely fail too.
### Step 5: Present findings
Summarize clearly:
```
Bill found: DHL
Invoice #: 24-165-12
Amount: $1,287.95 USD
Payment status: [status from API]
Approval status: [status from API]
Memo: Business package shipment and insurance
```
If the user wanted to confirm a payment went out, state explicitly whether the status confirms payment or not.
## Fields Available
### From `bills search`
| Field | Description |
|---|---|
| `id` | Bill UUID |
| `is_draft` | Whether the result is an unsubmitted draft; route true values to `bills draft` |
| `invoice_number` | Vendor invoice number |
| `vendor_name` | Payee name |
| `amount` | Numeric amount in major currency units; never divide by 100 |
| `currency` | Currency code |
| `due_date` | Payment due date (may be null) |
| `payment_status` | Payment state (may show "Unknown" for unpaid) |
| `approval_status` | Approval state |
| `memo` | Bill description |
### From `bills get` (when available)
Additional fields beyond search — exact schema depends on bill state. May include payment method and accounting metadata. Use `bills history` for approval history.
## Example Session
```
User: Did we pay DHL this month?
Agent: Let me check.
> ramp bills search --query "DHL" --include_paid --include_drafts=false --limit 10 --agent --rationale "Search bills for the user"
Found 1 bill:
DHL — Invoice #24-165-12
Amount: $1,287.95 USD
Payment status: PAID
Approval status: APPROVED
Memo: Business package shipment and insurance
Let me get full details.
> ramp bills get fe8e01ce-12a4-4512-bd27-da8a7432466d --agent --rationale "Review bill details"
Bill found: DHL
Invoice #: 24-165-12
Amount: $1,287.95 USD
Payment status: PAID
Payment method: ACH
Payment date: Mon, 14 Apr 2026
Approval status: APPROVED
Yes, DHL was paid $1,287.95 via ACH on April 14.
User: What about PwC?
Agent: > ramp bills search --query "PwC" --include_paid --include_drafts=false --limit 10 --agent --rationale "Check whether the PwC bill has been paid"
Found 1 bill:
PwC — Invoice #231458926-12
Amount: $15,000.00 USD
Payment status: OPEN
Approval status: PENDING
Memo: Consulting Hours
This bill hasn't been paid yet — it's still pending approval.
Want me to pull the invoice attachment?
```
## When NOT to Use
- **Approving or rejecting bills** — use `ramp-approval-dashboard`
- **Analyzing spend across multiple vendors** — use `ramp-spend-analysis`
- **Uploading receipts** — use `ramp-complete-expenses`
## Gotchas
| Issue | Fix |
|---|---|
| Bill amount looks unexpectedly large | Treat it as major currency units and report it exactly; never divide by 100 |
| Search returns a result with `is_draft=true` | Use `ramp bills draft --bill_id ` instead of `bills get` |
| User asks who approved a bill | Use `ramp bills history --bill_id `; `bills get` does not include approval history |
| `payment_method` only in `bills get` | Search results don't include payment method — drill into details to find ACH vs check vs wire |
| Something broken? | With the user's consent, run `ramp feedback ""`. This sends only that message to Ramp support; omit secrets and diagnostic artifacts. |
---
URL: https://agents.ramp.com/skills/ramp-spend-analysis
---
name: ramp-spend-analysis
area: Cards and Spend
supported_surfaces: [cli, mcp]
description: |-
Analyze spend by vendor, category, or team over a date range. Broad AI-spend
questions include both financial spend and estimated token cost.
Use when: 'how much did we spend on', 'vendor spend', 'SaaS review', 'spend report',
'AI spend', 'token spend', 'token usage', 'inference costs', 'total spend',
'spend by vendor', 'spend analysis', 'pull transactions for', 'cost breakdown'.
Do NOT use for: approving transactions (use ramp-approval-dashboard), uploading receipts
(use ramp-complete-expenses), or verifying a single bill payment (use ramp-payment-lookup).
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Always query both **transactions** and **bills** when investigating complete vendor spend. Card charges and bill payments are separate resources — there is no unified spend endpoint.
- For a broad **AI, LLM, or inference spend** question, query token cost in addition to transactions and bills. Report these measures separately and never add them because they can overlap. If the user explicitly asks for card/Bill Pay or token data, query only that data source.
- Never treat a search result's bill `amount` as paid-in-period spend. It is the full invoice amount, and a payment-date match may represent only one partial payment. A complete paid total requires payment-allocation amounts and dates from another source.
- Pass `--agent` for machine-readable JSON output on all commands.
- Handle amount format differences: transactions use strings (`"$1,048.25"`, `"-$259.49"`), bills use numbers (`15000`), PO amounts use numbers. Reimbursement amounts are in dollars.
- Paginate until `next_page_cursor` is null — a single page may not return everything.
- Flag vendor name variants explicitly (e.g., "Delta Air Lines" vs "Delta Airlines") — the API does not normalize.
- Negative transaction amounts are refunds. Include them in totals but call them out.
## AI Token Cost
Run `ramp ai-spend` to see the available token-spend commands, then use the command that matches the requested scope.
## Workflow
### Step 1: Pull card transactions
For each vendor (or all vendors if doing a broad analysis):
```bash
ramp transactions list \
--transactions_to_retrieve all_transactions_across_entire_business \
--reason_memo_merchant_or_user_name_text_search "" \
--from_date \
--to_date \
--include_count \
--page_size 200 \
--agent --rationale "List the user's transactions"
```
If `next_page_cursor` is not null, paginate:
```bash
ramp transactions list \
--transactions_to_retrieve all_transactions_across_entire_business \
--reason_memo_merchant_or_user_name_text_search "" \
--from_date \
--to_date \
--page_size 200 \
--next_page_cursor "" \
--agent --rationale "List the user's transactions"
```
**For broad analysis (all vendors):** Omit `--reason_memo_merchant_or_user_name_text_search` to pull all transactions, then group client-side.
### Step 2: Pull bill payments
For a specific vendor:
```bash
ramp bills search --query "" --include_paid \
--from_payment_date --to_payment_date \
--limit 50 --agent --rationale "Search bills paid during the user's requested period"
```
For broad analysis (all vendors):
```bash
ramp bills search --include_paid \
--from_payment_date --to_payment_date \
--limit 50 --agent --rationale "Search all bills paid during the user's requested period"
```
Repeat either with `--page_cursor` if `next_page_cursor` is not null, preserving the same query and date bounds on every page.
**Note:** Use `from_payment_date` and `to_payment_date` to find bills with payment activity in the period, then use each result's `payment_date` when presenting or validating the period. Do not substitute `due_date`: it describes when payment was due, not when spend was paid. A matched bill may be partially paid, while its `amount` is the full invoice amount. Do not add that amount to actual spend without payment-allocation data. Bills with `payment_status: "OPEN"` are unpaid — report them separately as commitments.
### Step 3: Parse and aggregate
#### Transaction amounts (strings → numbers)
```bash
# Sum a single vendor's transactions
... | jq '[.data[0].transactions[].amount | gsub(","; "") | if startswith("-$") then ltrimstr("-$") | tonumber | (. * -1) elif startswith("$") then ltrimstr("$") | tonumber else tonumber end] | add'
```
#### Bill amounts are invoice amounts, not paid amounts
Use bill search to identify payment activity and relevant bill IDs. Do not sum
`BillInfo.amount` as paid-in-period spend: payment-date filtering can match one
installment of a partially paid bill while `amount` remains the full invoice
amount. Only combine bills into an actual-spend total when payment-allocation
amounts and dates are available; otherwise present the card subtotal and matched
bills separately, and label the complete paid total unavailable.
#### Multi-vendor table with pagination
Fetch all pages first, then aggregate. Write only the minimum fields needed into
a **fresh per-run directory** (`mktemp -d`) — never a fixed path or glob shared
across runs, or stale pages from a previous or concurrent analysis get silently
included in the totals. These files can contain sensitive spend data: do not
commit or upload them, redact values before any user-approved sharing, and
delete the directory as soon as the analysis is complete. The loop continues
until `next_page_cursor` is null:
```bash
run_dir=$(mktemp -d /tmp/spend_analysis.XXXXXX)
# Page 1
ramp transactions list \
--transactions_to_retrieve all_transactions_across_entire_business \
--from_date 2026-01-01 --page_size 200 --include_count --agent \
--rationale "List the user's transactions" \
> "$run_dir/txns_page1.json"
# Page 2+ (repeat until next_page_cursor is null)
ramp transactions list \
--transactions_to_retrieve all_transactions_across_entire_business \
--from_date 2026-01-01 --page_size 200 --include_count --agent \
--next_page_cursor "" \
--rationale "List the user's transactions" \
> "$run_dir/txns_page2.json"
```
Then merge all pages from this run's directory and build the vendor table:
```bash
jq -s -r '
[.[].data[0].transactions[] |
{merchant: .merchant_name,
amt: (.amount | gsub(","; "") |
if startswith("-$") then ltrimstr("-$") | tonumber | (. * -1)
elif startswith("$") then ltrimstr("$") | tonumber
else tonumber end)}]
| group_by(.merchant)
| map({vendor: .[0].merchant, total: (map(.amt) | add), count: length})
| sort_by(-.total)
| .[] | "\(.vendor)\t$\(.total)\t(\(.count) txns)"
' "$run_dir"/txns_page*.json
```
Clean up with `rm -rf "$run_dir"` when the analysis is done.
### Step 4: Present results
Format as a clear table:
```
Vendor Spend: 2026-01-01 to 2026-04-01
Vendor Card Spend Bills Matched Complete Paid Total
─────────────────────────────────────────────────────────────────────
Figma $24.00 0 $24.00
Delta Airlines $3,663.73 0 $3,663.73
AWS $12,450.00 2 Requires payment-allocation data
Confirmed card subtotal: $16,137.73
```
Call out:
- **Vendor name variants** found (e.g., "Delta Air Lines" + "Delta Airlines")
- **Refunds** included in the totals
- **Bills vs transactions** breakdown if both exist for a vendor; do not add full invoice amounts to paid spend
- **Pagination** — whether all results were captured or if there are more pages
## Fields Available
### From `transactions list`
| Field | Description |
|---|---|
| `transaction_uuid` | Transaction UUID (use for deduplication) |
| `merchant_name` | Merchant name (not normalized) |
| `amount` | Formatted string (`"$8.00"`, `"-$259.49"`, `"$1,048.25"`) |
| `transaction_time` | ISO 8601 timestamp |
| `spent_by_user` | Employee name |
| `merchant_category` | Category (e.g., "SaaS / Software", "Airlines") |
| `reason_or_justification` | Memo / reason |
| `spend_allocation_name` | Fund / budget name |
| `transaction_link` | Direct link to transaction in Ramp UI |
### From `bills search`
| Field | Description |
|---|---|
| `id` | Bill UUID |
| `vendor_name` | Payee name |
| `amount` | Full invoice amount in dollars; not necessarily the amount paid in the requested period |
| `invoice_number` | Vendor invoice number |
| `payment_date` | Displayed payment date, typically the scheduled or sent date |
| `payment_status` | Payment state |
| `memo` | Bill description |
## Multi-Vendor Queries
For SaaS reviews or inference spend monitoring, run vendors in parallel:
```bash
# Run these concurrently
ramp transactions list --transactions_to_retrieve all_transactions_across_entire_business \
--reason_memo_merchant_or_user_name_text_search "Figma" --from_date 2026-01-01 --include_count --agent --rationale "List the user's transactions"
ramp transactions list --transactions_to_retrieve all_transactions_across_entire_business \
--reason_memo_merchant_or_user_name_text_search "Anthropic" --from_date 2026-01-01 --include_count --agent --rationale "List the user's transactions"
ramp transactions list --transactions_to_retrieve all_transactions_across_entire_business \
--reason_memo_merchant_or_user_name_text_search "OpenAI" --from_date 2026-01-01 --include_count --agent --rationale "List the user's transactions"
```
**Tip:** For vendors with known name variants, run multiple searches and deduplicate by `transaction_uuid`:
- "AWS" and "Amazon Web Services"
- "Delta Air Lines" and "Delta Airlines"
## Vendor Name Variant Detection
The API does not normalize merchant names. Common variants to watch for:
| Vendor | Possible variants |
|---|---|
| AWS | "AWS", "Amazon Web Services", "Amazon.com Services" |
| Delta | "Delta Air Lines", "Delta Airlines" |
| Google | "Google Cloud", "Google", "GCP" |
When running broad analysis (all transactions), group results and flag merchants with similar names. The agent should suggest merges.
## Example Session
```
User: How much have we spent on Figma this year?
Agent: Let me pull both card transactions and bills.
> ramp transactions list --transactions_to_retrieve all_transactions_across_entire_business \
> --reason_memo_merchant_or_user_name_text_search "Figma" \
> --from_date 2026-01-01 --to_date 2026-04-01 --include_count --agent \
> --rationale "List the user's transactions"
> ramp bills search --query "Figma" --include_paid --from_payment_date 2026-01-01 \
> --to_payment_date 2026-04-01 --limit 50 --agent \
> --rationale "Search Figma bills paid during the user's requested period"
Figma spend (2026-01-01 to 2026-04-01):
Card transactions: 3 totaling $24.00
$8.00 Michael Scott 2026-03-17 "Trial spend for one Sales user"
$8.00 Michael Scott 2026-03-17 "Trial spend for one Sales user"
$8.00 Michael Scott 2026-03-17 "Trial spend for one Sales user"
Bills: 0
Total: $24.00
Note: There's also an open PO for Figma Enterprise ($150,042.75, PO #9880)
if you want to check commitment vs actual spend.
User: Give me a full SaaS vendor breakdown
Agent: Pulling all card transactions and bills with payment activity, then matching them by vendor...
> ramp transactions list --transactions_to_retrieve all_transactions_across_entire_business \
> --from_date 2026-01-01 --to_date 2026-04-01 --page_size 200 --include_count \
> --agent --rationale "List the user's transactions"
> ramp bills search --include_paid --from_payment_date 2026-01-01 \
> --to_payment_date 2026-04-01 --limit 50 --agent \
> --rationale "Search all bills paid during the user's requested period"
> # Paginate both searches to completion, then merge by vendor.
SaaS Vendor Spend Review (2026-01-01 to 2026-04-01):
Vendor Card Spend Bills Matched Complete Paid Total
─────────────────────────────────────────────────────────────────────
Brown Group $50,000.00 1 Requires payment-allocation data
Cochran Ltd $21,500.00 0 $21,500.00
Goody $9,119.13 2 Requires payment-allocation data
Morris-Allen $8,900.00 0 $8,900.00
Figma $24.00 1 Requires payment-allocation data
⚠ Vendor name variants detected:
"Delta Air Lines" (2 txns) + "Delta Airlines" (5 txns) — likely same vendor
Bill matches were filtered by `payment_date`, but their full invoice amounts were
not added to spend. Payment-allocation data is required to complete those totals.
```
## When NOT to Use
- **Verifying a single payment** — use `ramp-payment-lookup`
- **Approving transactions** — use `ramp-approval-dashboard`
- **Receipt or memo cleanup** — use `ramp-complete-expenses`
- **Detailed PO status** — run `ramp purchase_orders get` directly
## Gotchas
| Issue | Fix |
|---|---|
| Transaction amounts are strings (`"$1,048.25"`) | Strip `$` and `,` before summing. Handle `-$` prefix for refunds. |
| Bill amounts are numeric (dollars) | They are full invoice amounts. Do not sum them as paid-in-period spend without payment-allocation data. |
| `--transactions_to_retrieve` is required | Always include it. Use `all_transactions_across_entire_business` for company-wide analysis. |
| Search text must be ≥3 characters | "AI" won't work — use full vendor name |
| Paid-in-period bill activity | Pass `--from_payment_date` and `--to_payment_date` to `bills search`, and use the returned `payment_date`. Do not fall back to `due_date` or treat full invoice `amount` as the paid allocation. |
| Vendor name variants not normalized | Run multiple searches for known variants. Dedupe by `transaction_uuid`. |
| Pagination ceiling | `--page_size 200` is accepted. Still check `next_page_cursor`. |
| No unified spend endpoint | Query transactions and bills separately, then merge the results. |
| `--include_count` only on transactions | Bills search returns `total_found` automatically. |
| Something broken? | With the user's consent, run `ramp feedback ""`. This sends only that message to Ramp support; omit secrets and diagnostic artifacts. |
---
URL: https://agents.ramp.com/skills/ramp-spend-optimization
---
name: ramp-spend-optimization
area: Cards and Spend
supported_surfaces: [cli, mcp]
description: |-
Find credible potential savings and account-control opportunities across Ramp spend,
explain the evidence and confidence, and safely configure supported fund controls after
an exact preview and explicit approval. Use when: "optimize my Ramp account", "find
savings", "clean up recurring vendor spend", "consolidate vendor spend", "duplicate
subscriptions", "oversized funds", or "move recurring vendor spend onto a controlled
card". Do NOT use for a general spend report (use ramp-spend-analysis), expense cleanup
or missing receipts (use ramp-complete-expenses), approvals (use ramp-approval-dashboard),
subscription cancellation, or vendor-site payment changes.
---
# Spend Optimization
## Non-Negotiables
- **Pass `--rationale` on every command.** It is required on all agent-tool calls. Omitting it returns `HTTP 422`.
- **Pass `--agent`** for machine-readable JSON output on all CLI commands.
- **Read-only first.** The analysis phase requires no user confirmation. Action requires explicit approval of an exact preview.
- **Potential savings only.** Frame every dollar estimate as potential savings backed by observed charges. Never claim guaranteed savings, additional Ramp rewards, or vendor-negotiated discounts without an authoritative, account-specific source.
- **Missing products are normal.** If the account does not use Bill Pay or Procurement, continue with available card, fund, and transaction data. Do not treat missing products as setup problems or upsell requirements.
- **Separate currencies.** Do not add amounts across currencies unless an authoritative conversion method and conversion date are available and disclosed.
- **Account for refunds.** Negative transaction amounts are refunds. Subtract them from the vendor's observed spend rather than presenting them as separate savings.
- **Cleared spend only.** Base savings calculations on cleared or completed activity. Exclude declined, deleted, and draft items.
- **Paginate to completion.** A single page may not return everything. Repeat with `--cursor` / `cursor` using the next cursor value from the prior response until it is null.
- **Secure vendor-side handoff.** When directing the customer to update a vendor's payment method, tell them to retrieve the new virtual card credentials through the Ramp app or another secure first-party surface. Never ask the customer to paste full card details, OTPs, or credentials into this chat.
## Runtime Capability Check
Before starting, discover which tools are available:
1. **Analyst query** — `ramp analyst query` / `ExecuteAnalystQuery`. Required for account-wide spend correlation. If unavailable, explain that the account-wide optimization scan cannot be completed reliably and offer per-vendor analysis using transaction and bill listing instead.
2. **Fund listing** — `ramp funds list` / `GetUserFunds`. Required for fund-level analysis.
3. **Merchant search** — `ramp merchant search` / `SearchMerchants`. Required before any merchant-restriction write. If unavailable, merchant-restriction actions remain advisory.
4. **Fund creation** — `ramp funds create` / `IssueOneOffFunds`. Required for vendor-restricted fund creation. This tool does not accept an idempotency key; a retry after an ambiguous failure (timeout, network error) can create a duplicate fund and virtual card. See Safety Gate 1 for the required pre- and post-creation duplicate checks.
5. **Merchant restrictions** — `ramp funds update-merchant-restrictions` / `UpdateMerchantRestrictions`.
6. **Category restrictions** — `ramp funds update-category-restrictions` / `UpdateCategoryRestrictions`.
7. **Periodic limit increase** — `ramp funds edit` / `LimitIncrease`. Increase-only: sets a periodic limit to a higher positive value. Requires explicit user confirmation of the increase and choice of temporary vs permanent treatment. Cannot decrease a periodic limit; lowering a periodic limit is advisory/manual until a supported decrease action exists.
8. **Per-transaction limit** — `ramp funds update-transaction-amount-limit` / `UpdateTransactionAmountLimit`.
9. **Fund lock/unlock** — `ramp funds lock` / `LockOrUnlockSpendAllocation`.
If any write tool is unavailable, remain advisory for that action type and tell the user the exact manual steps.
## Analysis Workflow
### Step 1: Gather account-wide spend data
Use the analyst query to pull account-wide spend, grouping by vendor, payment route (card vs bill), and time period. Request at least the last six months of cleared activity.
Before writing any analyst SQL, call `ramp analyst catalog` / `GetAnalystCatalog` once to discover available tables and starter SQL. Then read domain docs for every physical `analyst.*` table that may appear in the query using `ramp analyst spend-docs` / `GetAnalystSpendFactsDomainDocs` and `ramp analyst table-docs` / `GetAnalystTableDomainDocs`. Do not generate SQL until documentation for all candidate tables has been read.
CLI:
```bash
ramp analyst query --sql "" \
--agent --rationale "Account-wide spend analysis for optimization scan"
```
MCP:
```text
ExecuteAnalystQuery(sql="", rationale="Account-wide spend analysis for optimization scan")
```
If analyst query is unavailable, fall back to paginated `ramp transactions list` and `ramp bills search` (see `ramp-spend-analysis` for exact pagination workflow). Explain that the analysis is limited to these data sources.
### Step 2: List existing funds
```bash
ramp funds list --funds_to_retrieve ALL_FUNDS \
--include_balance --include_lock_info --include_members --include_restrictions \
--page_size 100 --agent --rationale "List all funds with details for optimization review"
```
Paginate with `--cursor` using the next cursor from the prior response until it is null. Record each fund's UUID, display name, interval, limit, balance, lock state, merchant/category restrictions, and member list.
### Step 3: Cross-reference and rank opportunities
Apply the opportunity detection rules below. Rank findings by:
1. Credible potential savings (highest first).
2. Confidence level.
3. Customer impact or exposure reduction.
4. Actionability with available tools.
Return only the top three to five findings. Omit low-confidence findings unless the user explicitly requests a deeper scan.
## Opportunity Detection Rules
### Type 1: Potential Duplicate or Overlapping Recurring Spend
Look for repeated charges that plausibly represent spend the customer could eliminate.
**High confidence** — all of:
- Same normalized vendor name and currency.
- Charges occur within seven days of each other in each billing cycle.
- Amounts within 2% of each other.
- Pattern repeats across at least two billing cycles.
**Medium confidence** — any of:
- Amounts within 10% (but not 2%).
- Less precise billing-period alignment.
- Still requires at least two billing cycles.
- State the missing evidence that prevents high confidence.
Savings scope:
- **Observed overlap** — the suspicious charges already recorded. Count only the charges that plausibly disappear, never the customer's total vendor spend.
- **Forecast potential savings** — observed recurring overlap projected forward. Require at least three consistent cycles before annualizing.
State explicitly that separate subscriptions, teams, seats, products, and valid separate payment routes may be legitimate.
### Type 2: Bill-and-Card Fragmentation
The same vendor is paid through both bills and card transactions.
This is an account-optimization opportunity. Do not assign a dollar-savings value unless a separate authoritative source supports that amount. Recommend consolidating future payments under one owner and one controlled payment route.
### Type 3: Recurring Vendor Spend Without Dedicated Controls
Recurring vendor charges use a general-purpose card or fund with no vendor-restricted fund evident.
Recommend a dedicated vendor fund with an appropriate periodic limit, merchant restriction, owner, and per-transaction cap. Frame this as reducing exposure and improving ownership clarity, not as direct savings.
### Type 4: Dormant or Oversized Recurring Funds
**Dormant:** A recurring monthly fund is dormant when:
- It is active (not locked or terminated).
- It was created at least 90 days ago.
- It has no cleared spend during the most recent 90 days.
Do not classify one-time or newly created funds as dormant in V1.
**Oversized:** A recurring monthly fund is oversized when:
- At least three complete months of history exist.
- The current monthly limit is more than twice the highest cleared monthly spend during those months.
Suggested new limit: the highest observed monthly spend plus a 20% buffer. If history is insufficient, seasonal, or irregular, ask the user to choose the amount rather than presenting a false precision. Lowering the periodic fund limit is not supported by the available tools; recommend the target limit and tell the user to apply it through the Ramp dashboard. If the per-transaction cap is also oversized, that can be lowered via `UpdateTransactionAmountLimit`.
## Response Contract
Every numbered finding must include:
1. **Finding** — what was detected and the opportunity type.
2. **Confidence** — `High` or `Medium`, with the reason.
3. **Evidence window** — the date range and spend routes examined.
4. **Observed overlap** — when applicable, the charges that represent the potential waste.
5. **Forecast potential savings** — when supportable (at least three consistent cycles), the annualized projection.
6. **Calculation and assumptions** — how the dollar figures were derived.
7. **Recommended next action** — what the agent can do and what remains manual.
8. **What this will not do** — explicit limitations.
9. **Remaining customer/vendor-side work** — steps the agent cannot perform.
### Follow-up commands
- `explain ` — show the underlying dates, amounts, payment routes, fund ownership, and limits.
- `fix ` — for duplicate spend, explain the suspected overlap and provide vendor-side review/cancellation guidance. For fund actions, proceed to the preview and approval workflow. "Fix" never means the agent canceled a subscription.
- `ignore ` — acknowledge and skip the finding.
## Supported Actions
The following writes are available when the connected user is authorized and the live tool exists:
| Action | CLI | MCP |
|---|---|---|
| Create a vendor-restricted fund | `ramp funds create` | `IssueOneOffFunds` |
| Add merchant restrictions to a fund | `ramp funds update-merchant-restrictions` | `UpdateMerchantRestrictions` |
| Add category restrictions to a fund | `ramp funds update-category-restrictions` | `UpdateCategoryRestrictions` |
| Increase a periodic fund limit | `ramp funds edit` | `LimitIncrease` |
| Set a per-transaction limit | `ramp funds update-transaction-amount-limit` | `UpdateTransactionAmountLimit` |
| Lock or unlock a fund | `ramp funds lock` | `LockOrUnlockSpendAllocation` |
**Not supported in V1:** subscription cancellation, vendor-site payment updates, fund deletion or termination, periodic fund-limit decreases, fund interval changes, Procurement configuration, purchase-order cleanup, reimbursement actions, or approval actions.
Removing restrictions and unlocking funds are rollback/restoration actions only, not optimization recommendations. They require a full preview and separate explicit approval.
## Safety Gates
Before previewing any action:
1. **Duplicate fund check and creation safety.** Before proposing fund creation, resolve the target merchant via `SearchMerchants` to obtain both its integer `id` and `uuid`. Then use the full fund listing from Step 2 (which includes `--include_restrictions`) and compare the merchant's integer `id` against every existing fund's `vendor_whitelist` entries — these are integer merchant IDs, not UUIDs. Do not rely on display-name search alone — a fund named "Marketing subscriptions" with Asana-only restrictions would be missed by a name filter. If any existing fund already restricts to the target merchant ID, do not propose a new fund; recommend using the existing one instead.
**Retry safety:** `IssueOneOffFunds` does not accept an idempotency key. If a creation call returns an error or times out without a confirmed fund UUID, do **not** retry blindly. Instead, re-list all funds with `--include_restrictions` and check whether a fund with the target merchant ID in its `vendor_whitelist` was created. If a duplicate is found, report both funds and remain advisory for cleanup since fund deletion is not supported. Only retry creation after confirming the first attempt did not persist.
2. **Merchant resolution.** Before any merchant-restriction write, resolve the merchant using `ramp merchant search` / `SearchMerchants`. The result returns both `id` (integer) and `uuid` (string) per merchant. Use the integer `id` when comparing against existing fund `vendor_whitelist`/`vendor_blacklist` restrictions (which contain integer merchant IDs). Use the `uuid` when calling `IssueOneOffFunds` or `UpdateMerchantRestrictions` (which accept merchant UUIDs). If reliable merchant lookup is unavailable, remain advisory.
3. **Pre-lock/unlock checks.** Both locking and unlocking a fund require an exact preview and explicit user approval. Before presenting a lock or unlock preview, show the exact fund and all affected members.
- **For locking:** explain that locking will block all transactions on the fund until it is unlocked. Before presenting the lock preview, verify:
- **Pending transactions.** Query pending charges on the fund using `ramp transactions list` / `GetTransactions` with `--transactions_to_retrieve all_transactions_across_entire_business`, `--state pending`, and a `spend_allocation` filter for the target fund. Paginate to completion. If any pending charges exist, report them and remain advisory.
- **Annual or irregular renewals.** Review at least 13 months of cleared transaction history on the fund to identify annual or irregular charges that would be disrupted by locking. If renewal evidence cannot be established from available data, remain advisory and ask the user about known renewals.
- Which members are affected.
- A supported recovery path exists (unlock via the same tools, with its own approval).
- If pending activity or renewal evidence cannot be established, remain advisory and explain the risk.
- **For unlocking:** explain that unlocking will re-enable spending for all fund members. Show the fund's current lock state, restrictions, limit, and member list. Require explicit user approval before proceeding. Unlock is a rollback/restoration action only — do not recommend it as an optimization.
4. **Periodic limit increase protocol.** `LimitIncrease` is an increase-only tool. Before calling it:
- Preview the exact fund, current limit, and proposed new limit.
- Ask the user to explicitly confirm the increase.
- Ask the user to choose `temporary` (resets at the next interval boundary) or `permanent`.
- Pass `user_confirmed_limit_increase: true` and `user_confirmed_increase_type: true` only after both confirmations.
- Lowering a periodic fund limit is not supported by this tool. For oversized funds, recommend the lower limit and tell the user to apply it manually through the Ramp dashboard.
5. **Limit calculation.** Use observed peak recurring usage plus a 20% buffer only when at least three representative months exist. For seasonal or irregular spend, ask the user to choose the limit.
## Preview, Approval, and Verification
### Preview
Every write preview must show:
- **Account object** — exact fund name, ID, owner, and affected members.
- **Proposed values** — amount, interval, merchant/category restrictions, per-transaction cap.
- **Calculation** — how the values were derived from observed data.
- **Expected improvement** — what changes after the action.
- **Limitations** — what the action will not do.
- **Vendor-side steps** — any remaining customer/vendor work.
- **Reversibility** — whether the change is reversible with currently available tools, and the exact recovery action.
### Approval
- `Fix it` or `do it` is **not** approval until exact values have been previewed.
- Any change to the owner, member list, amount, interval treatment (temporary vs permanent), restriction, or cap requires a revised preview and new approval.
- The user must confirm the exact previewed values before the agent acts.
### Pre-action re-read
Immediately before acting, re-read the affected account state using the exact fund UUID:
```bash
ramp funds list --fund_uuid "" \
--include_balance --include_lock_info --include_members --include_restrictions \
--agent --rationale "Pre-action state check before approved change"
```
For fund creation, re-read the full fund list with `--include_restrictions` and compare the target merchant's integer `id` against all existing `vendor_whitelist` entries to confirm no matching vendor-restricted fund was created since the preview.
For lock actions, also repeat the pending-transaction query immediately before locking:
```bash
ramp transactions list \
--transactions_to_retrieve all_transactions_across_entire_business \
--state pending --filters '{"filters": [{"filter": "", "filter_type": "spend_allocation"}]}' \
--agent --rationale "Pre-lock pending-transaction recheck"
```
If any pending charges appeared since the initial check, **stop** and issue a revised preview.
If the owner, members, limits, restrictions, or other previewed values differ from the preview, **stop** and issue a revised preview. Do not apply a stale preview.
### Execution
Execute only the approved action(s), in the previewed order. When a preview contains multiple ordered steps (for example, fund creation followed by per-transaction cap), verify after each step and stop with `Failed or partial change` if any step fails.
### Post-action verification
After acting, read the resulting state using the exact fund UUID:
```bash
ramp funds list --fund_uuid "" \
--include_balance --include_lock_info --include_members --include_restrictions \
--agent --rationale "Post-action verification of approved change"
```
Compare the result to the approved preview. A successful write response alone is not sufficient proof. The verified state — including restrictions and per-transaction cap — must match. If the write response indicates the change is pending internal Ramp approval (e.g. `request_pending`), the fund will correctly retain its old values until approved — this is not a failure; see the `Pending Ramp approval` completion state.
### Completion states
Report exactly one:
- **Ramp configured** — the approved Ramp control exists and is verified. Customer or vendor-side work remains.
- **Vendor migration pending** — the new Ramp route is ready. The vendor payment method has not been confirmed.
- **Optimization complete** — the Ramp control is verified and every required external step has evidence of completion.
- **Pending Ramp approval** — the write was accepted but requires internal Ramp approval before taking effect. The fund retains its current values until the request is approved. Report the pending request and do not treat the old values as a verification failure.
- **Failed or partial change** — the resulting state does not fully match the approved preview and no pending-approval request explains the difference. Report the exact resulting state.
Do not report the account as optimized while an external migration remains incomplete.
## Example Session
```
User: Optimize my Ramp account.
Agent: Let me scan your account for optimization opportunities.
> ramp analyst catalog --agent --rationale "Discover analyst tables for optimization scan"
> ramp analyst spend-docs --agent --rationale "Read spend facts domain docs"
> ramp analyst query --sql "..." --agent --rationale "Account-wide spend analysis for optimization scan"
> ramp funds list --funds_to_retrieve ALL_FUNDS \
> --include_balance --include_lock_info --include_members --include_restrictions \
> --page_size 100 --agent --rationale "List all funds with details for optimization review"
> [paginate with --cursor until null]
I found 4 opportunities, ordered by potential impact:
1. **Recurring vendor spend without dedicated controls — Asana**
Confidence: High
Evidence: 6 months of monthly charges averaging $12,100 on a general-purpose
Marketing card. No vendor-restricted Asana fund exists.
Recommended action: Create a dedicated Asana fund with a $15,000 monthly limit
(peak month of $12,500 + 20% buffer), Asana merchant restriction, a single
owner, and a $15,000 per-transaction cap.
This will not: update the payment method in Asana, cancel any existing payment
route, or delete the fund if created in error.
Remaining work: After fund creation, retrieve the new virtual card credentials
through the Ramp app and update the payment method inside Asana. Confirm a
successful charge before cleaning up the old payment route.
2. **Potential duplicate recurring spend — Zoom**
Confidence: High
Evidence: Two Zoom charges of $499.00 and $499.00 on different cards, 3 days
apart, same currency, repeating across 4 billing cycles.
Observed overlap: $499.00/month × 4 months = $1,996.00
Forecast potential savings: ~$5,988/year (projected from 4 consistent cycles)
Calculation: One of the two charges assumed eliminable. Annualized from monthly
cadence. Separate subscriptions, teams, or products may justify both charges.
Recommended action: Review both Zoom subscriptions with the respective
cardholders. If one is redundant, cancel it through the Zoom admin portal. The
agent cannot cancel Zoom subscriptions.
Remaining work: Customer must verify which subscription is redundant and cancel
it directly with Zoom.
3. ...
User: fix 1
Agent: I'll create a vendor-restricted Asana fund. First, let me resolve the
Asana merchant and confirm no existing fund already restricts to this merchant:
> ramp merchant search --query "Asana" --agent --rationale "Resolve Asana merchant UUID for fund restriction"
Merchant resolved: id=42, uuid=abc123-...
Checking all existing funds from the initial scan — comparing merchant integer
ID 42 against every fund's vendor_whitelist entries.
No existing fund has merchant ID 42 in its vendor_whitelist.
**Proposed Ramp changes (2 ordered steps)**
Step 1 — Create a new fund:
- Display name: Asana
- Owner: Sarah Chen (current cardholder for Asana charges)
- Monthly limit: $15,000 (peak recent month $12,500 + 20% buffer)
- Merchant restriction: Asana only (merchant ID 42 / UUID abc123-...)
- Interval: Monthly
Step 2 — Set the per-transaction cap on the new fund:
- Per-transaction cap: $15,000
This gives Asana one owner and one controlled payment route. It does not:
- Update the payment method inside Asana
- Cancel existing bills or card routes
- Delete the newly created fund (fund deletion is not currently supported)
After both steps, retrieve the new virtual card credentials through the Ramp
app or another secure first-party surface. Do not paste full card details into
this chat. Update the payment method in Asana and confirm a successful charge
before cleaning up the old route.
This change is partially reversible: the fund can be locked but not deleted.
Approve creating this fund and setting the per-transaction cap exactly as
described?
User: Yes, approved.
Agent: Confirmed. Re-reading current state before acting...
> ramp funds list --funds_to_retrieve ALL_FUNDS --include_restrictions \
> --agent --rationale "Pre-action state check — verify no fund with Asana merchant restriction was created"
No fund has merchant ID 42 in its vendor_whitelist. Creating the fund.
Step 1: Create fund.
> ramp funds create --display_name "Asana" --amount 15000 --interval MONTHLY \
> --user_ids '["sarah-chen-uuid"]' --vendor_whitelist '["asana-merchant-uuid"]' \
> --agent --rationale "Create vendor-restricted Asana fund per user approval"
Verifying step 1...
> ramp funds list --fund_uuid "" \
> --include_balance --include_members --include_restrictions \
> --agent --rationale "Verify created Asana fund"
Fund created and verified. Applying step 2.
Step 2: Set per-transaction cap.
> ramp funds update-transaction-amount-limit \
> --spend_allocation_id "" --new_transaction_amount_limit 15000 \
> --agent --rationale "Set $15,000 per-transaction cap on Asana fund per user approval"
Verifying step 2...
> ramp funds list --fund_uuid "" \
> --include_balance --include_members --include_restrictions \
> --agent --rationale "Verify per-transaction cap on Asana fund"
**Ramp configured.** The Asana fund is live with a $15,000 monthly limit,
$15,000 per-transaction cap, and Asana-only merchant restriction.
Vendor migration pending — retrieve the new virtual card credentials through
the Ramp app, update the payment method in Asana, and confirm a successful
charge before cleaning up the old payment route.
```
## Gotchas
| Issue | Fix |
|---|---|
| Duplicate vendor names | The API does not normalize merchant names. Run `ramp merchant search` and check for variants ("Asana", "Asana Inc", "Asana, Inc."). Deduplicate by merchant integer ID. |
| Refunds reduce spend | Negative transaction amounts are refunds. Subtract from vendor totals; do not count as separate savings. |
| Multiple currencies | Keep currency totals separate. Do not combine USD and EUR spend. Disclose conversions when used. |
| Incomplete Bill Pay data | Bill `amount` is the full invoice amount, not the paid allocation. Do not sum bill amounts as paid-in-period spend without payment-allocation data. |
| Missing merchant lookup | If `ramp merchant search` is unavailable, merchant-restriction actions remain advisory. Provide the manual steps. |
| Stale previews | Always re-read state immediately before acting. If anything changed, stop and re-preview. |
| Annual or irregular renewals | Before locking a fund, review at least 13 months of cleared history for annual charges. Query pending transactions with `--transactions_to_retrieve all_transactions_across_entire_business --state pending` and a `spend_allocation` filter scoped to the fund. Repeat the pending-transaction query immediately before locking. If either check cannot be completed, remain advisory. |
| Duplicate vendor funds | Before creating a fund, compare the resolved target merchant's integer `id` against every existing fund's `vendor_whitelist` entries (which are integer merchant IDs). Do not compare UUIDs against this field — the types are incompatible. |
| Fund creation retries | `IssueOneOffFunds` has no idempotency key. If a creation call errors or times out, do not retry. Re-list funds with `--include_restrictions` and check whether the target merchant ID already appears in any `vendor_whitelist` before attempting again. |
| Partial writes | If any step in a multi-step approved action fails, stop and report `Failed or partial change` with the exact resulting state. Do not claim the per-transaction cap exists if step 2 failed. |
| Pending Ramp approval | Some writes (e.g. `UpdateTransactionAmountLimit`, `LimitIncrease`) can return `request_pending` when the change requires internal Ramp approval. The fund retains its old values until approved — this is not a failure. Report `Pending Ramp approval` and do not retry. |
| Unverified vendor migration | Do not report optimization as complete while the vendor payment method has not been confirmed on the new route. |
| `LimitIncrease` is increase-only | This tool cannot decrease a periodic fund limit. For oversized funds, recommend the target limit and tell the user to apply it manually. Pass `user_confirmed_limit_increase` and `user_confirmed_increase_type` only after explicit user confirmation of the increase and temporary/permanent choice. |
| Merchant ID vs UUID | `SearchMerchants` returns both `id` (integer) and `uuid` (string). Fund `vendor_whitelist`/`vendor_blacklist` restrictions use integer IDs. `IssueOneOffFunds` and `UpdateMerchantRestrictions` accept UUIDs. Use the correct type for each operation. |
| Per-transaction vs periodic limit | `UpdateTransactionAmountLimit` sets the per-transaction cap (supports increases and decreases). `LimitIncrease` sets the periodic total (increases only). `default_member_limit_amount` on fund creation is a per-member limit, not a per-transaction cap. Do not confuse the three. |
| Something broken? | With the user's consent, run `ramp feedback ""`. This sends only that message to Ramp support; omit secrets and diagnostic artifacts. |
## When NOT to Use
- **General spend report** — use `ramp-spend-analysis`
- **Missing receipts or memos** — use `ramp-complete-expenses`
- **Approving transactions** — use `ramp-approval-dashboard`
- **Card lock/unlock or status** — use `ramp-card-management`
- **Cancel a subscription** — unsupported vendor-side action; the agent cannot do this
- **Set up Procurement** — use `ramp-manage-procurement`
- **Verifying a single payment** — use `ramp-payment-lookup`
---
URL: https://agents.ramp.com/skills/ramp-submit-procurement-request
---
name: ramp-submit-procurement-request
area: Procurement
supported_surfaces: [cli, mcp]
description: |-
Create, complete, and submit new, existing draft, or approved-PO change-request
Ramp procurement requests. Use when: 'submit a procurement request', 'start a
purchase request', 'continue my draft', 'request to buy', 'create a PO
request', 'change an approved PO', or 'submit a software purchase'. Do NOT use
to track or approve a submitted request (use ramp-manage-procurement), or for
reimbursements, bills, card expenses, vendor onboarding documents, or
contract changes not tied to an approved Ramp PO.
---
# Submit Procurement Request
Guide the user conversationally through the pre-submission request lifecycle.
Use `ramp procurement_requests` for the draft itself. Resolve CLI flags, field
IDs, and object IDs without asking the user to understand them.
## Rules
- Run commands with `--agent` and pass `--rationale` every time. With `--json`,
put `rationale` in the JSON body. Omitting it can return
`HTTP 422 (DEVELOPER_INVALID_SCHEMA)`.
- CLI flags use snake_case, such as `--spend_request_uuid` and `--page_size`.
- After every `draft` or `get` call, replace prior form state with the returned
top-level `draft_state` and, for change requests, `change_request_state`;
conditional questions may change.
- Treat `draft_state.fields` as the current visibility-based write allowlist for
`answers` and `clear_field_ids`. Never target a field absent from `fields`.
- Treat `change_request_state.fields` as a separate write allowlist for
`change_request_answers` and `clear_change_request_field_ids`. Never put a
change-request form answer in `answers`, or a normal draft answer in
`change_request_answers`.
- Treat `fields_to_answer` as the prioritized subset of visible fields that are
missing required values or have field-level validation errors. Optional and
already-answered visible fields may still be intentionally updated.
- Build each answer from the visible field's `answer_template`;
`{field_id, value}` alone is not a valid generic shape.
- Never invent spend intent UUIDs, field IDs, choices, lookup IDs, or file
UUIDs.
- Ask the user for visible-field values that cannot be resolved from their
prompt, provided files, or a lookup. Clearly label optional questions and
say they may be skipped; batch related questions when practical.
- Submit only when every returned form state is `ready_to_submit`, the user has
seen the current summary and any `change_request_diff`, and they explicitly
confirm submission.
## Workflow
```text
choose spend intent or approved PO -> confirm with user -> create/resume draft -> fill current fields
-> attach relevant files -> revalidate -> review with user -> confirm -> submit
```
## Create or Resume a Draft
For a new request, list spend intents first:
```bash
ramp procurement_requests spend-intents --page_size 50 --rationale "List procurement spend programs before drafting" --agent
```
Choose only from returned spend intent UUIDs. Match the returned name,
description, status, and outcome type to the user's request. If more than one
program fits materially, ask the user to choose.
**Creating a draft is a write** — it creates a persistent procurement draft
under the selected program. Before running it, show the user the program you
selected (name and description) and confirm they want a draft created there;
selecting a spend intent is not itself consent to create one. Then:
```bash
ramp procurement_requests draft --spend_intent_uuid "" --rationale "User confirmed drafting a procurement request under the selected spend program" --agent
```
For an existing draft, fetch its current state before editing:
```bash
ramp procurement_requests get "" --rationale "Fetch the current procurement draft" --agent
```
Do not reuse field IDs or visibility from an earlier response or conversation.
For a change request, first use `ramp requests search` and `ramp requests get` to
identify and review the original approved purchase-order request. Use its
`spend_request_uuid` as `existing_spend_request_uuid`; do not use the unified
`unified_request_id` or `purchase_order_id` in its place. Confirm with the user
which approved purchase order the change request should be drafted from — same
confirm-before-draft rule as above — then create the linked child with
```bash
ramp procurement_requests draft --existing_spend_request_uuid "" --rationale "Create a change-request draft from the approved purchase order" --agent
```
`existing_spend_request_uuid` and `spend_intent_uuid` are mutually exclusive.
After creation, continue the returned child by passing only its
`spend_request_uuid`; do not resend `existing_spend_request_uuid`.
## Fill and Revalidate the Draft
Read the top-level `draft` summary plus `draft_state.status`,
`draft_state.fields`, `draft_state.fields_to_answer`, and
`draft_state.validation_errors`. Start with the IDs in `fields_to_answer`: they
are the visible fields that are missing required values or have field-level
validation errors. The broader `fields` list is the write boundary. Optional or
already-answered fields absent from `fields_to_answer` may be updated when the
user intends to change them or their value is supported by the user's request.
For a change request, inspect `change_request_state.status`, fields,
`fields_to_answer`, and validation errors independently. Build normal form
answers from `draft_state.fields` and send them in `answers`. Build change-request
form answers from `change_request_state.fields` and send them in
`change_request_answers`.
For the current visible fields, prioritizing `fields_to_answer`:
| Situation | Action |
|---|---|
| Required and known | Fill it |
| Required and unknown | Ask the user |
| Optional and supported by user information, a file, or a lookup | Fill it |
| Optional but unknown or weakly implied | Ask; label it optional and offer to skip it |
| Ambiguous and material | Ask one concise question |
Batch known standard fields and form answers into as few updates as practical:
```bash
ramp procurement_requests draft --json '{
"spend_request_uuid": "",
"rationale": "Populate current procurement request fields",
"request_name": "",
"currency": "USD",
"line_items": [
{"description": "- ", "amount": "1000.00"}
],
"answers": [
{"answer_type": "text", "field_id": "
", "value": ""}
]
}' --agent
```
Update semantics:
- Omit `line_items` to preserve them. Send the complete desired list to replace
them, or `[]` to clear them. `line_items: null` is invalid.
- For a change request, begin with the complete returned line-item list, preserve
every item UUID and all unchanged properties, modify only the user-requested
values, and send that full list. `line_items` is full replacement, not a
patch; sending only a changed item deletes omitted items.
- A matching line-item UUID preserves existing tracking categories, custom
fields, withholding rates, and external IDs that are not editable input. Do
not remove and recreate a line merely to change its amount.
- Omit `currency` to preserve it. `currency: null` is invalid.
- Use `clear_field_ids` to clear current visible custom-form answers; do not
send null answer values.
- Use `clear_change_request_field_ids` only for visible change-request fields;
do not send null `change_request_answers`.
After every update, replace your prior field state with the returned
`draft_state`:
- `needs_input`: resolve `fields_to_answer`, then fill supported optional visible
fields when useful.
- `has_validation_errors`: fix the returned validation errors.
- New or removed fields: rebuild the visible write set from `fields`, then
prioritize the new `fields_to_answer` subset.
- `ready_to_submit`: stop editing and review the request with the user.
Apply the same transition rules independently to a non-null
`change_request_state`. After each change-request update, inspect
`draft.original_request` and `draft.change_request_diff`. `original_request` is the
approved purchase-order request being changed and must match the user's intended
PO. The diff must show only the intended old and new values. Stop on an empty or
unexpected diff instead of submitting.
`LINE_ITEM` entries in `change_request_diff` are sparse and include only changed
values. Overlay a present `new_value` on `old_value`: a
null property means unchanged, while the whole `new_value` is null when the line
was removed. Do not report omitted or null properties as changes.
A visible form field may ask whether the purchase is new or a renewal; answer it
when known. Do not use this workflow to amend or renew a contract outside a
currently visible draft field.
## Resolve Lookup Fields
Run a lookup only when a current visible field requires an existing Ramp object
ID. Prefer that field's `lookup` metadata and `answer_template` over these
examples:
```bash
# Vendor
ramp vendors search --search_term "" --limit 10 --rationale "Find the vendor UUID for the procurement request" --agent
# Department
ramp business departments --name_filter "" --rationale "Find the department UUID for the procurement request" --agent
# Merchant
ramp merchant search --query "" --limit 10 --rationale "Find the merchant ID for the procurement request" --agent
# Merchant category
ramp merchant categories --rationale "List merchant category IDs for the procurement request" --agent
```
Use IDs returned by the lookup; never derive them from display labels.
Custom-form answers are typed objects. Copy the visible field's
`answer_template` and replace its placeholders. Representative shapes:
```json
{"answer_type":"text","field_id":"","value":"Business justification"}
{"answer_type":"vendor","field_id":"","payee_uuid":""}
{"answer_type":"merchant_category","field_id":"","category_ids":[456]}
{"answer_type":"file_upload","field_id":"","file_uuids":[""]}
```
For text-like fields such as `paragraph`, `email`, `link`, and `date`, preserve
the field's exact `answer_type`. For complex fields such as `address` and
`contact`, fill the exact properties returned in `answer_template`.
## Attach Files
If the user provides a file, first inspect the current visible fields. Upload it
only when a visible file field's label or help text matches the file's purpose,
such as a contract, quote, invoice, or supporting document. An already-answered
visible file field may accept an intentional additional or replacement file.
`procurement_requests upload-file` is CLI-only, even though this skill also
supports MCP. MCP callers must not attempt this command; hand off the file
attachment step to a CLI-capable caller, then resume with the refreshed draft
state after the attachment is applied.
```bash
ramp procurement_requests upload-file "" "" --file "/absolute/path/to/file.pdf" --rationale "Upload the document requested by the procurement form" --agent
```
Before uploading, retain whether the selected `field_id` came from
`draft_state.fields` or `change_request_state.fields`. Field IDs identify their
form context. Use the returned `answer` object without rebuilding it, and place
it back into the same form collection:
| Selected field collection | Draft update property |
|---|---|
| `draft_state.fields` | `answers` |
| `change_request_state.fields` | `change_request_answers` |
For a request-form upload:
```bash
ramp procurement_requests draft --json '{
"spend_request_uuid": "",
"rationale": "Attach the uploaded document to its procurement form field",
"answers": [
]
}' --agent
```
For a change-request-form upload:
```bash
ramp procurement_requests draft --json '{
"spend_request_uuid": "",
"rationale": "Attach the uploaded document to its change-request form field",
"change_request_answers": [
]
}' --agent
```
Do not upload a file when no current visible file field matches it, and do not
attach it to an unrelated field merely because that field is visible.
## Review and Submit
When the status is `ready_to_submit`, summarize the current `draft`, answered
form fields, and attached files in user language:
```text
I drafted this procurement request:
Spend program: Software
Request: Figma annual subscription
Vendor: Figma
Amount: USD 90,000.00
Line items:
- Figma Enterprise, 100 seats: USD 90,000.00
Filled form details:
- Business justification: Design collaboration
Attached files:
- figma-agreement.pdf
Should I submit it?
```
A confirmation given before the user sees this current summary does not count.
For a change request, also show the approved PO request from `original_request`
and every old/new value in `change_request_diff`. Ask the user to explicitly confirm
that exact diff. Submit only after they confirm the summarized request:
```bash
ramp procurement_requests submit "" --confirmed --rationale "Submit the confirmed procurement request" --agent
```
Report the returned spend request UUID, status, and submission time. Include a
web path only when the response returns one.
## Stop and Ask
Stop instead of guessing when:
- More than one spend intent materially fits.
- A required field in `fields_to_answer` lacks a known value.
- A lookup-backed field cannot be resolved.
- The user supplied a file but no current field matches it.
- Validation errors remain after correcting the supplied values.
- `original_request` does not match the intended original approved request.
- A change-request diff is empty, incomplete, or contains an unintended change.
For `HTTP 422`, first verify that `rationale` is present and the request matches
the current field templates.
---
URL: https://agents.ramp.com/skills/ramp-submit-reimbursement
---
name: ramp-submit-reimbursement
area: Reimbursements
supported_surfaces: [cli, mcp]
description: |-
Submit an out-of-pocket reimbursement from a receipt. Use when: 'submit reimbursement',
'reimburse me', 'I paid out of pocket', 'expense a receipt', 'file reimbursement',
'OOP expense', 'I bought something for work'. Do NOT use for: approving reimbursements
(use ramp-approval-dashboard), uploading receipts to card transactions (use ramp-complete-expenses),
or editing existing reimbursements.
---
## Non-Negotiables
- **Pass `--rationale` on every command** — it is a required field on these agent-tools (a non-empty string, max 1024 chars). With `--json`, supply it as a `"rationale"` key in the body. Omitting it returns `HTTP 422 (DEVELOPER_INVALID_SCHEMA)`, in both agent and human modes.
- Never submit without confirming the details with the user first. Show amount and currency, merchant, memo, fund, and accounting categories before submitting.
- Receipts must be **base64-encoded** for upload. Accepted types: PNG, JPEG, PDF, HEIC, WEBP.
- All CLI flags use **underscores**, not hyphens (e.g., `--fund_uuid`, `--page_size`).
- Reimbursement amounts use major units in the draft's existing currency (for example, `42.50` means EUR 42.50 for a EUR draft, not cents). An amount-only edit preserves the existing currency.
- After editing, always check `missing_items` in the response. Do not submit until all required items are resolved.
## Workflow
### Step 1: Upload the receipt
The user must provide a receipt file. Base64-encode and upload it:
```bash
# Encode the file
base64 -i /path/to/receipt.pdf | tr -d '\n'
# Upload (no --transaction_uuid — this is for a new reimbursement, not a card transaction)
ramp receipts upload \
--content_type "application/pdf" \
--filename "receipt.pdf" \
--file_content_base64 "{base64_string}" --rationale "Upload the receipt"
```
Response returns `receipt_uuid`. Save it for the next step.
### Step 2: Create a draft reimbursement from the receipt
```bash
ramp reimbursements create {receipt_uuid} --rationale "Create the reimbursement from the receipt"
```
Response returns:
- `reimbursement_uuid` — the draft reimbursement ID
- `suggested_memos` — AI-generated memo suggestions based on the receipt
- `suggested_codings` — suggested accounting category selections
- `suggested_funds` — suggested spend allocations to charge
- `missing_items` — what still needs to be filled in before submission
- `reimbursement_link` — direct link to view in the Ramp app
### Step 3: Fill in required fields
Check `missing_items` from the create response. Common required fields:
| Missing item | How to fill |
|---|---|
| `missing_memo: true` | Edit with `--memo` |
| `missing_fund: true` | Edit with `--fund_uuid` (pick from `suggested_funds` or list funds) |
| `missing_tracking_categories` (non-empty) | Edit with `--json` to set category selections |
| `missing_receipt: true` | Should not happen if you created from a receipt |
Use suggestions from the create response when available:
```bash
# Set memo and fund from suggestions
ramp reimbursements edit {reimbursement_uuid} \
--memo "Coffee with client" \
--fund_uuid "{suggested_fund_uuid}" --rationale "Update the reimbursement for the user"
```
If the receipt amount is wrong, correct it while the reimbursement is still in `DRAFT`. Use the draft's existing currency shown in the tool response:
```bash
# For a draft whose existing currency is EUR
ramp reimbursements edit {reimbursement_uuid} \
--amount 42.50 --rationale "Correct the reimbursement amount for the user"
```
Amounts must be positive values in the existing draft currency; `--amount` alone does not change the currency. After editing, check `missing_items` as usual and show both the corrected amount and currency in the final confirmation before submitting.
For tracking categories, use `--json`:
```bash
ramp reimbursements edit {reimbursement_uuid} --json '{
"rationale": "Update the reimbursement for the user",
"reimbursement_uuid": "{uuid}",
"tracking_category_selections": [
{
"category_uuid": "{category_uuid}",
"option_uuid": "{option_uuid}"
}
]
}'
```
If no suggestions are available for the fund, list the user's funds:
```bash
ramp funds list --agent --rationale "List the user's funds"
```
**After each edit**, check the response's `missing_items`. Repeat until all required items are resolved (all `false` / empty).
### Step 4: Confirm with the user
Present the complete reimbursement before submitting:
```
Ready to submit:
Amount: USD 42.50
Merchant: Blue Bottle Coffee
Memo: Coffee with client — discussed Q2 roadmap
Fund: Social bonding & Team outings 2026
Category: 70103 - Company Meals
Receipt: attached
Submit for approval?
```
### Step 5: Submit
```bash
ramp reimbursements submit {reimbursement_uuid} --rationale "Submit the reimbursement"
```
Response returns `reimbursement_uuid` and `error_message` (null on success).
After submitting, tell the user:
- The reimbursement has been submitted for approval
- Link to view it: the `reimbursement_link` from the create/edit response
- They'll be notified when it's approved
## Editing After Rejection
If a reimbursement was rejected and the user wants to fix and resubmit:
```bash
# Revert to draft
ramp reimbursements resubmit {reimbursement_uuid} --rationale "Resubmit the reimbursement"
# Edit as needed
ramp reimbursements edit {reimbursement_uuid} --memo "Updated memo with details" --rationale "Update the reimbursement for the user"
# Submit again
ramp reimbursements submit {reimbursement_uuid} --rationale "Submit the reimbursement"
```
## Duplicating a Previous Reimbursement
`ramp reimbursements duplicate` exists, but it is not a complete CLI path for a new receipt-backed reimbursement.
The duplicate keeps the prior coding context, but it does **not** copy the receipt, and the CLI cannot attach a new receipt to that duplicated draft afterward.
If the user needs to submit a new reimbursement with a new receipt, the executable CLI path is:
```bash
# Find a previous reimbursement to reference
ramp reimbursements list --reimbursements_to_retrieve my_reimbursements --page_size 10 --rationale "List the user's reimbursements"
# Duplicate exists, but the draft will still be missing a receipt
ramp reimbursements duplicate {previous_reimbursement_uuid} --rationale "Duplicate the reimbursement"
# Upload the new receipt
ramp receipts upload --content_type "image/jpeg" --filename "gym-apr.jpg" \
--file_content_base64 "{base64}" --rationale "Upload the receipt"
# Create a fresh reimbursement from that receipt
ramp reimbursements create {receipt_uuid} --rationale "Create the reimbursement from the receipt"
# Re-apply memo, fund, and tracking categories, then submit
ramp reimbursements edit {reimbursement_uuid} --memo "Monthly gym membership" --rationale "Update the reimbursement for the user"
ramp reimbursements submit {reimbursement_uuid} --rationale "Submit the reimbursement"
```
Do not imply that a new receipt can be attached to the duplicated reimbursement via CLI. `receipts attach` is for card transactions only, and `submit` can fail with "receipt must be specified" if the duplicate has no receipt.
## Example Session
```
User: I bought coffee for a meeting yesterday, here's the receipt ~/Downloads/bluebottle.jpg
Agent: Let me upload that receipt and create a reimbursement.
> base64 -i ~/Downloads/bluebottle.jpg | tr -d '\n'
> ramp receipts upload --content_type "image/jpeg" --filename "bluebottle.jpg" \
> --file_content_base64 "{base64}" --rationale "Upload the receipt"
Receipt uploaded (receipt_uuid: ``).
> ramp reimbursements create --rationale "Create the reimbursement from the receipt"
Draft created. The receipt shows Blue Bottle Coffee, $12.50.
Suggested memos: "Coffee at Blue Bottle for team meeting"
Suggested fund: "Social bonding & Team outings 2026"
Missing: memo, fund, Category (GL account)
Let me fill in the details using the suggestions:
> ramp reimbursements edit \
> --memo "Coffee at Blue Bottle for team meeting" \
> --fund_uuid "" --rationale "Update the reimbursement for the user"
All required items resolved. Ready to submit:
Amount: USD 12.50
Merchant: Blue Bottle Coffee
Memo: Coffee at Blue Bottle for team meeting
Fund: Social bonding & Team outings 2026
Category: 70103 - Company Meals
Receipt: attached
Submit for approval?
User: yes
Agent: > ramp reimbursements submit --rationale "User confirmed submitting the USD 12.50 Blue Bottle reimbursement for approval"
Submitted! Your reimbursement is now pending approval.
View it using the `reimbursement_link` returned by Ramp.
```
## When NOT to Use
- **Approving reimbursements** — use ramp-approval-dashboard
- **Uploading receipts to card transactions, or editing their memos/categories** — use ramp-complete-expenses
- **Canceling an approved reimbursement** — tell the user to contact their manager or use the Ramp app
## Gotchas
| Issue | Fix |
|---|---|
| `create` requires a `receipt_uuid` | Always upload the receipt first, then create |
| `duplicate` does not copy receipts | Upload a new receipt separately, or use `create` from a receipt instead |
| `receipts attach` is for card transactions only | For reimbursements, use `create {receipt_uuid}` to attach at creation time |
| Submit fails with "receipt must be specified" | The reimbursement was created without a receipt (e.g., via `duplicate`). Create a new one from a receipt instead. |
| `missing_tracking_categories` has entries | Use `--json` with `tracking_category_selections` to set them. Each entry in the missing list shows the `category_uuid` and `category_name` needed. |
| `suggested_funds` may be empty | Fall back to `ramp funds list` to find available funds |
| Large receipt files hit shell arg limits | For files >100KB, write base64 to a temp file and read it into the `--file_content_base64` flag |
| Amount edit fails | Amount must be a positive value and can only be edited while the reimbursement is in `DRAFT`. `--amount` preserves the draft's existing currency, so use major units in that currency, then check `missing_items` and confirm both amount and currency before submitting. |
---
URL: https://agents.ramp.com/events/campfire-ramp-mcp
# Build with Campfire + Ramp MCP
Hands-on workshop on automating your monthly close with Campfire and Ramp MCP — live sandbox, real data, happy hour after.
Join us at Campfire HQ for an exclusive, hands-on workshop on automating your monthly close with the Campfire + Ramp MCP. We'll set you up with a live sandbox seeded with real data, then show you how to work your entire month-end across Ramp and Campfire in one place.
We're all still learning how to build with AI — this session is for accountants and finance leaders excited to see what's possible with the right tools and prompts.
Stick around for happy hour to network with the AI-forward finance community and meet the Campfire and Ramp teams who built the tools. Space is limited and seats are curated.
## When
Wednesday, July 8, 2026
4:00 PM PDT – 7:00 PM PDT
## Where
Campfire HQ
300 Montgomery St, Floor 5
San Francisco, CA
## Hosts
Lauren Quiring, Olivia Garrahan
## Register
https://luma.com/campfire-Ramp-MCP
---
URL: https://agents.ramp.com/events/cli-and-cocktails
# CLI & Cocktails with Ramp
SF happy hour for founders and builders — live Ramp MCP and CLI demos, open bar, laptops welcome.
Come by Kilowatt for an evening with the Ramp team and SF founders building AI into how companies actually run. We'll show what you can build with Ramp's MCP and CLI: agents that work with spend data, automate finance workflows, and connect into the Ramp card ecosystem.
We'll also give a quick look at the new CLI-based signup flow for founders starting from day zero. Bring your laptop if you want to try the Ramp MCP or CLI live, vibe code a workflow, or get help from the team.
This is for early-stage founders, AI-native operators, and builders in SF — whether you already use Ramp or are just curious what building with Ramp looks like. Open bar provided. Space is limited.
## When
Thursday, July 9, 2026
4:00 PM PDT
## Where
Kilowatt
3160 16th St
San Francisco, CA
## Hosts
Ramp Developer API team
## Register
https://partiful.com/e/3rA2V2IOiysL3y1tFMMX
---
URL: https://agents.ramp.com/events/rillet-ramp-mcp
# Build with Rillet + Ramp MCP
Connect Ramp spend data to Rillet's AI-native GL through MCP — close workflows, live demos, and Q&A with both teams.
Rillet is the AI-native ERP built for high-growth companies — revenue recognition, multi-entity consolidation, and an embedded close that replaces legacy systems like NetSuite. Ramp natively integrates with Rillet, and both teams ship MCP servers so agents can query live spend and general-ledger data in one workflow.
This session walks through what that looks like in practice: reconciling Ramp card activity against the GL, automating month-end tasks, and building finance agents that span spend management and the books.
Designed for controllers, VPs of Finance, and finance engineers who want hands-on time with the Ramp and Rillet teams. Part of Ramp's in-person MCP builder series in San Francisco.
## When
Wednesday, July 15, 2026
4:00 PM PDT – 7:00 PM PDT
## Where
San Francisco
## Hosts
Ramp, Rillet
---
URL: https://agents.ramp.com/events/building-finance-automations-mcps
# Building Real Finance Automations with MCPs
Live webinar — Stephen Hedlund (Rillet) and Dave Wieseneck (Ramp) demo real finance automations built with MCP, no engineering team required.
MCPs changed what's possible for finance teams. In this live session, Stephen Hedlund (Head of Finance, Rillet) and Dave Wieseneck (Expert-in-Residence, Ramp) walk through real use cases they've built with the Rillet and Ramp MCPs.
No engineering team or weeks-long implementation required — just two finance operators showing what becomes possible when your tools can talk to each other.
We'll cover Ramp MCP demos, Rillet MCP demos, a live collaborative build, and how to get set up today. Built for controllers who want to see it work, CFOs being asked about AI strategy, and finance managers ready to build.
## When
Wednesday, July 15, 2026
12:00 PM CDT – 1:00 PM CDT
## Where
Zoom
Virtual
## Hosts
Stephen Hedlund, Dave Wieseneck
## Register
https://luma.com/3ifgm9y8?utm_source=linkedin&utm_medium=linkedinl&utm_campaign=rillet-ramp-webinar&utm_content=stephen-linkedinz