Errors follow RFC 7807:
{
"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
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
For criteria and library mutations, use Idempotency-Key and reuse the same key on retries. Scoring submissions already deduplicate by jobId and applicationId.
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 |
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.
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 |
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.
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.
Error handling with the SDK
The TypeScript SDK retries automatically on 429 and 5xx errors with exponential backoff and Retry-After support. You only need to handle non-retryable errors:
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:
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:
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();
const retryAfter = response.headers.get("Retry-After");
if (
response.status === 429 ||
(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 });
}
}
Debugging
When contacting support, include the traceId, your timestamp and endpoint, and the HTTP status code.