> ## 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.

# Error Handling

> Error format, retry strategies, and common error codes

## Error response format

Errors follow RFC 7807:

```json theme={null}
{
  "type": "https://docs.nova.dweet.com/embed-api/errors#validation-error",
  "code": "VALIDATION_ERROR",
  "status": 400,
  "message": "Request validation failed",
  "retryable": false,
  "traceId": "5c2f4f5b2c0a4ce0b6a31a1a18f8e9a1",
  "details": [
    {
      "field": "resume.url",
      "code": "invalid",
      "message": "Resume URL must be a valid URL"
    }
  ]
}
```

| Field       | Description                                           |
| ----------- | ----------------------------------------------------- |
| `type`      | RFC 7807 URI identifying the error category           |
| `code`      | Machine-readable error code for programmatic matching |
| `status`    | HTTP status code                                      |
| `message`   | Human-readable error description                      |
| `retryable` | Whether retrying may succeed                          |
| `traceId`   | Trace ID for debugging (also in `X-Trace-Id` header)  |
| `details`   | Field-level validation errors (when present)          |

## When to retry

* `MONTHLY_TRIAL_QUOTA_EXCEEDED`: contact your Nova account manager or `nova@dweet.com` to upgrade your plan, or submit again after the UTC month resets
* `retryable: true`: retry with exponential backoff
* `429 RATE_LIMITED` or retryable `503 SERVICE_UNAVAILABLE`: wait for `Retry-After` when present
* Other `4xx`: fix the request first

<Tip>
  For criteria and library mutations, use `Idempotency-Key` and reuse the same key on retries. Scoring submissions already deduplicate by `jobId` and `applicationId`.
</Tip>

## Common error codes

| Code                           | Status | Action                                                                                               |
| ------------------------------ | ------ | ---------------------------------------------------------------------------------------------------- |
| `UNAUTHORIZED`                 | 401    | Check API key and `Authorization` header                                                             |
| `VALIDATION_ERROR`             | 400    | Fix the fields listed in `details`                                                                   |
| `ANSWER_MISMATCH`              | 400    | Answers must match the question set                                                                  |
| `CRITERIA_NOT_FOUND`           | 404    | Generate criteria for the job first                                                                  |
| `SCORING_JOB_NOT_FOUND`        | 404    | Verify `scoringJobId` exists for this tenant                                                         |
| `RESUME_FETCH_FAILED`          | 422    | Check resume URL is accessible and not expired                                                       |
| `RESUME_TOO_LARGE`             | 422    | File must be under 50 MB                                                                             |
| `RESUME_ENCRYPTED`             | 422    | Remove password protection                                                                           |
| `RESUME_CORRUPTED`             | 422    | Submit a corrected, complete resume file                                                             |
| `RESUME_PARSE_FAILED`          | 422    | Confirm the file opens normally and contact support if it does                                       |
| `RATE_LIMITED`                 | 429    | Wait for `Retry-After` and retry                                                                     |
| `MONTHLY_TRIAL_QUOTA_EXCEEDED` | 429    | Contact your Nova account manager or `nova@dweet.com` to upgrade, or wait until the UTC month resets |
| `SERVICE_UNAVAILABLE`          | 503    | Retry with backoff; wait for `Retry-After` when present                                              |

### Batch intake rejections

Batch scoring separates a valid batch envelope from invalid individual resume inputs. If at least one application can be accepted, `POST /v1/jobs/{jobId}/scoring-batches` returns `202 Accepted` with `acceptedApplications` and `rejectedApplications`. Rejected applications do not create scoring jobs and do not emit `score.failed` webhooks.

For malformed PDFs, `rejectedApplications[].error.reason` may include `escaped_pdf_line_endings` when the PDF structure contains escaped newline characters where real line breaks are required. Submit a corrected PDF and retry that application in a new batch shape or through the single scoring endpoint.

If every application is rejected before queueing, the endpoint returns `422 VALIDATION_ERROR`. The `details` array contains one item per rejected application with the concrete public code, such as `RESUME_CORRUPTED` or `RESUME_FETCH_FAILED`.

### Idempotency

These errors apply to routes that support the HTTP `Idempotency-Key` replay layer. Scoring submissions use built-in scoring idempotency instead.

