> ## Documentation Index
> Fetch the complete documentation index at: https://nova.dweet.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Install and use the official TypeScript SDK for the Nova Embed API

The `@nova-sdk/api` package gives you typed methods for every Nova Embed API endpoint, automatic retries with exponential backoff, and webhook signature verification. It works in Node.js 18+ and any runtime that supports the Fetch API.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @nova-sdk/api
  ```

  ```bash pnpm theme={null}
  pnpm add @nova-sdk/api
  ```

  ```bash yarn theme={null}
  yarn add @nova-sdk/api
  ```
</CodeGroup>

## Quick start

```typescript theme={null}
import { Nova } from '@nova-sdk/api';

const nova = new Nova({
  apiKey: 'sk_test_your_key',
  tenantId: 'acme-corp',
});

// Generate criteria for a job
const { criteria } = await nova.jobs.criteria.generate.create({
  jobId: 'job-123',
  body: {
    jobContext: {
      jobTitle: 'Senior Software Engineer',
      companyName: 'Acme Corp',
      jobDescription: 'We are looking for a Senior Software Engineer with 5+ years experience...',
    },
  },
});

// Submit a scoring job
const { scoringJob } = await nova.jobs.applications.scoringJobs.submit({
  jobId: 'job-123',
  applicationId: 'app-456',
  body: {
    resume: { type: 'url', url: 'https://storage.example.com/resumes/abc123.pdf' },
    jobDescription: 'We are looking for a Senior Software Engineer with 5+ years experience...',
  },
});
```

## Configuration

Both `apiKey` and `tenantId` are required. Everything else is optional.

```typescript theme={null}
const nova = new Nova({
  // Required
  apiKey: 'sk_live_your_key',
  tenantId: 'acme-corp',

  // Optional
  baseUrl: 'https://embed.nova.dweet.com', // default
  retry: {
    maxRetries: 2,         // default: 2 (set to 0 to disable)
    initialDelayMs: 500,   // default: 500
    maxDelayMs: 30_000,    // default: 30,000
    backoffMultiplier: 2,  // default: 2
    retryableStatusCodes: [429, 500, 502, 503, 504], // default
  },
  fetch: customFetch,      // custom fetch implementation
});
```

<Tip>
  Use `sk_test_*` keys for sandbox and `sk_live_*` keys for production. The environment is derived from the key prefix.
</Tip>

## Per-request tenant override

Tenant-scoped methods accept an optional `RequestOptions` object as their last argument. You can override the tenant ID for a single call without creating a new client.

```typescript theme={null}
const library = await nova.criteriaLibrary.list(
  { category: 'engineering' },
  { tenantId: 'other-tenant' },
);
```

## Criteria library

The criteria library stores reusable criteria across jobs for a tenant.

```typescript theme={null}
// List all criteria (optionally filter by category)
const { criteria } = await nova.criteriaLibrary.list({ category: 'engineering' });

// Create a criterion
const { criterion } = await nova.criteriaLibrary.create({
  text: '3+ years of production TypeScript experience',
  importance: 'PREFERRED',
  category: 'engineering',
});

// Get a single criterion
const { criterion: item } = await nova.criteriaLibrary.get('crit_abc123');

// Update a criterion
await nova.criteriaLibrary.update({
  criterionId: 'crit_abc123',
  body: { importance: 'MUST_HAVE' },
});

// Delete a criterion
await nova.criteriaLibrary.delete('crit_abc123');
```

## Job criteria

Criteria attached to a specific job control how applications get scored.

### Generate criteria with AI

<Steps>
  <Step title="Generate clarification questions (optional)">
    Ask Nova for clarification questions that help calibrate criteria generation.

    ```typescript theme={null}
    const qs = await nova.jobs.questionSets.create({
      jobId: 'job-123',
      body: {
        jobContext: {
          jobTitle: 'Senior Software Engineer',
          companyName: 'Acme Corp',
          jobDescription: 'We are looking for...',
        },
      },
    });

    // Each question has: id, question (string), selectionType, options (string[]), hint
    // Show qs.questionSet.questions to the user, collect answers
    ```
  </Step>

  <Step title="Generate criteria from job context and answers">
    Pass the question set ID and answers to refine the generated criteria. The call is synchronous (10-20 seconds) and returns the criteria directly.

    ```typescript theme={null}
    const { criteria } = await nova.jobs.criteria.generate.create({
      jobId: 'job-123',
      body: {
        jobContext: {
          jobTitle: 'Senior Software Engineer',
          companyName: 'Acme Corp',
          jobDescription: 'We are looking for...',
        },
        questionSetId: qs.questionSet.id,
        answers: [
          { id: 'q1', value: 'Senior' },
        ],
      },
    });

    console.log(criteria); // Array of generated Criterion objects
    ```
  </Step>
</Steps>

### Manage criteria manually

```typescript theme={null}
// List criteria for a job
const { criteria } = await nova.jobs.criteria.list('job-123');

