Client Usage
The SDK gives you three ways to call epilot APIs, from simplest to most flexible.
Operation methods (recommended)โ
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?)
| Argument | Purpose | Example |
|---|---|---|
parameters | Path and query parameters | { slug: 'contact', id: '123' } |
data | Request body (JSON payload, FormData, etc.) | { name: 'John' } |
config | Axios 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.
| Pattern | Always up to date? | Notes |
|---|---|---|
epilot.entity.getEntity(...) | Yes | Re-resolves the client on every call |
epilot.entity.getClient() | At time of call | Can go stale after config changes |
createClient() | Independent | Not 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'