---
name: pp-concur
description: "Every expense-report and travel workflow Concur's web app offers, plus duplicate detection and real flight/hotel search no Concur tool has -- filed through the same session your browser already uses. Trigger phrases: `file my Concur expense report`, `submit my expense report`, `what's in my Concur trip`, `use concur`, `run concur`."
author: "Allen Lew"
license: "Apache-2.0"
argument-hint: "<command> [args] | install cli|mcp"
allowed-tools: "Read Bash"
metadata:
  openclaw:
    requires:
      bins:
        - concur-pp-cli
    install:
      - kind: go
        bins: [concur-pp-cli]
        module: github.com/mvanhorn/printing-press-library/library/accounting/concur/cmd/concur-pp-cli
---
<!-- GENERATED FILE — DO NOT EDIT.
     This file is a verbatim mirror of library/productivity/concur/SKILL.md,
     regenerated post-merge by tools/generate-skills/. Hand-edits here are
     silently overwritten on the next regen. Edit the library/ source instead.
     See the repository agent guide, section "Generated artifacts: registry.json, cli-skills/". -->

# SAP Concur — Printing Press CLI

## Prerequisites: Install the CLI

This skill drives the `concur-pp-cli` binary. **You must verify the CLI is installed before invoking any command from this skill.** If it is missing, install it first:

1. Install via the Printing Press installer. It defaults binaries to `$HOME/.local/bin` on macOS/Linux and `%LOCALAPPDATA%\Programs\PrintingPress\bin` on Windows:
   ```bash
   npx -y @mvanhorn/printing-press-library install concur --cli-only
   ```
2. Verify: `concur-pp-cli --version`
3. Ensure the reported install directory is on `$PATH` for the agent/runtime that will invoke this skill.

If the `npx` install fails (no Node, offline, etc.), fall back to a direct Go install (requires Go 1.26.6 or newer). This installs into `$GOPATH/bin` (default `$HOME/go/bin`), so add that directory to `$PATH` instead:

```bash
go install github.com/mvanhorn/printing-press-library/library/accounting/concur/cmd/concur-pp-cli@latest
```

If `--version` reports "command not found" after install, the runtime cannot see the binary directory on `$PATH`. Do not proceed with skill commands until verification succeeds.

SAP Concur's official API requires enterprise partner credentials most individual users can never get. This CLI defaults to your logged-in browser session instead, so filing expense reports and checking travel works the same day you install it. Local SQLite sync turns your report history into something you can search, join, and validate offline.

## When to Use This CLI

Use this CLI for filing and checking your own SAP Concur expense reports and travel -- creating reports, pulling in available card charges, validating and submitting, and checking trip/itinerary status. It is the right choice whenever the task is something an individual employee would otherwise do by logging into concursolutions.com.

## Anti-triggers

Do not use this CLI for:
- Do not use this CLI to register a new SAP Concur partner application or manage OAuth2 App Center listings -- that is an admin/partner workflow, not an end-user one.
- Do not use this CLI to actually book new travel (flights/hotels/cars). `flights search` and `hotels search` return real, live fares and rates from your actual corporate-negotiated policy for research and comparison, but completing a booking is a complex multi-step web/agent experience this CLI does not implement -- finish the booking in Concur's web app.
- Do not use this CLI for company-wide financial reporting or accounting-system integration -- use the documented v3/v4 partner REST API directly for that.

## Unique Capabilities

These capabilities aren't available in any other tool for this API.

### Conditional browser fallback for report and expense creation
- **`reports create`** — Automatically and transparently retries report creation via automated browser when the Concur v4 API rejects pure HTTP requests with a `policyId is required` error. This fallback is completely conditional and only triggers for tenants requiring explicit policy assignment. It never guesses a Concur region: the UI host is derived only from a base URL that's actually `concursolutions.com`, or from an explicit `CONCUR_UI_BASE_URL` override — anything else is a clear error, not a silent default. If the browser click already succeeded before a later step fails, the error says so explicitly (with the report ID when known) instead of looking like a safely-retryable failure; do not blindly retry in that case.
- **`expenses create`** — Same conditional fallback pattern, triggered by a different confirmed-live defect: once a request body passes every client-side validation check, the API 404s instead of persisting (deliberately-invalid bodies correctly get a 400 instead, ruling out a body-shape bug). Triggers for a `--stdin` body exactly as reliably as the flag-driven path -- the fields it needs are read from the constructed request body, not the command's flag variables. Since the browser never surfaces a usable expense ID, success is confirmed by diffing the report's expense list (`GET .../reports/{id}/expenses`, confirmed unaffected by the defect) before and after the form's Save click, correlated against the submitted amount and expense type so a report modified concurrently through a shared manager/processor/proxy context can't have another actor's new expense misattributed as this call's own. Fills `--vendor`/`--business-purpose` directly in the form when set; if either can't be found or filled, the fallback aborts BEFORE the irreversible Save click rather than saving without a value the caller explicitly asked for. Transaction Date has no stable accessible name and Payment Type's only verified-live value is Concur's own default (Cash) -- rather than silently substitute today/Cash and report success, a `--date` or `--payment-type` this can't honor is rejected before the browser even opens.

