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

# GET /v1/jobs/{job_id}

> Poll an async scoring job.

Returns the current state of a cold-scoring job. Jobs are created automatically by [`POST /v1/score`](/endpoints/score) when no fresh cached record exists.

## Status transitions

| `status`   | Meaning                                                                        |
| ---------- | ------------------------------------------------------------------------------ |
| `pending`  | The underlying cold pipeline is still running. Poll again in \~5s.             |
| `complete` | The pipeline succeeded. `result_ref` and `score_url` are populated.            |
| `failed`   | The pipeline ended in a non-success terminal state. `failure_reason` explains. |

Status is settled **lazily** — the first poll that observes a terminal underlying execution flips the row. A job that's never polled stays `pending` in storage until its 30-day TTL.

## Reading a complete job

```json theme={null}
{
  "job_id":       "9f0c2d83-...",
  "status":       "complete",
  "domain":       "stripe.com",
  "created_at":   "2026-05-12T18:30:00Z",
  "completed_at": "2026-05-12T18:30:43Z",
  "result_ref":   { "domain": "stripe.com", "scored_at": "2026-05-12T18:30:43Z" },
  "score_url":    "/v1/score/stripe.com"
}
```

Once you have the `score_url`, do `GET https://api.keplerinsights.us/v1/score/stripe.com` to read the actual score. The two-step is intentional: the job row stays small and cheap; the full score response stays the same shape regardless of how it was triggered.

## Failure reasons

| `failure_reason`              | Meaning                                                                                                                                    |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `sfn_failed`                  | The pipeline ended in `FAILED` (engine raised an exception).                                                                               |
| `sfn_timed_out`               | The pipeline hit its task timeout. Usually transient — retry.                                                                              |
| `sfn_aborted`                 | An operator manually aborted the run. Should never happen in production.                                                                   |
| `scoring_completed_no_record` | The pipeline succeeded but no score-history row appeared. Possible cause: insufficient data stub. Check `/v1/company/{domain}/confidence`. |

## Cross-user access

Job IDs are server-side bound to the API key that created them. Polling someone else's job ID returns:

```
HTTP/1.1 403 Forbidden
{ "error": "forbidden" }
```

If you're proxying multiple downstream users through one Kepler key, you'll need to map their session IDs to your `job_id`s yourself.

## Expiration

Jobs are kept 30 days then auto-expire. Polling an expired job returns `404`. Don't rely on job rows for long-term history; use `GET /v1/score/{domain}/history` for that.


## OpenAPI

````yaml GET /v1/jobs/{job_id}
openapi: 3.1.0
info:
  title: Kepler Insights API
  version: 1.0.0
  summary: Curated company-scoring intelligence over a 67-signal engine.
  description: >
    The Kepler Insights API exposes a curated subset of the Kepler scoring
    engine

    over a stable REST surface. Every score is a composite over 67 signals
    organized

    into 4 buckets (Team & Structure, Market Position, Momentum & Tailwinds,

    Financial Health), with a separate scale premium that lifts established

    companies whose underlying fundamentals justify a higher tier.


    **Authentication.** All requests require a single header: `X-API-Key`.

    Keys are issued from the developer console at
    `https://console.keplerinsights.us`.


    **Sandbox.** Test keys prefixed `ki_test_` accept only the four canned

    domains (`acme.test`, `unicorn.test`, `struggling.test`, `cohort.test`),

    never invoke fetchers, and return deterministic responses tagged with

    `mode: sandbox`. Live keys prefixed `ki_live_` reject test domains and

    run against the real engine.


    **Cold vs cached.** `GET /v1/score/{domain}` is always cached (404 if no

    recent record exists). `POST /v1/score` decides per-call based on your

    tier's freshness window: a fresh record returns inline (`200`), a stale

    or missing one queues a cold pipeline run and returns `202` with a

    `job_id` to poll. Cold spend is bounded by per-tier monthly caps +

    circuit breakers.
  contact:
    name: Kepler Insights API support
    email: noah@keplerinsights.us
  license:
    name: Proprietary
servers:
  - url: https://api.keplerinsights.us
    description: Production
security:
  - apiKey: []
tags:
  - name: scoring
    description: Score a company and retrieve the latest result.
  - name: history
    description: Time-series of past scoring runs for a single company.
  - name: company
    description: Per-company analytics (cohort, confidence).
  - name: universe
    description: Distribution + movers across the full scored universe.
  - name: meta
    description: Signal manifest + caller usage + async job tracking.
paths:
  /v1/jobs/{job_id}:
    get:
      tags:
        - meta
      summary: Poll an async scoring job.
      description: >
        Returns the current state of a cold-scoring job. Jobs are created
        automatically by `POST /v1/score` whenever a cold pipeline run is
        needed.

        `status` advances `pending → complete | failed` on the first poll that

        observes a terminal underlying execution. Settle is lazy: a never-polled

        job stays `pending` in storage until its 30-day TTL.


        On `complete`, the response includes `result_ref: {domain, scored_at}`

        and `score_url: /v1/score/{domain}` — fetch the actual score there.
      operationId: getJob
      parameters:
        - in: path
          name: job_id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Job state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Job belongs to another caller. Job IDs are not shareable across
            keys.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Unknown job ID (or expired past 30-day TTL).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    JobResponse:
      type: object
      properties:
        mode:
          type: string
        job_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - complete
            - failed
        domain:
          type: string
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        result_ref:
          type: object
          nullable: true
          description: Present on `complete`. Use `score_url` to fetch the actual record.
          properties:
            domain:
              type: string
            scored_at:
              type: string
              format: date-time
        score_url:
          type: string
          nullable: true
          example: /v1/score/stripe.com
        failure_reason:
          type: string
          nullable: true
          enum:
            - sfn_failed
            - sfn_timed_out
            - sfn_aborted
            - scoring_completed_no_record
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: unauthorized
        message:
          type: string
        reason:
          type: string
          description: Additional context for rate-limit responses.
  responses:
    Unauthorized:
      description: Missing or invalid `X-API-Key`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: >
        Live keys are prefixed `ki_live_`, test keys `ki_test_`. Issue + revoke

        keys at https://console.keplerinsights.us. Never embed a key in
        client-side

        code — every endpoint is backend-to-API only.

````