That means you won't see `IDEMPOTENCY_*` errors on scoring submission or scoring batch endpoints just because you sent `Idempotency-Key`. Nova ignores the header there.

| Code                              | Status | What to do                                                                                  |
| --------------------------------- | ------ | ------------------------------------------------------------------------------------------- |
| `IDEMPOTENCY_REQUEST_IN_PROGRESS` | 409    | Wait briefly and retry using the same `Idempotency-Key`                                     |
| `IDEMPOTENCY_KEY_ALREADY_USED`    | 422    | Do not retry. Generate a new key and retry only after you have fixed the request parameters |

<Note>
  If a mutation reaches endpoint logic and returns a cacheable `4xx`, retrying with the same `Idempotency-Key` replays that same error. If the request fails before endpoint logic runs, for example because a required header is missing, the `Content-Type` is invalid, the JSON is malformed, schema or param validation fails, or rate limiting blocks the request, the response is not cached.
</Note>

<Tip>
  This page covers the most common errors. For the complete list of error codes with HTTP status, retryability, and surfaces, see the [Error Code Reference](/embed-api/errors).
</Tip>

## Error handling with the SDK

The TypeScript SDK retries automatically on short-window `429` and `5xx` errors with exponential backoff and `Retry-After` support.

Monthly trial quota errors surface immediately as non-retryable `MONTHLY_TRIAL_QUOTA_EXCEEDED`. Submit again after your plan is upgraded or the UTC month resets. Handle non-retryable errors in your catch block:

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

const nova = new Nova({
  apiKey: process.env.NOVA_API_KEY!,
  tenantId: 'acme-corp',
});

try {
  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...',
    },
  });
} catch (err) {
  if (err instanceof NovaApiError) {
    console.log(err.code);      // e.g. 'VALIDATION_ERROR'
    console.log(err.status);    // e.g. 400
    console.log(err.retryable); // false
    console.log(err.traceId);   // for debugging
    console.log(err.details);   // field-level validation errors
  }
}
```

Disable retries if you want full control:

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

## Manual retry logic (cURL / raw HTTP)

If you aren't using the SDK, implement retries yourself:

<CodeGroup>
  ```javascript Node.js theme={null}
  async function novaRequest(url, { method, headers, body }) {
    const maxAttempts = 3;

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
      });

      if (response.ok) {
        return response.json();
      }

      const error = await response.json();

      if (error.code === "MONTHLY_TRIAL_QUOTA_EXCEEDED") {
        throw Object.assign(new Error(error.message), { error });
      }

      const retryAfter = response.headers.get("Retry-After");
      if (
        (response.status === 429 && error.retryable) ||
        (response.status === 503 && error.retryable && retryAfter)
      ) {
        const retryAfterSeconds = Number(retryAfter ?? "60");
        await new Promise((r) => setTimeout(r, retryAfterSeconds * 1000));
        continue;
      }

      if (error.retryable && attempt < maxAttempts) {
        const delayMs = 250 * Math.pow(2, attempt - 1);
        await new Promise((r) => setTimeout(r, delayMs));
        continue;
      }

      throw Object.assign(new Error(error.message), { error });
    }
  }
  ```

  ```python Python theme={null}
  import time
  import requests

  def nova_request(url: str, method: str, headers: dict, body: dict | None = None):
      max_attempts = 3

      for attempt in range(1, max_attempts + 1):
          response = requests.request(method, url, headers=headers, json=body)

          if response.ok:
              return response.json()

          error = response.json()

          if error.get("code") == "MONTHLY_TRIAL_QUOTA_EXCEEDED":
              raise Exception(f'{error.get("code")}: {error.get("message")} (traceId={error.get("traceId")})')

          retry_after = response.headers.get("Retry-After")
          if (response.status_code == 429 and error.get("retryable")) or (
              response.status_code == 503 and error.get("retryable") and retry_after
          ):
              time.sleep(int(retry_after or "60"))
              continue

          if error.get("retryable") and attempt < max_attempts:
              time.sleep(0.25 * (2 ** (attempt - 1)))
              continue

          raise Exception(f'{error.get("code")}: {error.get("message")} (traceId={error.get("traceId")})')
  ```
</CodeGroup>

## Debugging

When contacting support, include the `traceId`, your timestamp and endpoint, and the HTTP status code.