// Replace all criteria at once, guarded against stale overwrites
const currentCriteriaVersion = await nova.jobs.criteria.versions.getCurrent('job-123');

await nova.jobs.criteria.set('job-123', {
  criteria: [
    {
      text: '3+ years of Python experience',
      importance: 'MUST_HAVE',
    },
  ],
}, {
  expectedCriteriaRevision: currentCriteriaVersion.criteriaRevision,
});

// Add a single criterion
await nova.jobs.criteria.items.add({
  jobId: 'job-123',
  body: {
    text: 'Docker and containerization experience',
    importance: 'PREFERRED',
  },
});

// Update a criterion
await nova.jobs.criteria.items.update({
  jobId: 'job-123',
  criterionId: 'crit_abc123',
  body: { importance: 'MUST_HAVE' },
});

// Remove a criterion
await nova.jobs.criteria.items.remove({
  jobId: 'job-123',
  criterionId: 'crit_abc123',
});

// Archive all criteria for a job
await nova.jobs.criteria.archive('job-123');
```

### Criteria versions

Every change to a job's criteria creates a new version. You can list them or fetch a specific one.

```typescript theme={null}
// Get the current active version
// Returns { version: CriteriaVersionMeta, criteria: Criterion[] }
const { version, criteria } = await nova.jobs.criteria.versions.getCurrent('job-123');

// List all versions (metadata only, no criteria arrays)
const { versions } = await nova.jobs.criteria.versions.list('job-123');

// Get a specific version (includes criteria)
const v = await nova.jobs.criteria.versions.get({
  jobId: 'job-123',
  criteriaVersionId: 'cv_abc123',
});
```

### Batch criteria lookup

Fetch criteria for multiple jobs in a single call.

```typescript theme={null}
const { items } = await nova.jobs.criteria.getBatch(['job-123', 'job-456', 'job-789']);
// items: [{ jobId: 'job-123', criteria: [...] }, { jobId: 'job-456', criteria: null }, ...]
```

## Scoring

### Score a single application

```typescript theme={null}
const { scoringJob } = await nova.jobs.applications.scoringJobs.submit({
  jobId: 'job-123',
  applicationId: 'app-456',
  body: {
    resume: { type: 'url', url: 'https://storage.example.com/resumes/abc123.pdf' },
    jobDescription: 'We are looking for a Senior Software Engineer...',
  },
});

console.log(scoringJob.id);     // 'sj_abc123'
console.log(scoringJob.status); // 'pending'
```

### Score a batch of applications

```typescript theme={null}
const { batch, scoringJobIds, acceptedApplications, rejectedApplications } =
  await nova.jobs.scoringBatches.submit({
  jobId: 'job-123',
  body: {
    applications: [
      {
        applicationId: 'app-456',
        resume: { type: 'url', url: 'https://storage.example.com/resumes/456.pdf' },
      },
      {
        applicationId: 'app-789',
        resume: { type: 'url', url: 'https://storage.example.com/resumes/789.pdf' },
      },
    ],
    jobDescription: 'We are looking for...',
  },
});

console.log(acceptedApplications); // applications with scoring jobs
console.log(rejectedApplications); // invalid resume inputs rejected before queueing

// Check batch progress
const status = await nova.jobs.scoringBatches.getStatus({
  jobId: 'job-123',
  scoringBatchId: batch.id,
});

console.log(status.rejectedApplications);
```

### Retrieve results

Use [webhooks](/docs/embed-api/concepts/webhooks) for real-time delivery. For polling, use these methods:

```typescript theme={null}
// Get a specific scoring job
const { scoringJob } = await nova.jobs.applications.scoringJobs.get({
  jobId: 'job-123',
  applicationId: 'app-456',
  scoringJobId: 'sj_abc123',
});
// scoringJob.score and scoringJob.assessment are top-level fields

// Get the latest score for an application (across all scoring jobs)
const { scoringJob: latest } = await nova.jobs.applications.getLatestScore({
  jobId: 'job-123',
  applicationId: 'app-456',
});
```

## Deletion requests

Use deletion requests when a customer leaves, a tenant asks you to delete its data, or an application must be removed before automatic cleanup runs.

Deletion runs in the background. The SDK returns a `deletionRequest.id` immediately, then you poll until the request is `completed` or `failed`.

```typescript theme={null}
// Delete one application under one job
const { deletionRequest } = await nova.deletion.deleteApplication({
  jobId: 'job-123',
  applicationId: 'app-456',
});

