> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Get Job

GET https://acme.captain.dev/v1/jobs/{job_id}

Get a job's status and stages, and a build's progress. A write key reads the jobs it started and the automatic builds of its indexes, the ones Get Update Status names. Any other job answers 404 JOB_NOT_FOUND.

Reference: https://docs.runcaptain.com/reference/product-search/indexes/get-job

## Authentication

- `Authorization` header (bearer token, required) — A write key, issued by Captain. Server side only. Sent as Authorization: Bearer \<key>.

## Request

### Path parameters

- `job_id` (string, required)

## Response

### 200

job

- `id` (string, optional)
- `type` (enum, optional) — index.write: record updates and deletes through the ordered update log, one job per burst
  - Allowed values: `index.build`, `index.delete`, `index.import`, `index.write`
- `index` (string, optional)
- `seq_from` (integer, optional) — index.write: the first update this job carries
- `seq_to` (integer, optional) — index.write: the last update this job carries
- `records` (integer, optional) — index.write: records written
- `status` (enum, optional)
  - Allowed values: `queued`, `running`, `importing`, `succeeded`, `failed`
- `created_at` (integer, optional)
- `started_at` (integer, optional, nullable)
- `finished_at` (integer, optional, nullable)
- `stages` (list of JobStagesItems, optional)
- `progress` (BuildProgress, optional) — index.build: how far the build has got. records_done counts the records the running step has done; a step that doesn't count records (tables, verify, publish) reports every record done
- `build_id` (string, optional)
- `error` (string, optional, nullable)
- `error_code` (string, optional) — why a failed job failed; error says it in words to show. A build: BAD_VALUES (more than half of a field's values don't fit its declared type; see bad_values), BUILD_QUALITY (the build's checks refused it; the previous build keeps serving), NO_RECORDS (every record was deleted or cleared before the build read them), CANCELLED (the index is being deleted), ENGINE_ERROR (a fault on Captain's side, in the build or in running the job; the index keeps serving its previous build; retry). An index delete: DELETE_FAILED (Captain couldn't delete the index; retry). An Algolia import: the Algolia code (ALGOLIA_APP_NOT_FOUND, ALGOLIA_UNREACHABLE, ALGOLIA_INDEX_NOT_FOUND, ALGOLIA_FORBIDDEN, ALGOLIA_TIMEOUT, ALGOLIA_ERROR, INDEX_TOO_LARGE, VALIDATION_ERROR) or IMPORT_FAILED (a fault on Captain's side; nothing was changed; retry)
- `bad_values` (list of JobBadValuesItems, optional) — with error_code BAD_VALUES: per field, count of bad values, of how many, and up to 3 examples
- `warnings` (list of JobWarningsItems, optional) — \{code, message}. Settings resolved by a rule (the same warnings GET settings shows). Any build: SETTLE\_TIMEOUT (the build is live, but its search results are still settling and may shift for a few minutes; no action needed)
- `facet_report` (list of JobFacetReportItems, optional) — per number or date facet, whether it has value counts and why
- `result` (JobResult, optional, nullable)

## Types

### JobStagesItems

- `name` (enum, optional)
  - Allowed values: `schema`, `dense`, `splade`, `upload`, `tables`, `verify`, `publish`, `replicate`, `drop`, `pull`, `read`, `map`, `add`, `validate`, `embed`, `upsert`, `refresh`
- `status` (enum, optional)
  - Allowed values: `queued`, `pending`, `running`, `done`, `failed`, `skipped`
- `count` (integer, optional)
- `ms` (double, optional)
- `started_at` (double, optional)
- `finished_at` (double, optional)
- `reason` (string, optional) — why this step was skipped, in words
- `items` (integer, optional)
- `of` (integer, optional)
- `seconds` (integer, optional)

### BuildProgress

index.build: how far the build has got. records_done counts the records the running step has done; a step that doesn't count records (tables, verify, publish) reports every record done

- `step` (string, optional) — the step running now, as named in stages; queued before the first, done after the last
- `step_number` (integer, optional) — its place in the build, from 1
- `steps` (integer, optional) — steps in this build
- `records_done` (integer, optional)
- `records_total` (integer, optional, nullable)
- `phase` (enum, optional) — the build in five phases a progress screen can show: prepare (read and analyze the records), encode (the records read for search and written), index (the search index built and checked), warm (the catalog read into memory), snapshot (a backup taken before the catalog goes live)
  - Allowed values: `queued`, `prepare`, `encode`, `index`, `warm`, `snapshot`, `done`
- `phase_number` (integer, optional) — 1 to phases; 0 while queued
- `phases` (integer, optional) — 5
- `phase_label` (string, optional) — the phase in plain words
- `phase_done` (integer, optional, nullable) — units done in this phase (phase_unit), never decreasing within it
- `phase_total` (integer, optional, nullable)
- `phase_unit` (enum, optional)
  - Allowed values: `records`, `percent`, `collections`
- `phase_fraction` (double, optional, nullable) — 0 to 1; null when the phase has no count of its own
- `fraction` (double, optional) — 0 to 1 for the whole build, by the phases' measured share of a build's time
- `skipped_phases` (list of string, optional) — phases this build does not run (warm without a warm keeper, snapshot on a cell that does not replicate)
- `eta_s` (BuildProgressEtaS, optional, nullable) — seconds until the index is searchable, as GET .../updates estimates it: \{low, point, high}

