<h1 align="center"><img alt="epilot" src="https://raw.githubusercontent.com/epilot-dev/sdk-js/main/logo.png" width="200"><br>@epilot/sdk</h1>

<p align="center">
  <a href="https://github.com/epilot-dev/sdk-js/actions?query=workflow%3ACI"><img src="https://github.com/epilot-dev/sdk-js/workflows/CI/badge.svg" alt="CI"></a>
  <a href="https://www.npmjs.com/package/@epilot/sdk"><img src="https://img.shields.io/npm/v/@epilot/sdk.svg" alt="npm version"></a>
  <a href="https://github.com/epilot-dev/sdk-js/blob/main/"><img src="http://img.shields.io/:license-mit-blue.svg" alt="License"></a>
</p>

<p align="center">JavaScript/TypeScript SDK for epilot APIs. Full types, tree-shakeable imports, and lazy-loaded OpenAPI specs.</p>

## Install

```bash
npm i @epilot/sdk axios openapi-client-axios
```

## Quick Start

```ts
import { epilot } from '@epilot/sdk'

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

const { data: entity } = await epilot.entity.createEntity(
  { slug: 'contact' },
  { first_name: 'John', last_name: 'Doe' },
)

const { data: file } = await epilot.file.getFile({ id: 'file-123' })

const { data: executions } = await epilot.workflow.getExecutions()
```