### Local state that compounds
- **`expenses scan-duplicates`** — Find potential double-entered charges across all of your synced expenses.

  _Run this before submitting a batch of reports if you suspect a corporate-card charge and a manually-entered cash expense might be the same transaction._

  ```bash
  concur-pp-cli expenses scan-duplicates --agent
  ```

### Live travel shopping (search only, never books)
- **`flights search`** — Real flight availability and fares from a live shopping session against your actual corporate-negotiated rates and travel policy -- not a public fare aggregator. One-way by default; `--return` includes both legs in the search but only renders the outbound leg (see `--help` for the known gap).

  _Use this to compare real options before requesting travel, with policy-compliance flags already applied per fare._

  ```bash
  concur-pp-cli flights search --from LAX --to "New York" --depart 2026-10-12 --yes --agent
  ```
- **`hotels search`** — Real hotel availability and rates via a live, policy-scoped search -- the same inventory and pricing Concur's own Hotel Search page shows. Drives a real browser (see HTTP Transport and Auth Setup below) rather than a direct API call, because the hotel shopping-session mutation is blocked from scripted replay by the tenant's bot-mitigation.

  _A one-time dedicated-browser setup (see Auth Setup) avoids a separate login every time this command's session expires._

  ```bash
  concur-pp-cli hotels search --to "New York" --check-in 2026-10-12 --check-out 2026-10-18 --yes --agent
  ```

## HTTP Transport

This CLI uses Chrome-compatible HTTP transport for browser-facing endpoints. It does not require a resident browser process for normal API calls.

**Exception: `hotels search`.** Confirmed live that Concur's hotel shopping-session mutation is blocked from scripted HTTP replay by the tenant's bot-mitigation (byte-for-byte replay of a request that had just succeeded natively in the browser still failed). So this one command drives a real browser via `agent-browser` instead (`npm install -g agent-browser && agent-browser install`) -- `flights search` and every other command remain pure HTTP; only the hotel-shopping mutation needs a real browser.

## Command Reference

**account** — Current user profile, policies, and delegate context

- `concur-pp-cli account travel <user_id>` — Get the current user's travel profile and loyalty programs
- `concur-pp-cli account whoami` — Get the current user's profile, addresses, and travel IDs

**attendees** — Attendee catalog and per-expense attendee associations