### JobBadValuesItems

### JobWarningsItems

### JobFacetReportItems

- `field` (string, optional)
- `type` (string, optional)
- `distinct` (integer, optional, nullable)
- `facet_counts` (boolean, optional)
- `reason` (string, optional)

### JobResult

- `alias` (string, optional)
- `build` (string, optional)
- `physical` (string, optional)
- `previous` (string, optional, nullable)
- `records` (integer, optional)
- `facets` (list of string, optional)
- `seconds` (integer, optional)
- `checks` (JobResultChecks, optional)
- `constraints` (JobResultConstraints, optional) — index.build: whether measurement queries ('under 30 inches') are scored on the new build, and why not
- `report` (string, optional) — index.import: where the import report is
- `build_job` (string, optional, nullable) — index.import: the build job it queued

### BuildProgressEtaS

seconds until the index is searchable, as GET .../updates estimates it: \{low, point, high}

### JobResultChecks

### JobResultConstraints

index.build: whether measurement queries ('under 30 inches') are scored on the new build, and why not

- `ranking` (boolean, required)
- `reason` (string, required, nullable)

## Examples

### Get an import job

**Response**

```json
{
  "id": "job_20260928213040_9b27e1",
  "type": "index.import",
  "index": "products",
  "status": "succeeded",
  "created_at": 1759095040,
  "started_at": 1759095040,
  "finished_at": 1759095161,
  "stages": [
    {
      "name": "pull",
      "status": "done",
      "records": 12840,
      "synonyms": 46,
      "rules": 6,
      "notes": []
    },
    {
      "name": "map",
      "status": "done",
      "fields": 14,
      "facets": [
        "brand",
        "category",
        "color"
      ],
      "counts": {
        "settings": {
          "mapped": 9,
          "approximated": 2,
          "not_mapped": 1
        },
        "synonyms": {
          "mapped": 46,
          "approximated": 0,
          "not_mapped": 0
        },
        "rules": {
          "mapped": 5,
          "approximated": 0,
          "not_mapped": 1
        },
        "records": 12840
      }
    },
    {
      "name": "add",
      "status": "done",
      "documents": 12840,
      "synonyms_saved": 46,
      "rules_saved": 5
    }
  ],
  "error": null,
  "result": {
    "records": 12840,
    "build_job": "job_20260928213301_4c8d2e",
    "documents": 12840,
    "counts": {
      "settings": {
        "mapped": 9,
        "approximated": 2,
        "not_mapped": 1
      },
      "synonyms": {
        "mapped": 46,
        "approximated": 0,
        "not_mapped": 0
      },
      "rules": {
        "mapped": 5,
        "approximated": 0,
        "not_mapped": 1
      },
      "records": 12840
    }
  }
}
```

**SDK Code**

```python Python REST
import json
import requests

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_write_key"
JOB_ID = "job_20260928213040_9b27e1"

headers = {
    "Authorization": f"Bearer {API_KEY}",
}

response = requests.get(
    f"{BASE_URL}/v1/jobs/{JOB_ID}",
    headers=headers,
    timeout=30.0,
)

print(json.dumps(response.json(), indent=2))
```

```typescript TypeScript REST
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_write_key";
const jobId = "job_20260928213040_9b27e1";

const response = await fetch(`${BASE_URL}/v1/jobs/${jobId}`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
  },
});

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```

### Get a running build job

**Response**

```json
{
  "id": "job_20260929231500_1a2b3c",
  "type": "index.build",
  "index": "products",
  "status": "running",
  "created_at": 1759187700,
  "started_at": 1759187702,
  "finished_at": null,
  "stages": [
    {
      "name": "schema",
      "status": "done",
      "items": 250000,
      "seconds": 41
    },
    {
      "name": "dense",
      "status": "running",
      "items": 118000,
      "of": 250000
    },
    {
      "name": "splade",
      "status": "queued"
    },
    {
      "name": "upload",
      "status": "queued"
    },
    {
      "name": "tables",
      "status": "queued"
    },
    {
      "name": "verify",
      "status": "queued"
    },
    {
      "name": "publish",
      "status": "queued"
    },
    {
      "name": "replicate",
      "status": "queued"
    }
  ],
  "progress": {
    "step": "dense",
    "step_number": 2,
    "steps": 8,
    "records_done": 118000,
    "records_total": 250000
  },
  "error": null,
  "result": null
}
```

**SDK Code**

```python Python REST
import json
import requests

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_write_key"
JOB_ID = "job_20260929231500_1a2b3c"

headers = {
    "Authorization": f"Bearer {API_KEY}",
}

response = requests.get(
    f"{BASE_URL}/v1/jobs/{JOB_ID}",
    headers=headers,
    timeout=30.0,
)

print(json.dumps(response.json(), indent=2))
```

```typescript TypeScript REST
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_write_key";
const jobId = "job_20260929231500_1a2b3c";

const response = await fetch(`${BASE_URL}/v1/jobs/${jobId}`, {
  method: "GET",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
  },
});

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```