API clients are built on [openapi-client-axios](https://openapistack.co/docs/openapi-client-axios/intro/), which generates fully typed operation methods on top of regular [axios](https://axios-http.com/docs/intro) instances. All standard axios features (interceptors, defaults, config) work as expected. Each operation is forwarded to a lazy singleton — the spec is loaded and the client initialized on first use, then cached.

Full API documentation: [https://docs.epilot.io/api](https://docs.epilot.io/api)

## API Reference

<!-- api-reference-table -->
| API | Import | Docs |
| --- | ------ | ---- |
| `epilot.accessToken` | `@epilot/sdk/access-token` | [docs](./docs/access-token.md) |
| `epilot.address` | `@epilot/sdk/address` | [docs](./docs/address.md) |
| `epilot.addressSuggestions` | `@epilot/sdk/address-suggestions` | [docs](./docs/address-suggestions.md) |
| `epilot.aiAgents` | `@epilot/sdk/ai-agents` | [docs](./docs/ai-agents.md) |
| `epilot.app` | `@epilot/sdk/app` | [docs](./docs/app.md) |
| `epilot.auditLogs` | `@epilot/sdk/audit-logs` | [docs](./docs/audit-logs.md) |
| `epilot.automation` | `@epilot/sdk/automation` | [docs](./docs/automation.md) |
| `epilot.billing` | `@epilot/sdk/billing` | [docs](./docs/billing.md) |
| `epilot.blueprintManifest` | `@epilot/sdk/blueprint-manifest` | [docs](./docs/blueprint-manifest.md) |
| `epilot.consent` | `@epilot/sdk/consent` | [docs](./docs/consent.md) |
| `epilot.customerPortal` | `@epilot/sdk/customer-portal` | [docs](./docs/customer-portal.md) |
| `epilot.dashboard` | `@epilot/sdk/dashboard` | [docs](./docs/dashboard.md) |
| `epilot.dataGovernance` | `@epilot/sdk/data-governance` | [docs](./docs/data-governance.md) |
| `epilot.deduplication` | `@epilot/sdk/deduplication` | [docs](./docs/deduplication.md) |
| `epilot.design` | `@epilot/sdk/design` | [docs](./docs/design.md) |
| `epilot.document` | `@epilot/sdk/document` | [docs](./docs/document.md) |
| `epilot.emailSettings` | `@epilot/sdk/email-settings` | [docs](./docs/email-settings.md) |
| `epilot.emailTemplate` | `@epilot/sdk/email-template` | [docs](./docs/email-template.md) |
| `epilot.entity` | `@epilot/sdk/entity` | [docs](./docs/entity.md) |
| `epilot.entityMapping` | `@epilot/sdk/entity-mapping` | [docs](./docs/entity-mapping.md) |
| `epilot.environments` | `@epilot/sdk/environments` | [docs](./docs/environments.md) |
| `epilot.eventCatalog` | `@epilot/sdk/event-catalog` | [docs](./docs/event-catalog.md) |
| `epilot.file` | `@epilot/sdk/file` | [docs](./docs/file.md) |
| `epilot.iban` | `@epilot/sdk/iban` | [docs](./docs/iban.md) |
| `epilot.integrationToolkit` | `@epilot/sdk/integration-toolkit` | [docs](./docs/integration-toolkit.md) |
| `epilot.journey` | `@epilot/sdk/journey` | [docs](./docs/journey.md) |
| `epilot.kanban` | `@epilot/sdk/kanban` | [docs](./docs/kanban.md) |
| `epilot.message` | `@epilot/sdk/message` | [docs](./docs/message.md) |
| `epilot.metering` | `@epilot/sdk/metering` | [docs](./docs/metering.md) |
| `epilot.notes` | `@epilot/sdk/notes` | [docs](./docs/notes.md) |
| `epilot.notification` | `@epilot/sdk/notification` | [docs](./docs/notification.md) |
| `epilot.organization` | `@epilot/sdk/organization` | [docs](./docs/organization.md) |
| `epilot.partnerDirectory` | `@epilot/sdk/partner-directory` | [docs](./docs/partner-directory.md) |
| `epilot.permissions` | `@epilot/sdk/permissions` | [docs](./docs/permissions.md) |
| `epilot.pricing` | `@epilot/sdk/pricing` | [docs](./docs/pricing.md) |
| `epilot.pricingTier` | `@epilot/sdk/pricing-tier` | [docs](./docs/pricing-tier.md) |
| `epilot.purpose` | `@epilot/sdk/purpose` | [docs](./docs/purpose.md) |
| `epilot.sandbox` | `@epilot/sdk/sandbox` | [docs](./docs/sandbox.md) |
| `epilot.sharing` | `@epilot/sdk/sharing` | [docs](./docs/sharing.md) |
| `epilot.submission` | `@epilot/sdk/submission` | [docs](./docs/submission.md) |
| `epilot.targeting` | `@epilot/sdk/targeting` | [docs](./docs/targeting.md) |
| `epilot.templateVariables` | `@epilot/sdk/template-variables` | [docs](./docs/template-variables.md) |
| `epilot.user` | `@epilot/sdk/user` | [docs](./docs/user.md) |
| `epilot.validationRules` | `@epilot/sdk/validation-rules` | [docs](./docs/validation-rules.md) |
| `epilot.webhooks` | `@epilot/sdk/webhooks` | [docs](./docs/webhooks.md) |
| `epilot.workflow` | `@epilot/sdk/workflow` | [docs](./docs/workflow.md) |
| `epilot.workflowDefinition` | `@epilot/sdk/workflow-definition` | [docs](./docs/workflow-definition.md) |
<!-- /api-reference-table -->

## OpenAPI Spec

Retrieve the full OpenAPI specification for any API at runtime. The spec is lazy-loaded on first call and cached.

```ts
import { epilot } from '@epilot/sdk'

// Via API handle
const entitySpec = await epilot.entity.openapi()
console.log(entitySpec.info.title) // "Entity API"
console.log(entitySpec.paths)       // all paths with full schemas

// Via top-level method
const spec = await epilot.openapi('entity')
```

For tree-shakeable imports:

```ts
import { openapi } from '@epilot/sdk/entity'

const spec = await openapi()
```

## Explicit Client Access

```ts
import { epilot } from '@epilot/sdk'

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

// Get the cached singleton client
const entityClient = epilot.entity.getClient()
const { data } = await entityClient.getEntity({ slug: 'contact', id: '123' })

// Create a fresh (non-singleton) client instance
const freshClient = epilot.entity.createClient()
authorize(freshClient, () => '<my-token>')
```

## Tree-Shakeable Imports

Import only what you need. Other APIs never touch your bundle.

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

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

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

// Or use the handle for direct operation forwarding
import { entity } from '@epilot/sdk/entity'
const { data } = await entity.getEntity({ slug: 'contact', id: '123' })
```

## Types

Each API subpath re-exports all schema types generated from the OpenAPI spec. Import them directly:

```ts
import type { Entity, EntitySchema, RelationAttribute } from '@epilot/sdk/entity'
import type { FileItem } from '@epilot/sdk/file'
import type { AutomationFlow } from '@epilot/sdk/automation'
```

The `Client`, `OperationMethods`, and `PathsDictionary` types are also available for typing client instances:

```ts
import type { Client } from '@epilot/sdk/entity'

const entityClient: Client = epilot.entity.getClient()
```

## Headers

### Global Headers

Set default headers applied to all clients. Useful for `x-epilot-org-id`, `x-epilot-user-id`, etc.

```ts
import { epilot } from '@epilot/sdk'

epilot.authorize(() => '<my-token>')
epilot.headers({
  'x-epilot-org-id': 'org-123',
  'x-epilot-user-id': 'user-456',
})

const { data } = await epilot.entity.searchEntities(...)
```

### Standard Axios Headers

Use standard axios `defaults.headers.common` on individual clients:

```ts
const entityClient = epilot.entity.getClient()
entityClient.defaults.headers.common['x-epilot-org-id'] = 'org-123'
```

## Auth Patterns

`authorize()` accepts a string or a function. The function form is preferred — it is called on every request, so tokens stay fresh.

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

// Per-client — function predicate (recommended)
const entityClient = getClient()
authorize(entityClient, () => '<my-token>')

// Per-client — async function (e.g. OAuth / session)
authorize(entityClient, async () => {
  return await getTokenFromSession()
})

// Per-client — static string (sets default header once)
authorize(entityClient, 'my-static-api-token')
```

```ts
// Global — applies to all clients resolved from the SDK
import { epilot } from '@epilot/sdk'

epilot.authorize(() => '<my-token>')
epilot.authorize(async () => await getTokenFromSession())
epilot.authorize('my-static-api-token')
```

## Fresh Client Instance

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

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

## Multiple SDK Instances

```ts
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

Use axios interceptors for custom request/response processing. Since clients are axios instances, you can use `client.interceptors` directly:

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

Or register global interceptors applied to all clients:

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

## Auto-Retry (429 Too Many Requests)

The SDK automatically retries requests that receive a `429 Too Many Requests` response. It respects the `Retry-After` header (in seconds) to determine how long to wait before retrying.

Enabled by default with up to 3 retries.

```ts
import { epilot } from '@epilot/sdk'

// Customize retry behavior
epilot.retry({ maxRetries: 5, defaultDelayMs: 2000 })

// Disable retries
epilot.retry({ maxRetries: 0 })
```

| Option | Default | Description |
| --- | --- | --- |
| `maxRetries` | `3` | Maximum number of retries. Set to `0` to disable. |
| `defaultDelayMs` | `1000` | Fallback delay in ms when `Retry-After` header is missing. |

For individually imported clients (tree-shakeable imports), apply the interceptor manually:

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

const entityClient = getClient()
authorize(entityClient, () => '<my-token>')
applyRetryInterceptor({ client: entityClient, config: { maxRetries: 3 } })
```

## Large Response Handling (413 Payload Too Large)

epilot APIs use a [large response middleware](https://github.com/epilot-dev/aws-lambda-utility-middlewares) to work around the AWS Lambda 6MB response limit. When a response exceeds ~5.1MB, the API uploads the payload to S3 and returns a presigned URL instead.

The SDK handles this transparently — it sends the opt-in `Accept` header and automatically fetches the full payload from S3 when a large response URL is returned. Enabled by default.

```ts
import { epilot } from '@epilot/sdk'

// Disable large response handling
epilot.largeResponse({ enabled: false })
```

For individually imported clients (tree-shakeable imports), apply the interceptor manually:

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

const entityClient = getClient()
authorize(entityClient, () => '<my-token>')
applyLargeResponseInterceptor({ client: entityClient, config: { enabled: true } })
```

## Overrides & Custom APIs

Override built-in API specs or register custom APIs via `.epilot/sdk-overrides.json`. This is useful for testing new versions of an API spec or getting the latest types without waiting for an SDK release.

```json
{
  "entity": "./specs/entity-openapi.json",
  "myNewApi": "./specs/my-new-api-openapi.json"
}
```

```ts
// Built-in API with overridden spec
const { data } = await epilot.entity.getEntity({ slug: 'contact', id: '123' })
```

### Override Commands

```bash
# Apply all overrides from .epilot/sdk-overrides.json
npx epilot-sdk override

# Override a single API
npx epilot-sdk override entity ./my-local-entity-spec.yaml

# Regenerate types after spec changes
npx epilot-sdk typegen
```

<details>
<summary>Migration from <code>@epilot/*-client</code></summary>

Drop-in replacement — just change the import path:

```ts
// 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'
```

</details>

<details>
<summary>Client Lifecycle</summary>

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

**Operation methods are always up to date** — calls like `epilot.entity.getEntity(...)` re-resolve the client on every invocation, so they always use the latest config.

**Direct `getClient()` references can go stale** — if you hold a reference and then change config, your reference still points to the old client:

```ts
const entityClient = epilot.entity.getClient()

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

// entityClient still has the old token
// epilot.entity.getEntity(...) will use a new client with the new token
```

If you need a long-lived reference that survives config changes, call `getClient()` again after changing config, or use operation methods directly.

</details>