- `concur-pp-cli attendees add` — Add attendees to an expense (merge-preserves existing associations; Concur's underlying association call is a replace
- `concur-pp-cli attendees list` — Get attendees currently associated with an expense

**delegates** — Delegate (act-on-behalf-of) relationships

- `concur-pp-cli delegates` — List users the current session user delegates for, with permission flags

**expense_types** — Expense type catalog and per-type dynamic form fields

- `concur-pp-cli expense-types list` — List usable expense types for the current user's policy

**expenses** — Expense line items within a report

- `concur-pp-cli expenses create` — Create an expense inside a report (core v3-equivalent fields: type, date, amount, currency, payment type)
- `concur-pp-cli expenses get` — Get a single expense with its filled/empty field manifest
- `concur-pp-cli expenses update` — Fill or change writable fields on an expense (core + custom/list fields)

**flights** — Search flight locations, travel policy preferences, and real flight availability (creates a live shopping session -- searches only, never books)

- `concur-pp-cli flights locations <query>` — Resolve an airport, city, or metro name to Concur's travel location IDs; metro queries (e.g. "New York") resolve to one search endpoint covering all constituent airports
- `concur-pp-cli flights preferences` — Show your travel policy's flight search defaults
- `concur-pp-cli flights search --from <origin> --to <dest> --depart <date> [--return <date>]` — Search real flight availability and fares

**hotels** — Search real hotel availability and rates (drives a real browser search -- searches only, never books)

- `concur-pp-cli hotels search --to <destination> --check-in <date> --check-out <date>` — Search real hotel availability and rates; requires `agent-browser` installed (see HTTP Transport and Auth Setup below)

**lists** — Valid values for list-type expense form fields

- `concur-pp-cli lists --list-id <id>` — Get valid values for a list-type form field by list ID

**locations** — Location catalog for filling expense/attendee location fields

- `concur-pp-cli locations <query>` — Search the location catalog by city or venue name

**payment_types** — Payment type catalog (Cash, Company Card, etc.)

- `concur-pp-cli payment-types` — List payment types available to the current user

**receipts** — Receipt image/PDF attachment

- `concur-pp-cli receipts <expense_id> --file <path>` — Attach a receipt image or PDF to an expense

**reports** — Expense report headers and lifecycle

- `concur-pp-cli reports create` — Create a new expense report header (transparently falls back to browser-driven creation on tenants requiring policy selection)
- `concur-pp-cli reports get` — Get a report's header
- `concur-pp-cli reports list` — List the current user's expense reports
- `concur-pp-cli reports submit` — Submit a report for approval
- `concur-pp-cli reports update` — Update a report's name or business purpose

**requests** — Travel requests / pre-trip authorization (UNVERIFIED paths -- see spec header notes)

- `concur-pp-cli requests get` — Get a travel request's detail and workflow status
- `concur-pp-cli requests list` — List the current user's travel requests

**travel_allowance** — Per-diem / travel allowance calculations (UNVERIFIED path -- see spec header notes)

- `concur-pp-cli travel-allowance <trip_id>` — Get travel allowance (per-diem) calculation results for a trip

**trips** — Booked trips and itineraries (UNVERIFIED paths -- see spec header notes)

- `concur-pp-cli trips get` — Get a trip's itinerary detail
- `concur-pp-cli trips list` — List the current user's upcoming and past trips


### Finding the right command

When you know what you want to do but not which command does it, ask the CLI directly:

```bash
concur-pp-cli which "<capability in your own words>"
```

`which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query.

## Recipes

### Check for duplicate charges

```bash
concur-pp-cli expenses scan-duplicates --agent
```

### Compare real flight and hotel options before requesting travel

```bash
concur-pp-cli flights search --from LAX --to "New York" --depart 2026-10-12 --yes --agent
concur-pp-cli hotels search --to "New York" --check-in 2026-10-12 --check-out 2026-10-18 --yes --agent
```

Both create a live shopping session against your real tenant -- searches only, never books. `flights search` is a direct API call; `hotels search` drives a real browser (see HTTP Transport and Auth Setup) and is markedly slower.

Scan the local SQLite cache for likely double-entered transactions across all your reports.

### File a manual expense line item (e.g. a recurring personal-reimbursement stipend)

```bash
concur-pp-cli expenses create \
  --report-id <report-id> --user-id <user-id> \
  --type 01000 --date 2026-09-15 --amount 50 \
  --payment-type CASH --vendor "F45 Training Culver City" --business-purpose "gym" \
  --agent
```

`--type`/`--payment-type` take the `expenseTypeId`/`paymentTypeId` codes from `expense-types
list`/`payment-types`, not display names. `--vendor` and `--business-purpose` are distinct
Concur form fields (confirmed live 2026-09-15) -- Vendor Description is who you paid, Business
Purpose is why; don't put a business-purpose-shaped value like "gym" into `--vendor`. Setting
`--business-purpose` at creation time also means never needing `expenses apply-rules`'
PATCH-based fill, which hits this same command's confirmed-live 404 defect just like creation
itself used to. `--currency` set to anything other than `USD` is **rejected** -- no working
currency-override field is confirmed live for this endpoint, and silently creating an expense in
the report/policy default currency instead of what was requested is a correctness bug, not an
acceptable fallback. If this command hits its confirmed-live HTTP 404 defect, it now falls back
to browser automation automatically instead of just failing -- see "Conditional browser fallback
for report and expense creation" above; this triggers for a `--stdin` body just as reliably as
the flag-driven path, and will similarly reject (rather than silently substitute defaults for) a
historical `--date` or non-Cash `--payment-type` it can't reliably honor.

## Auth Setup

Concur's documented OAuth2 partner API is gated behind a Partner Enablement Manager relationship -- there is no self-serve signup, and this CLI does not implement that OAuth2 flow at all. Instead, this CLI authenticates via cookie/browser-session auth: run 'auth login --chrome', log into your company's Concur portal like you normally would (including SSO/MFA), and the CLI captures the resulting session. If a command fails with 401/403 and your company IT has partner OAuth2 credentials, that workflow requires calling the documented v3/v4 REST API directly (developer.concur.com) outside this CLI -- it is not something 'auth login' or any other command here can switch to.

Run `concur-pp-cli doctor` to verify setup.

### `hotels search` has a second, separate login by default

`hotels search` drives its own `agent-browser`-controlled Chrome instance (see HTTP Transport above), which does not share cookies with `auth login --chrome`'s source browser or credential store. Confirmed live that bridging them by copying cookies does not work -- Concur's bot-mitigation appears to bind the session to the browser/device that created it, not just the cookie value, so a copied JWT gets cleared by the server on the next navigation even when every cookie (including the Akamai bot-sensor ones) is copied alongside it. The first time (or whenever that session expires), `hotels search` opens its own Chrome window and asks you to log in there directly -- that login persists across later invocations until it expires again, so this is an occasional cost, not a per-search one.

**Optional one-time setup to avoid that second login entirely**: run a dedicated Chrome profile with remote debugging enabled and log into Concur there once. Use a real named profile (Chrome menu -> "Add Person", or chrome://settings -> Add profile) rather than a throwaway `--user-data-dir`, so `auth login --chrome --profile "<name>"` can read its cookies too:

```bash
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 --profile-directory="<profile dir name>"
```

`hotels search` auto-detects that session (tries CDP ports 9222, 9333, 9229 in order, or set `CONCUR_CDP_PORT` for a custom port) and *attaches* to it -- rather than copying its credentials -- before falling back to its own isolated login. Attaching, not copying, is what makes this work: it is the same live browser connection, so there is no separate device fingerprint for Concur's bot-mitigation to reject. Keep that Chrome window running whenever you plan to use `hotels search`; if you see "the dedicated Concur browser ... is no longer logged in", log in there again.

## Agent Mode

Add `--agent` to any command. Expands to: `--json --compact --no-input --no-color --yes`.

- **Pipeable** — JSON on stdout, errors on stderr
- **Filterable** — `--select` keeps a subset of fields. Dotted paths descend into nested structures; arrays traverse element-wise. Critical for keeping context small on verbose APIs:

  ```bash
  concur-pp-cli payment-types --agent --select paymentTypeId,paymentTypeName,description
  ```
- **Previewable** — `--dry-run` shows the request without sending
- **Offline-friendly** — sync/search commands can use the local SQLite store when available
- **Non-interactive** — never prompts, every input is a flag
- **Explicit retries** — use `--idempotent` only when an already-existing create should count as success

### Response envelope

Commands that read from the local store or the API wrap output in a provenance envelope:

```json
{
  "meta": {"source": "live" | "local", "synced_at": "...", "reason": "..."},
  "results": <data>
}
```

Parse `.results` for data and `.meta.source` to know whether it's live or local. A human-readable `N results (live)` summary is printed to stderr only when stdout is a terminal AND no machine-format flag (`--json`, `--csv`, `--compact`, `--quiet`, `--plain`, `--select`) is set — piped/agent consumers and explicit-format runs get pure JSON on stdout.

## Paths and state

Agents should treat the CLI's path resolver as part of the runtime contract:

- Use `--home <dir>` for one invocation, or set `CONCUR_HOME=<dir>` to relocate all four path kinds under one root.
- Use per-kind env vars only when a specific kind must diverge: `CONCUR_CONFIG_DIR`, `CONCUR_DATA_DIR`, `CONCUR_STATE_DIR`, `CONCUR_CACHE_DIR`.
- Resolution order is per-kind env var, `--home`, `CONCUR_HOME`, XDG (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CACHE_HOME`), then platform defaults.
- `config` contains settings like `config.toml` and profiles. `data` contains `credentials.toml`, `data.db`, cookies, and auth sidecars. `state` contains persisted queries, jobs, and `teach.log`. `cache` contains regenerable HTTP/cache files.
- Stored secrets live in `credentials.toml` under the data dir. Existing legacy `config.toml` secrets are read for compatibility and le