Skip to main content Inicio Creadores jeremylongshore tons-of-skills-marketplace navan-sdk-patterns
navan-sdk-patterns Build a typed API wrapper around Navan REST endpoints since no official SDK exists.
Use when you need production-grade API access with auto token refresh, retry logic, and typed responses.
Trigger with "navan sdk patterns", "navan api wrapper", "navan client class", "navan typed client".
Ir a la instalación Skills Marketplace Descubre y explora habilidades de IA creadas por la comunidad.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Copiar promptMostrar detalles del prompt Un comando directo omite el prompt de revisión. Revisa el origen antes de ejecutarlo.
npx skills add https://github.com/jeremylongshore/tons-of-skills-marketplace --skill navan-sdk-patternsEl comando permanece en una sola línea. Desplázate horizontalmente para revisarlo antes de copiarlo.
¿Prefieres una copia local? Descarga los archivos que SkillsMP tiene disponibles ahora.
Descargar Zip Descargando...
langchain-deploy-integration Deploy a LangChain 1.0 / LangGraph 1.0 app to Cloud Run, Vercel, or LangServe correctly — with timeouts sized for chain length, cold-start mitigation, SSE anti-buffering headers, and Secret Manager over .env. Use when prepping a first production deploy, debugging a stream that hangs behind a proxy, or diagnosing p99 latency spikes. Trigger with "langchain deploy", "langchain cloud run", "langchain vercel python", "langchain langserve", or "langchain docker".
Explorador de archivos
2 archivos name navan-sdk-patterns description Build a typed API wrapper around Navan REST endpoints since no official SDK exists.
Use when you need production-grade API access with auto token refresh, retry logic, and typed responses.
Trigger with "navan sdk patterns", "navan api wrapper", "navan client class", "navan typed client".
allowed-tools Read, Write, Edit, Bash(npm:*), Grep version 1.7.0 license MIT author Jeremy Longshore <jeremy@intentsolutions.io> tags ["saas","navan","travel"] compatibility Designed for Claude Code
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-auth with working OAuth 2.0 credentials
TypeScript 5+ project with dotenv installed
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:
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:
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' );
}
}
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 ;
this .tokenExpiry = Date .now () + (data.expires_in - 60 ) * 1000 ;
return this .accessToken !;
}
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>;
if ((response.status === 429 || response.status === 503 ) && attempt < 2 ) {
const delay = Math .pow (2 , attempt + 1 ) * 1000 ;
await new Promise ((r ) => setTimeout (r, delay));
continue ;
}
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 () };
}
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:
import { NavanAPI } from './navan-client' ;
let instance : NavanAPI | null = null ;
export function getNavanClient ( ): NavanAPI {
if (!instance) instance = new NavanAPI ();
return instance;
}
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:
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 };
}
}
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
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 NavanAPI class with automatic token refresh and retry logic
Response interfaces for trips, users, and errors
A singleton factory for client reuse across the application
A safeCall wrapper for structured error handling in calling code
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
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.