navan-pack
Claude Code skill pack for Navan (24 skills)
Installation
Open Claude Code and run this command:
/plugin install navan-pack@claude-code-plugins-plus
Use --global to install for all projects, or --project for current project only.
What It Does
> Claude Code skill pack for Navan integration (24 skills)
Corporate travel and expense management API for flight booking, expense reporting, and spend analytics.
Skills (26)
'Use when setting up CI/CD pipelines that validate Navan API integrations,.
Navan CI Integration
Overview
Navan has no SDK — all CI integration uses raw REST calls against https://api.navan.com with OAuth 2.0 clientcredentials authentication. This skill generates GitHub Actions workflows that validate your Navan integration on every push: token health checks, booking data schema validation, and travel policy compliance reports. Secrets (clientid, client_secret) are stored in GitHub Actions secrets, never in code.
Prerequisites
- Navan Admin access to create OAuth 2.0 application credentials (Admin > API Settings)
- GitHub repo with Actions enabled
- GitHub Secrets configured:
NAVANCLIENTID,NAVANCLIENTSECRET - Navan API base URL:
https://api.navan.com
Instructions
Step 1 — Store OAuth Credentials in GitHub Secrets
Navigate to your GitHub repo > Settings > Secrets and variables > Actions. Add:
NAVANCLIENTID— from Navan Admin > API SettingsNAVANCLIENTSECRET— from Navan Admin > API Settings
Step 2 — Create the CI Workflow
# .github/workflows/navan-integration-check.yml
name: Navan Integration Health Check
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am UTC
jobs:
navan-health:
runs-on: ubuntu-latest
env:
NAVAN_BASE_URL: https://api.navan.com
steps:
- uses: actions/checkout@v4
- name: Authenticate with Navan OAuth 2.0
id: auth
run: |
TOKEN_RESPONSE=$(curl -s -X POST \
https://api.navan.com/ta-auth/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=${{ secrets.NAVAN_CLIENT_ID }}" \
-d "client_secret=${{ secrets.NAVAN_CLIENT_SECRET }}")
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
echo "::error::OAuth authentication failed"
echo "$TOKEN_RESPONSE" | jq .
exit 1
fi
echo "::add-mask::$ACCESS_TOKEN"
echo "token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"
- name: API Health Check — Fetch Bookings
run: |
HTTP_CODE=$(curl -s -o /tmp/bookings.json -w "%{http_code}" \
"$NAVAN_BASE_URL/v1/bookings?page=0&size=5" \
-H "Authorization: Bearer ${{ steps.auth.outputs.token }}")
echo "Health check status: $HTTP_CODE"
if [ "$HTTP_CODE" != &'Diagnose and fix common Navan API errors with targeted fix procedures.
Navan Common Errors
Overview
Diagnose and resolve Navan API errors using targeted fix procedures. All errors surface as raw HTTP status codes since Navan has no public SDK — this guide covers 401, 403, 404, 429, 500, and 503 with curl-based diagnostics.
Purpose: Identify the root cause of a Navan API error and apply the correct fix.
Prerequisites
- Navan API credentials configured (see
navan-install-auth) curlandjqavailable in your terminal- Environment variables set:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL
Instructions
Error 401 — Unauthorized (Invalid or Expired OAuth Token)
Root causes:
- OAuth token has expired (tokens have a limited
expires_inwindow) client_secretwas rotated in the Navan dashboard but not updated in.env- Malformed
Authorizationheader (missingBearerprefix) - Token from a different Navan organization
Diagnostic steps:
# 1. Verify credentials can still obtain a token
curl -s -X POST https://api.navan.com/ta-auth/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('TOKEN OK' if 'access_token' in d else f'FAIL: {d}')"
# 2. Check if existing token is expired
echo "Token var length: ${#NAVAN_TOKEN}"
Fix: Re-run the token exchange. If that also returns 401, regenerate credentials at Admin > Travel admin > Settings > Integrations > Navan API Credentials.
Error 403 — Forbidden (Insufficient Permissions)
Root causes:
- API credentials lack required scopes for the endpoint
- Account is on Business tier but endpoint requires Enterprise
- Expense Transaction API not enabled (requires separate Navan support request)
- User role lacks admin permissions for admin-only endpoints
Diagnostic steps:
# Test the bookings endpoint
TOKEN=$(curl -s -X POST https://api.navan.com/ta-auth/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
echo "Bookings:" && curl -s -o /dev/null -w "%{http_code}" \
"https://api.navan.com/v1/'Manage the complete Navan travel booking lifecycle via REST API.
Navan — Travel Booking & Management
Overview
This skill provides the complete travel booking workflow through the Navan REST API. Navan has no public SDK — all access is via raw HTTP calls using OAuth 2.0 bearer tokens. This skill covers trip retrieval for both user and admin scopes, itinerary PDF downloads, invoice access, and trip filtering by date range and status. Every booking is keyed by a UUID primary key that must be tracked for deduplication and updates.
Prerequisites
- Navan account with API credentials (clientid + clientsecret)
- Credentials created in Admin > Travel admin > Settings > Integrations > Navan API Credentials
- OAuth 2.0 token obtained via POST
/ta-auth/oauth/token(seenavan-install-auth) - Node.js 18+ with
node-fetchor Python 3.8+ withrequests - Environment variables:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL NAVANBASEURLshould be set tohttps://api.navan.com
Instructions
Step 1: Authenticate and Obtain Bearer Token
const tokenRes = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
const { access_token } = await tokenRes.json();
const headers = { Authorization: `Bearer ${access_token}` };
Step 2: Retrieve Bookings
// GET /v1/bookings — returns booking records (paginated via page + size)
// Response: records in .data array, primary key uuid
const bookingsRes = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings?page=0&size=50`,
{ headers }
);
const { data: bookings } = await bookingsRes.json();
bookings.forEach((booking: any) => {
console.log(`UUID: ${booking.uuid}`);
console.log(` Route: ${booking.origin} -> ${booking.destination}`);
console.log(` Status: ${booking.status}`);
console.log(` Dates: ${booking.start_date} to ${booking.end_date}`);
});
Step 3: Retrieve Bookings with Date Filtering
// GET /v1/bookings with createdFrom/createdTo for incremental pulls
const filteredRes = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings?createdFrom=2026-01-01&createdTo=2026-03-31&page=0&size=50`,
{ headers }
);
const { data: filteredBookings } = await filteredRes.json();
console.log(`Total bookings in range: ${filteredBookings.length}`);
Step 4: Paginate Through All Bookings
// Paginate using pag'Manage Navan expense reporting, transaction data, and ERP synchronization.
Navan — Expense Management
Overview
This skill covers the Navan expense reporting workflow through the REST API. The Expense Transaction API requires separate enablement from Navan support — it is not available by default. Once enabled, you can retrieve transaction data incrementally (the TRANSACTION table is append-only, unlike BOOKING which re-imports weekly). This skill provides patterns for expense data retrieval, approval workflow automation, and synchronization with ERP systems including NetSuite, Sage Intacct, Xero, and QuickBooks.
Prerequisites
- Navan account with OAuth 2.0 API credentials (see
navan-install-auth) - Expense Transaction API enabled by Navan support (submit request via help center)
- For ERP sync: active integration configured in Navan Admin > Integrations
- Environment variables:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL - Node.js 18+ or Python 3.8+
Instructions
Step 1: Request Expense API Enablement
The Expense Transaction API is not self-service. To enable it:
- Navigate to Navan Help Center
- Submit a support request for "Expense Transaction API access"
- Provide your company ID and the OAuth client_id that needs access
- Navan support will enable the expense endpoints (typically 1-3 business days)
- Verify access by calling the transaction endpoint after enablement
Step 2: Authenticate
const tokenRes = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
const { access_token } = await tokenRes.json();
const headers = { Authorization: `Bearer ${access_token}` };
Step 3: Retrieve Expense Transactions
// Expense transactions are incremental — use date ranges for efficient pulls
// Note: The bookings endpoint is the primary data endpoint; expense data
// may require separate enablement from Navan support
const txnRes = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings` +
`?createdFrom=2026-03-01&createdTo=2026-03-31&page=0&size=50`,
{ headers }
);
const { data: transactions } = await txnRes.json();
transactions.forEach((txn: any) => {
console.log(`ID: ${txn.transaction_id}`);
console.log(` Employee: ${txn.employee_name} (${txn.employee_id})`);
console.log(` Amount: ${txn.currency} ${txn.amount}`);
console.log(` Category: ${txn.category}`);
console.log(` Status: ${txn.approval_status}`);
console.l'Use when optimizing travel spend with Navan''s policy engine, analyzing.
Navan Cost Tuning
Overview
Navan's platform includes a built-in policy engine, negotiated rate management, unused ticket tracking, and the Navan Rewards program — but these features require deliberate configuration to deliver savings. This skill covers the full cost optimization lifecycle: setting up travel policies with hard and soft caps, analyzing booking data via the REST API to find savings opportunities, enforcing negotiated corporate rates, recovering value from unused tickets, and incentivizing employees to choose cheaper options through Navan Rewards. No SDK exists — all analytics use direct REST API calls against https://api.navan.com/v1.
Prerequisites
- Navan Admin account with policy management permissions
- OAuth 2.0 credentials from Admin > API Settings (clientid, clientsecret)
- Historical booking data — at least 3 months for meaningful analysis
- Navan plan — Business (free for up to 300 employees), Expense ($15/user/month after 5 free), or Enterprise (custom)
- Pricing details: https://navan.com/pricing
Instructions
Step 1 — Analyze Current Spend via API
Pull booking data to identify where money is being lost:
# Authenticate
ACCESS_TOKEN=$(curl -sf -X POST https://api.navan.com/ta-auth/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${NAVAN_CLIENT_ID}&client_secret=${NAVAN_CLIENT_SECRET}" \
| jq -r '.access_token')
# Fetch last 90 days of bookings for analysis (page + size pagination)
curl -s "https://api.navan.com/v1/bookings?createdFrom=2026-01-01&page=0&size=50" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-o bookings.json
# Analyze: average booking lead time (days before travel)
# Response structure: records in .data array
jq '[.data[] | ((.departure_date | fromdate) - (.created_at | fromdate)) / 86400] | add / length' \
bookings.json
# Target: 14+ days average lead time for maximum savings
// Identify top savings opportunities by category
interface BookingAnalysis {
total_spend: number;
avg_lead_time_days: number;
out_of_policy_pct: number;
unused_tickets: number;
top_routes: { route: string; spend: number; trips: number }[];
}
async function analyzeSpend(token: string): Promise<BookingAnalysis> {
const response = await fetch(
'https://api.navan.com/v1/bookings?page=0&size=50',
{ headers: { 'Authorization': `Bearer ${token}` } }
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { data: bookings } = await response.json();
const routeMap = new Map<string, { spend: number; trips: number }>();
let totalSpend = 0;
let outOfPo'Extract and transform Navan booking and transaction data using pagination,.
Navan Data Handling
Overview
This skill covers data extraction and transformation patterns for Navan booking and transaction data. Navan exposes two primary data tables with different refresh behaviors: BOOKING (full re-import weekly, keyed by UUID) and TRANSACTION (incremental append-only). Data can be extracted via the direct REST API or through managed connectors — Fivetran, Airbyte (source-navan v0.0.42), and Estuary Flow. This skill provides pagination patterns, date-range filtering, UUID-based deduplication, and schema mapping for downstream analytics.
Prerequisites
- Navan account with OAuth 2.0 API credentials (see
navan-install-auth) - For direct API: Node.js 18+ or Python 3.8+
- For Fivetran: Fivetran account with Navan connector
- For Airbyte: Airbyte instance (Cloud or OSS) with source-navan v0.0.42+
- Environment variables:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL
Instructions
Step 1: Direct API — Paginated Booking Extraction
const tokenRes = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
const { access_token } = await tokenRes.json();
const headers = { Authorization: `Bearer ${access_token}` };
// Paginate through all bookings using page + size params
async function extractAllBookings(startDate: string, endDate: string) {
const allBookings: any[] = [];
let page = 0;
const size = 50;
while (true) {
const res = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings` +
`?createdFrom=${startDate}&createdTo=${endDate}` +
`&page=${page}&size=${size}`,
{ headers }
);
if (res.status === 429) {
// Rate limited — exponential backoff
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5');
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
const { data } = await res.json();
if (!data || !data.length) break;
allBookings.push(...data);
if (data.length < size) break; // last page
page++;
console.log(`Fetched ${allBookings.length} bookings...`);
}
return allBookings;
}
const bookings = await extractAllBookings('2026-01-01', '2026-03-31');
console.log(`Total bookings extracted: ${bookings.length}`);
Step 2: UUID-Based Deduplication
// BOOKING table re-imports weekly — same UUID may appear in multiple extractions
function deduplicateByUUID(records: any[]): any[] {
const seen = new Map&'Implement incremental sync strategies for Navan BOOKING and TRANSACTION.
Navan — Data Sync
Overview
This skill provides production-grade sync strategies for Navan data. The two primary tables have fundamentally different sync models: BOOKING requires weekly full-refresh with merge-upsert logic (every record is re-imported, keyed by UUID), while TRANSACTION is incremental and append-only. Real-time use cases require webhook callbacks for event-driven processing. This skill covers all three tiers — scheduled full-refresh, incremental watermark-based sync, and real-time webhooks — along with Airbyte connector configuration and idempotent SQL upsert patterns.
Prerequisites
- Navan account with OAuth 2.0 API credentials (see
navan-install-auth) - Destination warehouse (Snowflake, BigQuery, PostgreSQL, or Redshift)
- For managed sync: Airbyte instance (Cloud or OSS) with source-navan v0.0.42+
- For webhooks: publicly accessible HTTPS endpoint for callbacks
- Node.js 18+ or Python 3.8+
- Environment variables:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL
Instructions
Step 1: Full-Refresh Sync for BOOKING Table
The BOOKING table is re-imported weekly by Navan. Every record is refreshed, so your sync must use merge-upsert logic to avoid duplicates while capturing updates.
const tokenRes = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
const { access_token } = await tokenRes.json();
const headers = { Authorization: `Bearer ${access_token}` };
// Full extraction — paginate through all bookings for weekly refresh
let allBookings: any[] = [];
let page = 0;
const size = 50;
while (true) {
const res = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings?page=${page}&size=${size}`,
{ headers }
);
const { data } = await res.json();
if (!data || !data.length) break;
allBookings.push(...data);
if (data.length < size) break;
page++;
}
console.log(`Extracted ${allBookings.length} bookings for full refresh`);
SQL merge-upsert pattern (PostgreSQL):
-- Staging table receives raw API data
CREATE TABLE IF NOT EXISTS navan_booking_staging (
uuid TEXT PRIMARY KEY,
traveler_email TEXT,
origin TEXT,
destination TEXT,
start_date DATE,
end_date DATE,
total_cost NUMERIC(12,2),
currency TEXT DEFAULT 'USD',
department TEXT,
cost_center TEXT,
status TEXT,
in_policy BOOLEAN,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
synced_at TIMESTAMPTZ DEFAULT NOW()
);
-- Merge-upsert: insert new"Use when collecting diagnostic data from a Navan API integration \u2014\.
Navan Debug Bundle
Overview
Collect diagnostic data from Navan REST API integrations into a structured, shareable debug bundle. Navan has no SDK — all debugging uses raw HTTP requests against their OAuth 2.0 REST endpoints.
Prerequisites
- Navan API credentials:
clientidandclientsecretfrom Admin > Travel admin > Settings > Integrations curlandjqinstalled locally- Credentials are viewable only once at creation — store them in a secret manager immediately
- No sandbox environment exists; all API calls hit production
Instructions
Step 1 — Create Bundle Directory
BUNDLE_DIR="navan-debug-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BUNDLE_DIR"/{auth,api,connectivity,env}
echo "Bundle initialized: $BUNDLE_DIR"
Step 2 — Capture Environment State
cat > "$BUNDLE_DIR/env/config.txt" <<ENVEOF
Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
NAVAN_CLIENT_ID: ${NAVAN_CLIENT_ID:+SET (not empty)}${NAVAN_CLIENT_ID:-UNSET}
NAVAN_CLIENT_SECRET: ${NAVAN_CLIENT_SECRET:+SET (not empty)}${NAVAN_CLIENT_SECRET:-UNSET}
NAVAN_TOKEN_URL: ${NAVAN_TOKEN_URL:-https://api.navan.com/ta-auth/oauth/token}
curl version: $(curl --version | head -1)
jq version: $(jq --version 2>/dev/null || echo "not installed")
ENVEOF
Step 3 — Test OAuth Token Acquisition
curl -s -w "\n---HTTP_CODE:%{http_code}---\n" \
-X POST "https://api.navan.com/ta-auth/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET" \
| tee "$BUNDLE_DIR/auth/token-response.json" \
| jq '{has_token: (.access_token != null), error: .error}'
If the token response returns HTTP 401, the credentials are invalid or expired. If HTTP 403, the API integration may not be enabled for your organization.
Step 4 — Probe API Endpoints
Test each core endpoint and capture full response headers:
TOKEN=$(jq -r '.access_token' "$BUNDLE_DIR/auth/token-response.json")
# Test the primary bookings endpoint
ENDPOINT="v1/bookings"
curl -s -D "$BUNDLE_DIR/api/bookings-headers.txt" \
-w "\n---HTTP_CODE:%{http_code}---\n" \
-H "Authorization: Bearer $TOKEN" \
"https://api.navan.com/${ENDPOINT}?page=0&size=1" \
> "$BUNDLE_DIR/api/bookings-body.json" 2>&1
echo "bookings: $(grep 'HTTP_CODE' "$BUNDLE_DIR/api/bookings-body.json")"
Step 5 — Connectivity and DNS Tests
'Use when deploying Navan integrations with ERP systems (NetSuite, Sage.
Navan Deploy Integration
Overview
Navan connects to enterprise systems through multiple integration methods: direct REST API with OAuth 2.0, SCIM for user provisioning, SFTP for batch file exchange, SAML/OIDC for SSO, and webhooks for real-time events. There is no SDK — all integrations use Navan's REST endpoints or admin console configuration. This skill provides deployment checklists for the three most common integration categories: ERP expense sync, HRIS user provisioning, and identity provider SSO.
Prerequisites
- Navan Admin account with integration management permissions
- OAuth 2.0 credentials —
clientidandclientsecretfrom Admin > API Settings - Target system admin access — NetSuite/Sage Intacct/Xero admin, Workday/BambooHR admin, or Okta/Azure AD admin
- API base URL:
https://api.navan.com/v1
Instructions
Category A — ERP Expense Sync (NetSuite, Sage Intacct, Xero, QuickBooks)
Deployment Checklist:
- Create OAuth credentials in Navan Admin > API Settings
- Configure GL code mappings — Map Navan expense categories to your chart of accounts
- Set cost center mappings — Align Navan departments with ERP cost centers
- Enable expense export via REST API:
# Fetch approved expenses ready for ERP sync
curl -s -X GET "https://api.navan.com/v1/expenses?status=approved&limit=50" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json"
# Response includes fields for ERP mapping:
# {
# "uuid": "exp_abc123",
# "amount": 245.50,
# "currency": "USD",
# "category": "meals_entertainment",
# "cost_center": "engineering",
# "gl_code": "6200",
# "receipt_url": "https://api.navan.com/v1/receipts/exp_abc123",
# "approved_at": "2026-03-20T14:30:00Z"
# }
- Set up sync schedule — Navan supports daily or real-time export via webhooks
- Validate with test expenses — Submit 3-5 test expenses through the full approval flow
- Enable in production — Switch from sandbox to production OAuth credentials
Category B — HRIS User Provisioning (Workday, BambooHR, ADP)
SCIM Provisioning Setup:
- Enable SCIM in Navan Admin > Integrations > User Provisioning
- Configure SCIM endpoint in your HRIS:
- SCIM Base URL:
https://api.navan.com/scim/v2 - Authentication: OAuth 2.0 Bearer Token
'Configure Navan admin roles, travel policies, approval workflows, and.
Navan Enterprise RBAC
Overview
Navan's enterprise tier provides granular role-based access control, configurable travel policies, and multi-tier approval workflows. The platform enforces in-policy vs out-of-policy bookings at the point of purchase — travelers see policy-compliant options highlighted and must justify out-of-policy selections through approval chains. This skill covers the admin role hierarchy, policy rule configuration, department-scoped access, and API-driven policy management.
Prerequisites
- Navan enterprise account with Global Admin or Travel Admin access
- OAuth 2.0 credentials with admin-scoped permissions (see
navan-install-auth) - Organizational hierarchy defined (departments, cost centers, reporting lines)
- Dedicated Customer Success Manager contact (included with enterprise tier)
Instructions
Step 1: Understand the Navan Role Hierarchy
Global Admin
├── Travel Admin — Manage travel policies, view all bookings
├── Expense Admin — Manage expense policies, approve/reject reports
├── Finance Admin — View spend analytics, export financial reports
├── Department Manager — Approve bookings/expenses for direct reports
├── Arranger — Book travel on behalf of other employees
└── Traveler — Book own travel within policy, submit expenses
| Role | Book Travel | Approve | View All Bookings | Edit Policies | Manage Users |
|---|---|---|---|---|---|
| Global Admin | Yes | Yes | Yes | Yes | Yes |
| Travel Admin | Yes | Yes | Yes | Yes | No |
| Expense Admin | No | Yes | Expenses Only | Expense Only | No |
| Finance Admin | No | No | Yes (read-only) | No | No |
| Dept Manager | Yes | Own Dept | Own Dept | No | No |
| Arranger | Others | No | Arranged Only | No | No |
| Traveler | Self | No | Own Only | No | No |
Step 2: Configure Travel Policy Rules via API
const accessToken = process.env.NAVAN_ACCESS_TOKEN!;
// Retrieve current travel policy
const policyRes = await fetch('https://api.navan.com/v1/travel-policies', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const policies = await policyRes.json();
// Create a department-specific policy
const newPolicy = await fetch('https://api.navan.com/v1/travel-policies', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},'Manage Navan users, departments, cost centers, and approval chains via.
Navan — Entity Management
Overview
This skill covers organizational entity management in Navan: users, departments, cost centers, and approval chains. Navan supports two approaches for user lifecycle management — the REST API with GET /get_users for querying and auditing, and SCIM 2.0 provisioning for automated sync with identity providers like Okta, Entra ID (Azure AD), and OneLogin. Travel policies are assigned at the department level, and approval chains support multi-level routing based on expense thresholds and trip types. This skill is essential for organizations managing 100+ travelers.
Prerequisites
- Navan account with admin-level API credentials (see
navan-install-auth) - OAuth 2.0 token with admin scope
- For SCIM: Okta, Entra ID, or OneLogin with SCIM 2.0 support
- For SSO: SAML 2.0 or Google Workspace configured in Navan Admin
- Environment variables:
NAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL
Instructions
Step 1: Authenticate and Retrieve Booking Data
const tokenRes = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
const { access_token } = await tokenRes.json();
const headers = { Authorization: `Bearer ${access_token}` };
// GET /v1/bookings — retrieve bookings (records in .data array)
const bookingsRes = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings?page=0&size=50`,
{ headers }
);
const { data: bookings } = await bookingsRes.json();
// Extract unique users from booking data
const users = [...new Map(bookings.map((b: any) => [b.traveler_email, b])).values()];
users.forEach((user: any) => {
console.log(`${user.email} | Role: ${user.role} | Dept: ${user.department}`);
console.log(` Cost Center: ${user.cost_center} | Manager: ${user.manager_email}`);
console.log(` Travel Policy: ${user.travel_policy_name}`);
});
Step 2: Audit User Access and Roles
// Build an access audit report
interface UserAudit {
email: string;
role: string;
department: string;
hasManagerAssigned: boolean;
hasCostCenter: boolean;
hasTravelPolicy: boolean;
}
const audit: UserAudit[] = users.map((u: any) => ({
email: u.email,
role: u.role,
department: u.department ?? 'UNASSIGNED',
hasManagerAssigned: Boolean(u.manager_email),
hasCostCenter: Boolean(u.cost_center),
hasTravelPolicy: Boolean(u.travel_policy_name),
}));
// Flag users missing required configuration
const incomplete = audit.filter(
u => !u.hasManagerAssig'Make your first Navan API call to retrieve trip and user data.
Navan Hello World
Overview
Execute a first API call against the Navan REST API to retrieve trip data. All examples use raw REST calls — Navan has no public SDK.
Purpose: Confirm end-to-end integration by retrieving real trip data and parsing uuid primary keys.
Prerequisites
- Completed
navan-install-authwith working OAuth 2.0 credentials .envfile withNAVANCLIENTID,NAVANCLIENTSECRET, andNAVANBASEURL- Node.js 18+ (for TypeScript) or Python 3.8+ (for Python)
- At least one trip or user in your Navan organization
Instructions
Step 1: Acquire a Bearer Token
Reuse the token exchange from navan-install-auth:
import 'dotenv/config';
async function getNavanToken(): Promise<string> {
const response = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
if (!response.ok) throw new Error(`Auth failed: ${response.status}`);
const data = await response.json();
return data.access_token;
}
Step 2: Retrieve Bookings (TypeScript)
Call GET /v1/bookings to fetch booking records (paginated with page + size):
interface NavanBooking {
uuid: string; // Primary key for all booking records
traveler_name: string;
origin: string;
destination: string;
departure_date: string;
return_date: string;
booking_status: string;
booking_type: string; // "flight", "hotel", "car"
}
async function getBookings(token: string): Promise<NavanBooking[]> {
const response = await fetch(
`${process.env.NAVAN_BASE_URL}/v1/bookings?page=0&size=50`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
if (!response.ok) {
throw new Error(`GET /v1/bookings failed: ${response.status} ${response.statusText}`);
}
const { data } = await response.json(); // records in .data array
return data ?? [];
}
// Execute
const token = await getNavanToken();
const bookings = await getBookings(token);
console.log(`Retrieved ${bookings.length} bookings:`);
bookings.forEach((b) =>
console.log(` [${b.uuid}] ${b.origin} -> ${b.destination} (${b.booking_status})`)
);
Step 3: Retrieve Bookings (Python)
import os
import requests
from dotenv import load_dotenv
load_dotenv()
def get_navan_token() -> str:
resp = re"Use when responding to Navan platform incidents \u2014 flight cancellations,\.
Navan Incident Runbook
Overview
Structured incident response for Navan travel platform disruptions. Navan uses raw REST APIs with OAuth 2.0 — there is no SDK and no sandbox. All diagnostic commands run against production.
Prerequisites
- Access to Navan admin console (Admin > Travel admin)
- OAuth credentials (
clientid,clientsecret) stored in your secret manager - Familiarity with Navan's Ava AI assistant (in-app chat, first-line support)
curlandjqfor API health probing
Instructions
Step 1 — Classify Severity
| Severity | Condition | Response Time | Escalation |
|---|---|---|---|
| P1 — Critical | API fully down, all bookings failing | Immediate | Navan support + Ava AI + internal exec |
| P2 — High | Degraded performance, partial failures | 15 minutes | Navan support + internal travel admin |
| P3 — Medium | Intermittent errors, expense sync delays | 1 hour | Internal triage, monitor |
| P4 — Low | Cosmetic issues, non-blocking warnings | Next business day | Internal backlog |
Step 2 — Triage with Ava AI
Before manual debugging, use Navan's built-in AI assistant:
- Open the Navan app or visit app.navan.com
- Click the Ava chat icon (bottom-right)
- Describe the issue — Ava can check booking status, rebook flights, and surface known outages
- If Ava cannot resolve, proceed to API health checks
Step 3 — API Health Check
# Test OAuth authentication
AUTH_RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST "https://api.navan.com/ta-auth/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET")
HTTP_CODE=$(echo "$AUTH_RESPONSE" | tail -1)
BODY=$(echo "$AUTH_RESPONSE" | sed '$d')
echo "Auth endpoint: HTTP $HTTP_CODE"
echo "$BODY" | jq '{token_present: (.access_token != null), error: .error}' 2>/dev/null
# Test booking retrieval (requires valid token)
TOKEN=$(echo "$BODY" | jq -r '.access_token')
curl -s -w "\nHTTP %{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"https://api.navan.com/v1/bookings?page=0&size=50" | tail -1
Step 4 — Incident-Specific Playbooks
Booking API Failure (P1/P2):
- Confirm via API health check above —
'Set up OAuth 2.
Navan Install & Auth
Overview
Configure OAuth 2.0 client credentials for the Navan REST API. Navan has no public SDK — all API access uses raw REST calls with bearer tokens obtained via the client_credentials grant.
Purpose: Obtain a working OAuth 2.0 bearer token for calling Navan API endpoints.
Prerequisites
- Navan admin access — you need the Admin or Travel Admin role
- Node.js 18+ (for TypeScript) or Python 3.8+ (for Python)
- A
.env-aware project (dotenv for Node, python-dotenv for Python) - Navan Business tier or higher (free for up to 300 employees)
Instructions
Step 1: Create OAuth Credentials in Navan Dashboard
Navigate to: Admin > Travel admin > Settings > Integrations > Navan API Credentials > Create New
Save the clientid and clientsecret immediately — credentials are only viewable once. If lost, you must revoke and regenerate.
Step 2: Store Credentials Securely
Create a .env file in your project root:
# .env — NEVER commit this file
NAVAN_CLIENT_ID="your-client-id-here"
NAVAN_CLIENT_SECRET="your-client-secret-here"
NAVAN_BASE_URL="https://api.navan.com"
Ensure .env is in your .gitignore:
echo ".env" >> .gitignore
Step 3: Token Exchange (TypeScript)
Install dependencies and implement the OAuth 2.0 client credentials flow:
npm install dotenv
import 'dotenv/config';
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
async function getNavanToken(): Promise<string> {
const response = await fetch(`${process.env.NAVAN_BASE_URL}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.NAVAN_CLIENT_ID!,
client_secret: process.env.NAVAN_CLIENT_SECRET!,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Auth failed (${response.status}): ${error}`);
}
const data: TokenResponse = await response.json();
return data.access_token;
}
// Verify connection
const token = await getNavanToken();
console.log('Auth successful — token acquired');
Step 4: Token Exchange (Python)
pip install requests python-dotenv
import os
import requests
from dotenv import load_dotenv
load_dotenv()
def ge'Set up a local development environment for Navan API integrations with.
Navan Local Dev Loop
Overview
Configure a local development environment for Navan API integrations with token caching, request logging, and mock fixtures. Navan has no sandbox — all API calls hit production, making a structured local setup essential.
Purpose: Establish a safe local dev workflow that minimizes production API calls during iteration.
Prerequisites
- Completed
navan-install-authwith working OAuth 2.0 credentials - Node.js 18+ with
tsxfor TypeScript execution .envfile withNAVANCLIENTID,NAVANCLIENTSECRET,NAVANBASEURL
Instructions
Step 1: Project Structure
Set up a clean project layout that separates concerns:
my-navan-integration/
├── .env # Credentials (NEVER commit)
├── .env.example # Template for teammates
├── .gitignore # Must include .env, .token-cache, logs/
├── src/
│ ├── navan-client.ts # API wrapper (from navan-sdk-patterns)
│ ├── navan-types.ts # Response interfaces
│ └── index.ts # Entry point
├── tests/
│ ├── fixtures/ # Recorded API responses for offline dev
│ │ ├── bookings.json
│ │ └── users.json
│ └── navan-client.test.ts
├── logs/ # Request/response logs (gitignored)
├── .token-cache # Cached OAuth token (gitignored)
├── package.json
└── tsconfig.json
Step 2: Environment Configuration
Create .env.example as a safe template and enforce .gitignore:
# .env.example — commit this file, NOT .env
NAVAN_CLIENT_ID="your-client-id"
NAVAN_CLIENT_SECRET="your-client-secret"
NAVAN_BASE_URL="https://api.navan.com"
NAVAN_LOG_REQUESTS="true"
NAVAN_USE_FIXTURES="false"
# .gitignore additions for Navan projects
echo ".env" >> .gitignore
echo ".token-cache" >> .gitignore
echo "logs/" >> .gitignore
Step 3: Token Cache Implementation
Persist tokens to disk to avoid hitting /ta-auth/oauth/token on every script run:
// src/token-cache.ts
import { readFileSync, writeFileSync, existsSync } from 'fs';
interface CachedToken {
access_token: string;
expires_at: number; // Unix timestamp in ms
}
const CACHE_FILE = '.token-cache';
export function getCachedToken(): string | null {
if (!existsSync(CACHE_FILE)) return null;
try {
const cached: CachedToken = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
if (Date.now() < cached.expires_at - 60_000) {
return cached.access_token;
}
} catch { /* corrupt cache, re-auth */ }
return"Use when planning or executing a migration from SAP Concur or legacy\.
Navan Migration Deep Dive
Overview
End-to-end migration playbook for moving from SAP Concur or legacy travel management systems to Navan. Navan uses REST APIs with OAuth 2.0 — there is no SDK, no automated migration tool, and no sandbox for testing.
Prerequisites
- Admin access to both source system (SAP Concur, legacy TMC) and Navan
- Navan OAuth credentials from Admin > Travel admin > Settings > Integrations
- Data export from source system (expense reports, itineraries, user directory)
- SSO identity provider (Okta, Azure AD, Google Workspace) for user provisioning
- Executive sponsor and travel program manager identified
Instructions
Phase 1 — Discovery and Planning (Weeks 1-2)
Inventory current state:
| Data Category | SAP Concur Source | Navan Target | Migration Method |
|---|---|---|---|
| User profiles | Concur user export (CSV) | SCIM provisioning or /get_users API |
IdP-driven |
| Travel policies | Concur policy rules | Navan admin console | Manual recreation |
| Expense categories | Concur expense types | Navan expense categories | Mapping table |
| Historical bookings | Concur trip export | Archive only (not imported) | Export + cold storage |
| Historical expenses | Concur expense reports | Archive only (not imported) | Export + cold storage |
| Approval workflows | Concur approval chains | Navan approval policies | Manual recreation |
| Corporate card feeds | Concur card integrations | Navan card program or integration | New setup |
Key decision: historical data strategy. Navan does not support importing historical booking or expense data. Export source system data to a data warehouse or archive storage for reference. Future Navan data starts fresh from migration day.
Phase 2 — User Provisioning (Weeks 2-3)
Option A — SCIM provisioning (recommended):
- Configure SCIM connector in your IdP (Okta, Azure AD)
- Map IdP groups to Navan roles: traveler, travel arranger, approver, admin
- Enable provisioning — users are created in Navan automatically
- Test with a pilot group (10-20 users) before full rollout
Option B — CSV bulk upload:
- Export user directory from source system
- Format per Navan's CSV template (available in Admin > User Management)
- Upload via Navan admin console
- Manually verify role assignments
Verify provisioned users via API:
TOKEN=$(curl -s -X POST "h'Set up dev/staging/prod environment separation for Navan integrations.
Navan Multi-Environment Setup
Overview
Navan does not offer a sandbox or staging API — every call hits production data with real corporate bookings and expense records. This creates risk for development and testing: a bug in a sync script could modify live itineraries, and CI pipelines cannot safely run integration tests. This skill implements environment isolation using separate OAuth apps, environment variable validation, a local development proxy, and a CI mock server.
Prerequisites
- Navan admin access to create multiple OAuth apps (Admin > Travel admin > Settings > Integrations)
- Node.js 18+ for proxy and mock server
- Understanding of OAuth 2.0 client credentials flow (see
navan-install-auth) .envmanagement tooling (dotenv, direnv, or cloud secret manager)
Instructions
Step 1: Create Per-Environment OAuth Apps
Create separate API credentials in the Navan admin dashboard for each environment. This provides natural isolation — the dev app can have read-only scopes while production gets full access.
# .env.development — read-only scoped OAuth app
NAVAN_ENV=development
NAVAN_CLIENT_ID=dev-client-id-xxxxx
NAVAN_CLIENT_SECRET=dev-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=true
# .env.staging — read + limited write, separate audit trail
NAVAN_ENV=staging
NAVAN_CLIENT_ID=stg-client-id-xxxxx
NAVAN_CLIENT_SECRET=stg-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=false
# .env.production — full access, rotation-managed
NAVAN_ENV=production
NAVAN_CLIENT_ID=prod-client-id-xxxxx
NAVAN_CLIENT_SECRET=prod-client-secret-xxxxx
NAVAN_API_BASE=https://api.navan.com/v1
NAVAN_READ_ONLY=false
Step 2: Build an Environment-Aware Client
import { config } from 'dotenv';
interface NavanConfig {
env: string;
clientId: string;
clientSecret: string;
apiBase: string;
readOnly: boolean;
}
function loadConfig(): NavanConfig {
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
config({ path: envFile });
const required = ['NAVAN_CLIENT_ID', 'NAVAN_CLIENT_SECRET', 'NAVAN_API_BASE'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing ${key} in ${envFile}`);
}
}
return {
env: process.env.NAVAN_ENV || 'development',
clientId: process.env.NAVAN_CLIENT_ID!,
clientSecret: process.env.NAVAN_CLIENT_SECRET!,
apiBase: process.env.NAVAN_API_BASE!,
readOnly: process.env.NAVAN_READ_ONLY === 'true'
};
}
class NavanClient {
private config: NavanConfig;
private accessToken: string | null = null;
constructor() {
this.config = loadConfig();
console.log(`Navan client initialized [${this.config.env}] readOnly=${this.config.readOnly}`);
}
async r'Use when setting up monitoring, logging, and alerting for Navan API.
Navan Observability
Overview
Navan exposes no built-in API metrics dashboard — monitoring is your responsibility. This skill implements structured logging, latency tracking, error classification, and alerting for Navan REST API integrations. Since Navan uses OAuth 2.0 with token expiration, observability must also cover the authentication lifecycle. Patterns are provided for Datadog, CloudWatch, and Prometheus/Grafana.
Prerequisites
- Running Navan API integration with OAuth 2.0 credentials
- Monitoring platform — Datadog, AWS CloudWatch, or Prometheus/Grafana
- Node.js 18+ or equivalent runtime for the instrumentation middleware
- API base URL:
https://api.navan.com/v1
Instructions
Step 1 — Instrument API Calls with Structured Logging
Wrap every Navan API call with timing, status, and correlation tracking:
import { randomUUID } from 'crypto';
interface NavanApiLog {
event: 'navan.api.request';
correlation_id: string;
method: string;
endpoint: string;
status: number;
duration_ms: number;
error_type?: 'auth' | 'rate_limit' | 'server' | 'client' | 'network';
timestamp: string;
}
async function navanRequest(
method: string,
endpoint: string,
token: string,
body?: object
): Promise<Response> {
const correlationId = randomUUID();
const start = performance.now();
try {
const response = await fetch(`https://api.navan.com/v1/${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Correlation-ID': correlationId,
},
body: body ? JSON.stringify(body) : undefined,
});
const log: NavanApiLog = {
event: 'navan.api.request',
correlation_id: correlationId,
method,
endpoint,
status: response.status,
duration_ms: Math.round(performance.now() - start),
timestamp: new Date().toISOString(),
};
if (response.status === 401) log.error_type = 'auth';
else if (response.status === 429) log.error_type = 'rate_limit';
else if (response.status >= 500) log.error_type = 'server';
else if (response.status >= 400) log.error_type = 'client';
console.log(JSON.stringify(log));
return response;
} catch (err) {
console.log(JSON.stringify({
event: 'navan.api.request',
correlation_id: correlationId,
method,
endpoint,
status: 0,
duration_ms: Math.round(performance.now() - start),
error_type: 'network',
error_message: (err as Error).message,
timestamp: new Date().toISOString(),
}));
throw err;
}
}
Step 2 — Track OAuth Token Lifecycle
<"Use when optimizing Navan API call patterns for high-volume integrations\.
Navan Performance Tuning
Overview
Navan's REST API has no bulk endpoints or GraphQL — every data fetch is a separate HTTP request. High-volume integrations syncing thousands of bookings, expenses, or user records quickly become bottlenecked by sequential API calls, redundant fetches, and naive pagination. This skill provides concrete optimization patterns: response caching with data-type-specific TTLs, parallel request execution with concurrency controls, cursor-based pagination handling, and HTTP connection reuse. Each pattern targets the real constraint: minimizing total API calls while staying under rate limits.
Prerequisites
- Active Navan integration with OAuth 2.0 credentials (client_credentials grant)
- Node.js 18+ (for native fetch and AbortController)
- Understanding of your data volume — bookings/day, users, expense reports/month
- API base URL:
https://api.navan.com/v1
Instructions
Step 1 — Implement Response Caching with Data-Appropriate TTLs
Different Navan data types change at different rates. Cache accordingly:
interface CacheEntry<T> {
data: T;
expires_at: number;
etag?: string;
}
// TTLs based on data volatility
const CACHE_TTL: Record<string, number> = {
'users': 3600_000, // 1 hour — user profiles rarely change
'policies': 86400_000, // 24 hours — travel policies change infrequently
'bookings': 300_000, // 5 minutes — bookings update frequently
'expenses': 600_000, // 10 minutes — expenses change during approval flow
};
const cache = new Map<string, CacheEntry<unknown>>();
async function cachedFetch<T>(
endpoint: string,
token: string
): Promise<T> {
const cacheKey = endpoint;
const entry = cache.get(cacheKey) as CacheEntry<T> | undefined;
// Return cached data if still valid
if (entry && entry.expires_at > Date.now()) {
return entry.data;
}
// Determine TTL from endpoint path
const dataType = endpoint.split('?')[0].split('/')[0];
const ttl = CACHE_TTL[dataType] ?? 300_000; // Default 5 minutes
const response = await fetch(`https://api.navan.com/v1/${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Navan API ${response.status}: ${endpoint}`);
}
const data = await response.json() as T;
cache.set(cacheKey, {
data,
expires_at: Date.now() + ttl,
etag: response.headers.get('etag') ?? undefined,
});
return data;
}
Step 2 — Parallel Fetch with Concurrency Throttling
Fetch multiple resources concurrently without overwhelming rate limits:
async funct"Use when validating production readiness for a Navan API integration\.
Navan Production Checklist
Overview
Gated production readiness verification for Navan REST API integrations. Navan has no SDK and no sandbox — production is the only environment, making this checklist critical.
Prerequisites
- Navan admin access (Admin > Travel admin > Settings)
- OAuth credentials stored in a secret manager (credentials are viewable only once)
- SSO identity provider configured (Okta, Azure AD, or Google Workspace)
curlandjqfor verification commands
Instructions
Domain 1 — Credential Security
- [ ] Secret storage: OAuth
clientidandclientsecretstored in a secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) — never in environment variables, config files, or source control - [ ] Rotation plan documented: Schedule for rotating credentials (recommend 90-day cycle)
- [ ] Zero-downtime rotation tested: Dual-credential swap procedure validated
# Verify current credentials work
curl -s -X POST "https://api.navan.com/ta-auth/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET" \
| jq '{authenticated: (.access_token != null), error: .error}'
Rotation procedure:
- Generate new credentials in Admin > Integrations (old ones remain valid)
- Deploy new credentials to secret manager
- Update application configuration to reference new secret version
- Verify new credentials with
/ta-auth/oauth/token - Revoke old credentials in Admin > Integrations
- Confirm old credentials return HTTP 401
Domain 2 — Error Handling and Alerting
- [ ] All HTTP error codes handled: 400, 401, 403, 404, 429, 500, 502, 503
- [ ] Retry logic with exponential backoff: For 429 and 5xx responses
- [ ] Alert thresholds configured: Error rate > 5% over 5 minutes triggers alert
- [ ] Dead letter queue: Failed API requests stored for retry or manual review
# Health check endpoint pattern
health_check() {
RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/navan-health.json \
-X POST "https://api.navan.com/ta-auth/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET")
if [ "$RESPONSE" = "200" ]; then
echo '{"status":"healthy","navan_api":"reachable"'Implement adaptive rate-limiting for the Navan REST API with exponential.
Navan Rate Limits
Overview
Navan does not publicly document its API rate limits. Developers typically discover thresholds empirically when bulk data pulls or batch operations begin returning HTTP 429 responses. This skill implements defensive rate-limiting patterns that adapt to server responses rather than relying on fixed quotas — inspecting response headers, applying exponential backoff with jitter, and queuing requests to prevent flooding.
Prerequisites
- Active Navan OAuth 2.0 credentials (see
navan-install-auth) - Node.js 18+ (examples use native fetch)
- Understanding of HTTP 429 status code and Retry-After header semantics
Instructions
Step 1: Build an Adaptive Retry Wrapper
interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
async function navanFetch(
url: string,
options: RequestInit,
retry: RetryOptions = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= retry.maxRetries; attempt++) {
const response = await fetch(url, options);
// Log rate limit headers when present (undocumented but sometimes returned)
const remaining = response.headers.get('X-RateLimit-Remaining');
const limit = response.headers.get('X-RateLimit-Limit');
if (remaining !== null) {
console.log(`Rate limit: ${remaining}/${limit} remaining`);
}
if (response.status !== 429) return response;
// Extract delay from Retry-After header or calculate exponential backoff
const retryAfter = response.headers.get('Retry-After');
let delayMs: number;
if (retryAfter) {
delayMs = parseInt(retryAfter, 10) * 1000;
console.log(`429 received — Retry-After: ${retryAfter}s`);
} else {
// Exponential backoff with jitter
delayMs = Math.min(
retry.baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
retry.maxDelayMs
);
console.log(`429 received — backoff: ${Math.round(delayMs)}ms (attempt ${attempt + 1})`);
}
await new Promise(resolve => setTimeout(resolve, delayMs));
lastError = new Error(`Rate limited after ${attempt + 1} attempts`);
}
throw lastError ?? new Error('Max retries exceeded');
}
Step 2: Implement a Request Queue for Bulk Operations
class NavanRequestQueue {
private queue: Array<() => Promise<void>> = [];
private running = 0;
private readonly concurrency: number;
private readonly delayBetweenMs: number;
constructor(concurrency = 3, delayBetweenMs = 500) {
this.concurrency = concurrency;
this.delayBetweenMs = delayBetweenMs;
}
async add<T>(fn: () => Promise<T>): Promise<T> {
while (this.running >= this.conc"Use when designing a production Navan API integration architecture \u2014\.
Navan Reference Architecture
Overview
Production-grade architecture for Navan API integrations. Navan provides raw REST endpoints with OAuth 2.0 — no SDK, no webhooks, no sandbox. This architecture handles those constraints with five purpose-built layers.
Prerequisites
- Navan API credentials from Admin > Travel admin > Settings > Integrations
- Cloud infrastructure (AWS, GCP, or Azure) for hosting integration services
- Data warehouse for BOOKING and TRANSACTION tables
- Understanding of OAuth 2.0 client credentials flow
Instructions
Architecture Overview
┌──────────────────────────────────────────────────────────────────┐
│ CONSUMERS │
│ Travel Dashboard │ Expense Reports │ Finance System │
└────────┬────────────┴────────┬──────────┴────────┬───────────────┘
│ │ │
┌────────▼─────────────────────▼────────────────────▼───────────────┐
│ LAYER 1: API GATEWAY │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ Rate Limiter │ │ Request Log │ │ Circuit Breaker (5xx) │ │
│ └─────────────┘ └──────────────┘ └─────────────────────────┘ │
└────────┬─────────────────────────────────────────────────────────┘
│
┌────────▼─────────────────────────────────────────────────────────┐
│ LAYER 2: TOKEN MANAGEMENT SERVICE │
│ ┌──────────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ OAuth Client Cred │ │ Token Cache │ │ Auto-Refresh │ │
│ │ POST /ta-auth/ │ │ (Redis/KMS) │ │ (before expiry) │ │
│ └──────────────────┘ └──────────────┘ └────────────────────┘ │
└────────┬─────────────────────────────────────────────────────────┘
│
┌────────▼─────────────────────────────────────────────────────────┐
│ LAYER 3: NAVAN API CLIENT │
│ ┌───────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ /get_user_trips│ │ /get_users │ │ /get_admin_trips │ │
│ │ /get_invoices │ │ /get_itin_pdf│ │ /reauthenticate │ │
│ └───────────────┘ └──────────────┘ └────────────────────────┘ │
└────────┬────────────────────┬────────────────────────────────────┘
│ │
┌────────▼──────────┐ ┌──────▼─────────────────────────────────────┐
│ LAYER 4: DATA │ │ LAYER 5: MONITORING │
│ SYNC PIPELINE │ │ ┌──────────┐ ┌─────────┐ ┌────────────┐ │
│ ┌───────────────┐ │ │ │ API Call │ │ Error │ │ Token │ │
│ │ Fivetran / │ │ │ │ Metrics │ │ Alerts │ │ Expiry │ │
│ │ Airbyte / │ │ │ │ (volume, │ │ (PD/ │ │ Monitor │ │
│ │ Estuary │ │ │ │ latency)│ │ Slack) │ │ │ │
│ ├───────────────┤ │ │ └──────────┘ └─────────┘ └────────────┘ │
│ │ BOOKING tabl'Build a typed API wrapper around Navan REST endpoints since no official.
Navan SDK Patterns
Overview
Build a typed API wrapper around Navan REST endpoints since no official SDK exists (@navan/sdk is not a real package). These patterns provide automatic token lifecycle management, typed responses, retry middleware, and centralized error handling.
Purpose: Create a reusable NavanAPI class encapsulating authentication, request handling, and error recovery.
Prerequisites
- Completed
navan-install-authwith working OAuth 2.0 credentials - TypeScript 5+ project with
dotenvinstalled - Familiarity with the Navan endpoints from
navan-hello-world
Instructions
Step 1: Define Response Interfaces
Type the known API response shapes so every call returns structured data:
// navan-types.ts
export interface NavanTrip {
uuid: string;
traveler_name: string;
origin: string;
destination: string;
departure_date: string;
return_date: string;
booking_status: string;
booking_type: 'flight' | 'hotel' | 'car';
}
export interface NavanUser {
id: string;
email: string;
first_name: string;
last_name: string;
department: string;
role: string;
}
export interface NavanApiError {
status: number;
message: string;
endpoint: string;
timestamp: string;
}
Step 2: Build the NavanAPI Wrapper Class
Create a singleton client with automatic token management:
// navan-client.ts
import 'dotenv/config';
import type { NavanTrip, NavanUser, NavanApiError } from './navan-types';
export class NavanAPI {
private baseUrl: string;
private clientId: string;
private clientSecret: string;
private accessToken: string | null = null;
private tokenExpiry: number = 0;
constructor() {
this.baseUrl = process.env.NAVAN_BASE_URL ?? 'https://api.navan.com';
this.clientId = process.env.NAVAN_CLIENT_ID ?? '';
this.clientSecret = process.env.NAVAN_CLIENT_SECRET ?? '';
if (!this.clientId || !this.clientSecret) {
throw new Error('NAVAN_CLIENT_ID and NAVAN_CLIENT_SECRET must be set');
}
}
/** Acquire or refresh the OAuth 2.0 bearer token */
private async authenticate(): Promise<string> {
if (this.accessToken && Date.now() < this.tokenExpiry) {
return this.accessToken;
}
const response = await fetch(`${this.baseUrl}/ta-auth/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
if (!response.ok) {
throw this.toApiError(response.status, 'Authentication failed', '/t'Secure Navan API credentials with OAuth 2.
Navan Security Basics
Overview
Navan holds SOC 1 Type II, SOC 2 Type II, ISO 27001, PCI DSS Level 1, GDPR, CSA, and VSA certifications. Infrastructure runs on AWS with TLS encryption in transit and AES encryption at rest via KMS. Annual penetration testing and OWASP compliance are standard. This skill covers the developer's responsibility: securing OAuth 2.0 credentials, configuring SSO through supported identity providers, setting up SCIM for automated user provisioning, and establishing rotation schedules.
Prerequisites
- Navan admin account with API credential management permissions
- Access to Admin > Travel admin > Settings > Integrations for OAuth app creation
- Identity provider admin access (Okta, Azure AD, or Google Workspace) for SSO/SCIM setup
- Node.js 18+ or Python 3.8+ for credential management scripts
Instructions
Step 1: Secure OAuth 2.0 Credential Storage
# Create .env file — NEVER commit this
cat > .env << 'EOF'
NAVAN_CLIENT_ID=your-client-id
NAVAN_CLIENT_SECRET=your-client-secret
NAVAN_TOKEN_URL=https://api.navan.com/ta-auth/oauth/token
EOF
# Ensure .env is gitignored
echo '.env' >> .gitignore
echo '.env.*' >> .gitignore
// Load credentials from environment only — never hardcode
import { config } from 'dotenv';
config();
async function getAccessToken(): Promise<string> {
const { NAVAN_CLIENT_ID, NAVAN_CLIENT_SECRET, NAVAN_TOKEN_URL } = process.env;
if (!NAVAN_CLIENT_ID || !NAVAN_CLIENT_SECRET) {
throw new Error('Missing NAVAN_CLIENT_ID or NAVAN_CLIENT_SECRET in environment');
}
const response = await fetch(NAVAN_TOKEN_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: NAVAN_CLIENT_ID,
client_secret: NAVAN_CLIENT_SECRET
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: HTTP ${response.status}`);
}
const { access_token, expires_in } = await response.json();
console.log(`Token acquired — expires in ${expires_in}s`);
return access_token;
}
Step 2: Implement Credential Rotation
// Rotation script — run on a schedule (e.g., monthly cron)
async function rotateCredentials(adminToken: string): Promise<void> {
// Step 1: Create new credentials
const createRes = await fetch('https://api.navan.com/v1/api-credentials', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: `rotation-${new Date().toISOString().slice(0, 10)}` })
});
const newCreds = await createRes.jso"Use when handling Navan API changes in production \u2014 defensive coding\.
Navan Upgrade Migration
Overview
Defensive patterns for maintaining Navan API integrations over time. Navan does not publicly version their API, publish a changelog, or guarantee backward compatibility. Every API response should be treated as potentially different from the last.
Prerequisites
- Existing Navan API integration in production
- OAuth credentials (
clientid,clientsecret) stored in a secret manager - Baseline API response snapshots for comparison (see Step 1)
curl,jq, anddifffor schema comparison
Instructions
Step 1 — Capture Response Baselines
Store known-good API responses as reference schemas. Compare against these regularly to detect drift.
TOKEN=$(curl -s -X POST "https://api.navan.com/ta-auth/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$NAVAN_CLIENT_ID&client_secret=$NAVAN_CLIENT_SECRET" \
| jq -r '.access_token')
BASELINE_DIR="navan-api-baselines/$(date +%Y%m%d)"
mkdir -p "$BASELINE_DIR"
# Capture response structure (keys only, no values)
for ENDPOINT in users bookings; do
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.navan.com/v1/${ENDPOINT}?page=0&size=1" \
| jq '[.data[] | keys] | .[0]' > "$BASELINE_DIR/${ENDPOINT}-schema.json" 2>/dev/null
echo "Captured: $ENDPOINT → $(cat "$BASELINE_DIR/${ENDPOINT}-schema.json" | jq length) fields"
done
Step 2 — Schema Drift Detection
Run this periodically (daily cron or CI pipeline) to detect API changes:
LATEST_BASELINE=$(ls -d navan-api-baselines/*/ | sort | tail -1)
for ENDPOINT in users bookings; do
CURRENT=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.navan.com/v1/${ENDPOINT}?page=0&size=1" \
| jq '[.data[] | keys] | .[0]' 2>/dev/null)
BASELINE=$(cat "${LATEST_BASELINE}${ENDPOINT}-schema.json" 2>/dev/null)
# Compare field sets
ADDED=$(comm -13 <(echo "$BASELINE" | jq -r '.[]' | sort) <(echo "$CURRENT" | jq -r '.[]' | sort))
REMOVED=$(comm -23 <(echo "$BASELINE" | jq -r '.[]' | sort) <(echo "$CURRENT" | jq -r '.[]' | sort))
[ -n "$ADDED" ] && echo "WARNING: $ENDPOINT has NEW fields: $ADDED"
[ -n "$REMOVED" ] && echo "CRITICAL: $ENDPOINT has REMOVED fields: $REMOVED"
[ -z "$ADDED" ] && [ -z "$REMOVED" ] && echo "OK: $ENDPOINT schema unchanged"
done
Step 3 — Defensive Response Parsing
Never assume a fix
'Set up webhook listeners for real-time Navan event notifications.
Navan Webhooks & Events
Overview
Navan supports asynchronous event delivery via callback URLs registered through the REST API. Since there is no public SDK, webhook handlers receive raw HTTP POST requests with JSON payloads. This skill covers endpoint setup, payload verification, event routing, and idempotent processing for the key event types: booking lifecycle, expense workflow, and travel disruptions.
Prerequisites
- Active Navan account with API credentials (Admin > Travel admin > Settings > Integrations)
- OAuth 2.0 access token (see
navan-install-auth) - Publicly accessible HTTPS endpoint for callback URL (use ngrok for local development)
- Node.js 18+ with Express or equivalent HTTP framework
Instructions
Step 1: Register a Webhook Callback URL
const response = await fetch('https://api.navan.com/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://your-domain.com/webhooks/navan',
events: [
'booking.created',
'booking.updated',
'booking.cancelled',
'expense.submitted',
'expense.approved',
'expense.rejected',
'trip.disrupted'
],
secret: process.env.NAVAN_WEBHOOK_SECRET
})
});
const webhook = await response.json();
console.log('Webhook registered:', webhook.id);
Step 2: Verify Payload Signatures
import crypto from 'node:crypto';
function verifySignature(payload: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Step 3: Build the Webhook Handler
import express from 'express';
const app = express();
// Track processed events for idempotency
const processedEvents = new Set<string>();
app.post('/webhooks/navan', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-navan-signature'] as string;
const rawBody = req.body.toString();
// Verify authenticity
if (!verifySignature(rawBody, signature, process.env.NAVAN_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Acknowledge immediately — process async
res.status(200).json({ received: true });
const event = JSON.parse(rawBody);
// Idempotency check — Navan may retry on timeout
if (processedEvents.has(event.id)) return;
processedEvents.add(event.id);
// Route by event type
switchHow It Works
Skills trigger automatically when you discuss Navan topics.
Ready to use navan-pack?
Related Plugins
supabase-pack
Complete Supabase integration skill pack with 30 skills covering authentication, database, storage, realtime, edge functions, and production operations. Flagship+ tier vendor pack.
/plugin install supabase-pack@claude-code-plugins-plus
vercel-pack
Complete Vercel integration skill pack with 30 skills covering deployments, edge functions, preview environments, performance optimization, and production operations. Flagship+ tier vendor pack.
/plugin install vercel-pack@claude-code-plugins-plus
clay-pack
Complete Clay integration skill pack with 30 skills covering data enrichment, waterfall workflows, AI agents, and GTM automation. Flagship+ tier vendor pack.
/plugin install clay-pack@claude-code-plugins-plus
cursor-pack
Complete Cursor integration skill pack with 30 skills covering AI code editing, composer workflows, codebase indexing, and productivity features. Flagship+ tier vendor pack.
/plugin install cursor-pack@claude-code-plugins-plus
exa-pack
Complete Exa integration skill pack with 30 skills covering neural search, semantic retrieval, web search API, and AI-powered discovery. Flagship+ tier vendor pack.
/plugin install exa-pack@claude-code-plugins-plus
firecrawl-pack
Complete Firecrawl integration skill pack with 30 skills covering web scraping, crawling, markdown conversion, and LLM-ready data extraction. Flagship+ tier vendor pack.
/plugin install firecrawl-pack@claude-code-plugins-plus