Skip to main content

Client Usage

The SDK gives you three ways to call epilot APIs, from simplest to most flexible.

Call operation methods directly on the SDK client. The SDK handles client creation, caching, and auth for you.

import { epilot } from '@epilot/sdk'

epilot.authorize(() => '<my-token>')

const { data: entity } = await epilot.entity.getEntity({ slug: 'contact', id: '123' })
const { data: user } = await epilot.user.getMe()

Operation methods always use the latest config. If you call epilot.authorize() with a new token, every subsequent call picks it up automatically.

Function argumentsโ€‹

Every operation method accepts up to three positional arguments:

client.operationId(parameters?, data?, config?)
ArgumentPurposeExample
parametersPath and query parameters{ slug: 'contact', id: '123' }
dataRequest body (JSON payload, FormData, etc.){ name: 'John' }
configAxios request config overrides{ headers: { 'x-custom': 'value' } }

When the operation has path/query params and a body, pass both:

// PATCH /v1/entity/{slug}/{id}
await epilot.entity.patchEntity(
{ slug: 'contact', id: '123' }, // parameters
{ name: 'Jane Doe' }, // request body
)

When the operation has path/query params but no body, pass only parameters:

// GET /v1/entity/{slug}/{id}
await epilot.entity.getEntity({ slug: 'contact', id: '123' })

When the operation has a body but no path/query params, pass null as the first argument:

// POST /v1/entity/search
await epilot.entity.searchEntities(
null, // no parameters โ€” pass null
{ q: '_schema:contact AND John' }, // request body
)
Forgetting to pass null

This is a common mistake. If you skip the first argument, the request body gets interpreted as parameters and the API call will fail silently or return unexpected results:

// Wrong โ€” body is treated as parameters
await epilot.entity.searchEntities({ q: '_schema:contact AND John' })

// Correct โ€” null tells the client "no parameters"
await epilot.entity.searchEntities(null, { q: '_schema:contact AND John' })

When the operation has no params and no body (e.g. a simple GET), call with no arguments:

await epilot.user.getMe()

IntelliSense and typesโ€‹

Operation methods are fully typed. Your editor will autocomplete operation names, parameter keys, request bodies, and response shapes.

// Parameters and response are both typed โ€” hover to see the shape
const { data } = await epilot.entity.getEntity({ slug: 'contact', id: '123' })
// ^? { entity: Entity; relations: Entity[] }

You can also import the generated types directly for use in your own code:

import type { Client, Entity, EntityItem } from '@epilot/sdk/entity'
Prefer operation methods over raw axios methods

Operation methods give you type safety and autocomplete. Raw axios calls don't.

// Good: typed params, typed response
const { data } = await epilot.entity.getEntity({ slug: 'contact', id: '123' })

// Avoid: no type safety, easy to mistype paths
const { data } = await entityClient.get('/v2/entity/schemas/contact/123')

Tree-shakeable importsโ€‹

Import only the APIs you use. Other APIs never touch your bundle.

import { getClient, authorize } from '@epilot/sdk/entity'

const entityClient = getClient()
authorize(entityClient, () => '<my-token>')

const { data } = await entityClient.getEntity({ slug: 'contact', id: '123' })

Each API subpath also exports a handle for calling operation methods directly:

import { entity } from '@epilot/sdk/entity'

const { data } = await entity.getEntity({ slug: 'contact', id: '123' })

Multiple SDK instancesโ€‹

Create isolated SDK instances when you need to work with multiple orgs or tokens simultaneously.

import { createSDK } from '@epilot/sdk'

const sdk1 = createSDK()
sdk1.authorize(() => '<token-for-org-1>')
sdk1.headers({ 'x-epilot-org-id': 'org-1' })

const sdk2 = createSDK()
sdk2.authorize(() => '<token-for-org-2>')
sdk2.headers({ 'x-epilot-org-id': 'org-2' })

Interceptorsโ€‹

Clients are standard axios instances, so you can use interceptors for logging, tracing, or request transformation.

Per-client interceptor:

entityClient.interceptors.response.use((response) => {
console.debug(`${response.config.method?.toUpperCase()} ${response.config.url}`, {
status: response.status,
data: response.data,
})
return response
})

Global interceptor (applied to all clients):

epilot.interceptors.request((config) => {
config.headers['x-correlation-id'] = generateTraceId()
return config
})

Client lifecycleโ€‹

When you call authorize(), headers(), retry(), largeResponse(), or interceptors, the SDK invalidates all cached clients. The next operation creates a fresh client with the updated config.

PatternAlways up to date?Notes
epilot.entity.getEntity(...)YesRe-resolves the client on every call
epilot.entity.getClient()At time of callCan go stale after config changes
createClient()IndependentNot affected by SDK config changes

Cached client referenceโ€‹

Use getClient() when you need direct access to the underlying axios instance, for example to set custom headers or add interceptors.

const entityClient = epilot.entity.getClient()
const { data } = await entityClient.getEntity({ slug: 'contact', id: '123' })

getClient() is synchronous and returns a cached singleton. Calling it multiple times returns the same instance (until config changes).

Stale references

If you hold a client reference and then change config, your reference still points to the old client:

const entityClient = epilot.entity.getClient()

epilot.authorize(() => 'new-token') // invalidates cached clients

// entityClient still uses the old token!
// epilot.entity.getEntity(...) already uses the new one

Re-call getClient() after config changes, or use operation methods directly to avoid this entirely.

Fresh client instanceโ€‹

Use createClient() when you need a completely independent client that is not shared or cached. Useful for one-off requests with different credentials or headers.

import { createClient, authorize } from '@epilot/sdk/entity'

const entityClient = createClient()
authorize(entityClient, () => '<my-token>')
entityClient.defaults.headers.common['x-epilot-org-id'] = 'org-123'

Migration from @epilot/*-clientโ€‹

Drop-in replacement โ€” just change the import path:

// Before
import { getClient, createClient, authorize } from '@epilot/entity-client'
import type { Client, Entity } from '@epilot/entity-client'

// After
import { getClient, createClient, authorize } from '@epilot/sdk/entity'
import type { Client, Entity } from '@epilot/sdk/entity'