Claude-code-plugins-plus-skills navan-sdk-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/navan-pack/skills/navan-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-navan-sdk-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/navan-pack/skills/navan-sdk-patterns/SKILL.mdsource content
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
with working OAuth 2.0 credentialsnavan-install-auth - TypeScript 5+ project with
installeddotenv - 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', '/ta-auth/oauth/token'); } const data = await response.json(); this.accessToken = data.access_token; // Buffer 60 seconds before actual expiry this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000; return this.accessToken!; } /** Core request method with auth, retries, and error handling */ private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { const token = await this.authenticate(); const url = `${this.baseUrl}${endpoint}`; for (let attempt = 0; attempt < 3; attempt++) { const response = await fetch(url, { ...options, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers, }, }); if (response.ok) return response.json() as Promise<T>; // Retry on 429 (rate limit) and 503 (maintenance) if ((response.status === 429 || response.status === 503) && attempt < 2) { const delay = Math.pow(2, attempt + 1) * 1000; // 2s, 4s await new Promise((r) => setTimeout(r, delay)); continue; } // Re-auth on 401 (expired token) if (response.status === 401 && attempt < 2) { this.accessToken = null; this.tokenExpiry = 0; continue; } throw this.toApiError(response.status, await response.text(), endpoint); } throw this.toApiError(0, 'Max retries exceeded', endpoint); } private toApiError(status: number, message: string, endpoint: string): NavanApiError { return { status, message, endpoint, timestamp: new Date().toISOString() }; } // --- Public API methods --- async getBookings(page = 0, size = 50): Promise<NavanTrip[]> { const data = await this.request<{ data: NavanTrip[] }>(`/v1/bookings?page=${page}&size=${size}`); return data.data ?? []; } async getUsers(): Promise<NavanUser[]> { const data = await this.request<{ data: NavanUser[] }>('/v1/users'); return data.data ?? []; } }
Step 3: Implement a Singleton Factory
Ensure one client instance per process to reuse the token:
// navan-singleton.ts import { NavanAPI } from './navan-client'; let instance: NavanAPI | null = null; export function getNavanClient(): NavanAPI { if (!instance) instance = new NavanAPI(); return instance; } // Usage const navan = getNavanClient(); const bookings = await navan.getBookings(); const users = await navan.getUsers();
Step 4: Add Error Handling Middleware
Wrap API calls with structured error handling for calling code:
// navan-safe.ts import type { NavanApiError } from './navan-types'; type Result<T> = { ok: true; data: T } | { ok: false; error: NavanApiError }; export async function safeCall<T>(fn: () => Promise<T>): Promise<Result<T>> { try { const data = await fn(); return { ok: true, data }; } catch (err) { const apiError = err as NavanApiError; console.error(`Navan API error [${apiError.status}] ${apiError.endpoint}: ${apiError.message}`); return { ok: false, error: apiError }; } } // Usage const result = await safeCall(() => navan.getUserTrips()); if (result.ok) { console.log(`Found ${result.data.length} trips`); } else { console.error(`Failed: ${result.error.message}`); }
Step 5: Python Equivalent Pattern
# navan_client.py import os import time import requests from dataclasses import dataclass from dotenv import load_dotenv load_dotenv() @dataclass class NavanAPIError(Exception): status: int message: str endpoint: str class NavanAPI: def __init__(self): self.base_url = os.environ.get("NAVAN_BASE_URL", "https://api.navan.com") self.client_id = os.environ["NAVAN_CLIENT_ID"] self.client_secret = os.environ["NAVAN_CLIENT_SECRET"] self._token: str | None = None self._token_expiry: float = 0 def _authenticate(self) -> str: if self._token and time.time() < self._token_expiry: return self._token resp = requests.post(f"{self.base_url}/ta-auth/oauth/token", data={ "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, }) resp.raise_for_status() data = resp.json() self._token = data["access_token"] self._token_expiry = time.time() + data.get("expires_in", 3600) - 60 return self._token def _request(self, method: str, endpoint: str, **kwargs) -> dict: token = self._authenticate() for attempt in range(3): resp = requests.request(method, f"{self.base_url}{endpoint}", headers={"Authorization": f"Bearer {token}"}, **kwargs) if resp.ok: return resp.json() if resp.status_code in (429, 503) and attempt < 2: time.sleep(2 ** (attempt + 1)) continue if resp.status_code == 401 and attempt < 2: self._token = None token = self._authenticate() continue raise NavanAPIError(resp.status_code, resp.text, endpoint) raise NavanAPIError(0, "Max retries exceeded", endpoint) def get_bookings(self, page: int = 0, size: int = 50) -> list[dict]: return self._request("GET", f"/v1/bookings?page={page}&size={size}").get("data", []) def get_users(self) -> list[dict]: return self._request("GET", "/v1/users").get("data", [])
Output
Successful implementation produces:
- A typed
class with automatic token refresh and retry logicNavanAPI - Response interfaces for trips, users, and errors
- A singleton factory for client reuse across the application
- A
wrapper for structured error handling in calling codesafeCall
Error Handling
| Error | Code | Cause | Solution |
|---|---|---|---|
| Invalid credentials | 401 | Expired token or wrong secrets | Client auto-retries with fresh token; check .env |
| Forbidden | 403 | Insufficient permissions or wrong tier | Verify admin role; check Navan plan tier |
| Not found | 404 | Invalid endpoint path | Verify endpoint against known paths in this guide |
| Rate limited | 429 | Too many requests | Client auto-retries with exponential backoff |
| Server error | 500 | Navan service issue | Client auto-retries; escalate if persistent |
| Maintenance | 503 | Navan downtime | Client auto-retries; check for scheduled windows |
Examples
Fetch all bookings with error handling:
const navan = getNavanClient(); const result = await safeCall(() => navan.getBookings()); if (result.ok) { const flights = result.data.filter((t) => t.booking_type === 'flight'); console.log(`${flights.length} flight bookings found`); }
Resources
- Navan Help Center — primary documentation hub
- Navan TMC API Docs — endpoint reference
- Navan Integrations — integration partner ecosystem
- Navan Security — SOC 2 Type II, ISO 27001, PCI DSS Level 1
Next Steps
With your typed wrapper in place, see
navan-common-errors for a comprehensive error reference, or navan-local-dev-loop to set up token caching and request logging for development.