// Delete all active Embed data for one tenant
await nova.deletion.deleteTenant({
  tenantExternalId: 'acme-corp',
});

// Poll for completion
const status = await nova.deletion.getRequest(deletionRequest.id);
console.log(status.deletionRequest.status); // 'pending' | 'processing' | 'completed' | 'failed'
```

Application deletion uses the client tenant ID, or a per-request tenant override. Tenant deletion scopes the request by `tenantExternalId`, so it doesn't need `X-Tenant-Id`.

Deletion request responses include only the request ID, scope, status, and timestamps. They don't include internal cleanup counts, raw tenant IDs, job IDs, or application IDs.

See [Deletion Requests](/docs/embed-api/endpoints/deletion/overview), [Data Retention](/docs/embed-api/concepts/data-retention), [Delete Application Data](/docs/embed-api/endpoints/deletion/application), [Delete Tenant Data](/docs/embed-api/endpoints/deletion/tenant), and [Get Deletion Request Status](/docs/embed-api/endpoints/deletion/status).

## Question sets

Generate and retrieve clarification question sets for jobs.

```typescript theme={null}
// Create a question set
const { questionSet } = await nova.jobs.questionSets.create({
  jobId: 'job-123',
  body: {
    jobContext: {
      jobTitle: 'Senior Software Engineer',
      companyName: 'Acme Corp',
      jobDescription: 'We are looking for...',
    },
  },
});

// Get the current question set for a job
const current = await nova.jobs.questionSets.getCurrent('job-123');

// Get a specific question set by ID
const specific = await nova.jobs.questionSets.get({
  jobId: 'job-123',
  questionSetId: questionSet.id,
});
```

## Rate limit status

Check your current rate limit usage without consuming a rate-limited request.

```typescript theme={null}
const limits = await nova.rateLimitStatus();
```

## Error handling

Every method throws `NovaApiError` on non-2xx responses. The error carries structured fields you can use for logging, retry decisions, and support tickets.

```typescript theme={null}
import { Nova, NovaApiError } from '@nova-sdk/api';

try {
  await nova.jobs.criteria.list('nonexistent-job');
} catch (err) {
  if (err instanceof NovaApiError) {
    console.log(err.code);      // 'JOB_NOT_FOUND'
    console.log(err.status);    // 404
    console.log(err.retryable); // false
    console.log(err.traceId);   // 'trace-abc-123' (share this with support)
    console.log(err.message);   // 'Job not found'
    console.log(err.details);   // field validation errors (on 400s)
  }
}
```

<Note>
  The `traceId` field is the fastest way to debug issues with Nova support. Always log it.
</Note>

### Error properties

| Property    | Type            | Description                                                         |
| ----------- | --------------- | ------------------------------------------------------------------- |
| `code`      | `ErrorCode`     | Machine-readable error code (e.g., `JOB_NOT_FOUND`, `RATE_LIMITED`) |
| `status`    | `number`        | HTTP status code                                                    |
| `message`   | `string`        | Human-readable description                                          |
| `retryable` | `boolean`       | Whether retrying might succeed                                      |
| `traceId`   | `string`        | Trace ID for debugging                                              |
| `type`      | `string`        | URI identifying the error type                                      |
| `details`   | `array \| null` | Field-level validation errors (400 responses only)                  |

## Retry configuration

The SDK retries failed requests automatically using exponential backoff with jitter. Retries trigger on:

* **Status codes** in the `retryableStatusCodes` list (default: 429, 500, 502, 503, 504), except `MONTHLY_TRIAL_QUOTA_EXCEEDED`
* **Body-driven retryability**: for other 4xx errors, the SDK checks the response body's `retryable` field

The `Retry-After` header is respected when the server sends it.

Monthly trial quota exhaustion is surfaced immediately as non-retryable `MONTHLY_TRIAL_QUOTA_EXCEEDED`. Submit a new request only after your plan is upgraded or the UTC month resets.

### Defaults

| Setting                | Default                     |
| ---------------------- | --------------------------- |
| `maxRetries`           | 2                           |
| `initialDelayMs`       | 500                         |
| `maxDelayMs`           | 30,000                      |
| `backoffMultiplier`    | 2                           |
| `retryableStatusCodes` | `[429, 500, 502, 503, 504]` |

### Custom retry config

```typescript theme={null}
const nova = new Nova({
  apiKey: 'sk_live_your_key',
  tenantId: 'acme-corp',
  retry: {
    maxRetries: 5,
    initialDelayMs: 1000,
    maxDelayMs: 60_000,
  },
});
```

### Disable retries

```typescript theme={null}
const nova = new Nova({
  apiKey: 'sk_live_your_key',
  tenantId: 'acme-corp',
  retry: { maxRetries: 0 },
});
```

## Webhook verification

The SDK includes utilities for verifying webhook signatures. Both methods use the Web Crypto API and work in Node.js 18+, Cloudflare Workers, Vercel Edge Functions, and Deno.

<Warning>
  Always verify webhook signatures before processing the payload. The raw request body must be passed as a string; don't parse it first.
</Warning>

### Verify and parse

```typescript theme={null}
import { Nova, WebhookSignatureVerificationError } from '@nova-sdk/api';

// In your webhook endpoint handler
const payload = req.body; // raw string, NOT parsed JSON
const signature = req.headers['x-webhook-signature'];

try {
  const event = await Nova.webhooks.constructEvent({
    payload,
    signatureHeader: signature,
    secret: 'whsec_your_signing_secret',
  });

  switch (event.event) {
    case 'score.completed':
      console.log('Score:', event.result.score);
      break;
    case 'score.failed':
      console.log('Score failed:', event.error.code);
      break;
    case 'batch.completed':
      console.log(`Batch done: ${event.completedJobs}/${event.totalJobs}`);
      break;
  }
} catch (err) {
  if (err instanceof WebhookSignatureVerificationError) {
    console.error('Invalid signature:', err.message);
    // Return 401
  }
}
```

### Verify-only mode

If you want to verify the signature without parsing the JSON payload, use `verify`.

```typescript theme={null}
await Nova.webhooks.verify({
  payload,
  signatureHeader: signature,
  secret: 'whsec_your_signing_secret',
});
```

### Timestamp tolerance

By default, webhooks older than 5 minutes (300 seconds) are rejected. You can adjust this.

```typescript theme={null}
await Nova.webhooks.constructEvent({
  payload,
  signatureHeader: signature,
  secret: 'whsec_your_signing_secret',
  options: { toleranceInSeconds: 600 }, // 10 minutes
});
```

## Types

All request/response types and enums are exported from the package root.

```typescript theme={null}
import {
  type Criterion,
  type ScoringJob,
  type JobContext,
  type DeletionRequest,
  type WebhookEvent,
  CriterionImportance,
  ScoringJobStatus,
  DeletionStatus,
  ErrorCode,
} from '@nova-sdk/api';
```

## Resource reference

Full list of available methods on the `Nova` client.

```
nova.criteriaLibrary.list(query?, opts?)
nova.criteriaLibrary.create(body, opts?)
nova.criteriaLibrary.get(criterionId, opts?)
nova.criteriaLibrary.update({ criterionId, body }, opts?)
nova.criteriaLibrary.delete(criterionId, opts?)

nova.jobs.criteria.getBatch(jobIds, opts?)
nova.jobs.criteria.list(jobId, opts?)
nova.jobs.criteria.set(jobId, body, opts?)
nova.jobs.criteria.archive(jobId, opts?)
nova.jobs.criteria.generate.create({ jobId, body }, opts?)
nova.jobs.criteria.generate.get({ jobId, criteriaGenerationId }, opts?)
nova.jobs.criteria.items.add({ jobId, body }, opts?)
nova.jobs.criteria.items.update({ jobId, criterionId, body }, opts?)
nova.jobs.criteria.items.remove({ jobId, criterionId }, opts?)
nova.jobs.criteria.versions.getCurrent(jobId, opts?)
nova.jobs.criteria.versions.list(jobId, opts?)
nova.jobs.criteria.versions.get({ jobId, criteriaVersionId }, opts?)

nova.jobs.applications.getLatestScore({ jobId, applicationId }, opts?)
nova.jobs.applications.scoringJobs.submit({ jobId, applicationId, body }, opts?)
nova.jobs.applications.scoringJobs.get({ jobId, applicationId, scoringJobId }, opts?)

nova.jobs.scoringBatches.submit({ jobId, body }, opts?)
nova.jobs.scoringBatches.getStatus({ jobId, scoringBatchId }, opts?)

nova.jobs.questionSets.create({ jobId, body }, opts?)
nova.jobs.questionSets.getCurrent(jobId, opts?)
nova.jobs.questionSets.get({ jobId, questionSetId }, opts?)

nova.deletion.deleteApplication({ jobId, applicationId }, opts?)
nova.deletion.deleteTenant({ tenantExternalId })
nova.deletion.getRequest(deletionRequestId)

nova.rateLimitStatus()
```

## Requirements

* **Node.js** >= 18 (or any runtime with Fetch API support)
* **TypeScript** >= 5.0 (if using TypeScript)
