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

# POST /v1/score

> Score a company. Cached returns inline; cold returns a job to poll.

The primary scoring entry point. Returns the latest score for `domain` from cache when a stored record is within your tier's freshness window, **or starts a cold scoring job** when no fresh record exists.

## Two response shapes

* **HTTP 200** — a cached score is available. Returned inline; ready to use.
* **HTTP 202** — no fresh record. The server has started a cold scoring run and returned a `job_id`. Poll [`GET /v1/jobs/{job_id}`](/endpoints/jobs) until `status: "complete"`.

Cold scoring runs typically take **25–90 seconds** end-to-end. They are billed when the job starts, not when you finish polling — abandoning a poll loop does not get you a refund.

## When you'll see each path

* Cached (200) — the dominant path. Most calls hit it; tier-determined freshness windows are designed so that customer traffic mostly reuses recent work.
* Cold (202) — first time you've ever asked for `domain`, or the most recent record is older than your tier's window.

Your tier's freshness window:

| Tier       | Window                                           |
| ---------- | ------------------------------------------------ |
| Free       | 7 days (sandbox only — Free can't run live cold) |
| Starter    | 24 hours                                         |
| Growth     | 6 hours                                          |
| Scale      | 1 hour                                           |
| Enterprise | 15 minutes                                       |

## Example (Python SDK)

The official SDK handles both shapes transparently. `score()` blocks until the score is ready, polling internally on the cold path.

```python theme={null}
from kepler_insights import Kepler

k = Kepler(api_key="ki_live_...")

score = k.score("stripe.com")               # blocks up to 180s on cold
print(f"{score.domain}: {score.ki_rating} ({score.composite_score:.1f})")

# Non-blocking — get the Job and poll yourself:
job = k.start_score("acme-co.com")
print(job.id)
score = job.wait(timeout=240)
```

## Example (curl, manual polling)

```bash theme={null}
# 1. Submit
curl -X POST https://api.keplerinsights.us/v1/score \
  -H "X-API-Key: ki_live_..." \
  -H "Content-Type: application/json" \
  -d '{"domain":"stripe.com"}'

# Cached response (200):
# { "domain": "stripe.com", "ki_rating": "KI-1", "composite_score": 73.65, ... }

# Cold response (202):
# { "job_id": "679a9e32-...", "status": "pending",
#   "domain": "stripe.com", "poll_url": "/v1/jobs/679a9e32-..." }

# 2. Poll until terminal (cold only)
curl https://api.keplerinsights.us/v1/jobs/679a9e32-... \
  -H "X-API-Key: ki_live_..."
```

Branch on the response: if the body has `composite_score`, you have the score inline. If it has `job_id`, queue a poll loop on the `poll_url`.

## Field notes

* **`composite_score`** is 0–100, **not 0–10.** Easy mistake when reading.
* **`scale_premium`** is a separate additive bonus (up to \~12 pts). It's already applied to `composite_score`; the field is broken out so you can show "47.0 composite + 4.1 scale premium" if you want.
* **`rank`** is computed against the full universe Kepler has ever scored, not just the active cohort. Use [`/v1/company/{domain}/cohort`](/endpoints/cohort) for sector-matched comparison.
* **`x_kepler.cache_status`** is `"cached"` on every score body, including the one you fetch after a job completes — the fresh cold-pipeline result is written to cache before the response shape is constructed. The enum is reserved for a future sync-cold response shape; today only `"cached"` is emitted.
* **`wait` parameter** is accepted but ignored. Cold = async, always. The parameter is preserved so older client code doesn't break.

## Sandbox behavior

`ki_test_` keys against the 4 canned domains (`acme.test`, `unicorn.test`, `struggling.test`, `cohort.test`) always return inline 200. Sandbox has no cold path — there's nothing to queue. This means production code paths that branch on 202 won't be exercised in the sandbox. Test the polling path against a fresh live domain in staging before relying on it.


## OpenAPI

````yaml POST /v1/score
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/score:
    post:
      tags:
        - scoring
      summary: Score a company. Cached returns inline, cold returns a job.
      description: |
        Returns the latest score for `domain`. Two response shapes:

        - **200 OK** when a cached record is fresh under your tier's freshness
          window. Score returned inline. No fetcher spend, no cold-call usage.

        - **202 Accepted** when no fresh record exists. The server starts a
          cold scoring run and returns `{job_id, poll_url}`. Poll
          `GET /v1/jobs/{job_id}` until `status: complete`, then read the
          score via `GET /v1/score/{domain}`. Cold runs typically take
          25–90 seconds.

        Auto-queue applies to all paid tiers. Free tier cannot run cold
        (returns 403 `free_tier_sandbox_only`). The `wait` parameter is
        accepted for backwards compatibility but is now a no-op — cold is
        always async.

        **Freshness windows by tier:** free 7d, starter 24h, growth 6h,
        scale 1h, enterprise 15min.
      operationId: scoreCompany
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - domain
              properties:
                domain:
                  type: string
                  example: stripe.com
                force_fresh:
                  type: boolean
                  description: >-
                    Enterprise-only. Forces a cold run even if a fresh record
                    exists.
                  default: false
                wait:
                  type: boolean
                  description: >-
                    Accepted for backwards compatibility, ignored. Cold is
                    always async.
                  default: true
                  deprecated: true
      responses:
        '200':
          description: Cached score (fresh under your tier's window).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScoreResponse'
        '202':
          description: Cold scoring job started. Poll `poll_url` until complete.
          content:
            application/json:
              schema:
                type: object
                required:
                  - job_id
                  - status
                  - domain
                  - poll_url
                properties:
                  job_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - pending
                  domain:
                    type: string
                  poll_url:
                    type: string
                    example: /v1/jobs/9f0c…
        '400':
          description: Validation error (missing or malformed domain).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Free trial — live scoring not included. Use a `ki_test_` key, or
            upgrade.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Scoring engine unavailable (pipeline or job tracking failure).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    ScoreResponse:
      type: object
      required:
        - domain
        - scored_at
        - ki_rating
        - composite_score
        - buckets
      properties:
        mode:
          type: string
          description: Present and = `sandbox` only with a ki_test_ key.
        domain:
          type: string
        scored_at:
          type: string
          format: date-time
        ki_rating:
          type: string
          enum:
            - KI-1+
            - KI-1
            - KI-2+
            - KI-2
            - KI-3
            - KI-4
            - KI-5
        composite_score:
          type: number
          format: float
          minimum: 0
          maximum: 100
        scale_premium:
          type: number
          format: float
          minimum: 0
          maximum: 12
          description: >-
            Up to ~12 pts for established mega-caps; up to ~6 pts for growth
            profile.
        buckets:
          $ref: '#/components/schemas/Buckets'
        rank:
          $ref: '#/components/schemas/Rank'
        x_kepler:
          $ref: '#/components/schemas/XKeplerMeta'
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: unauthorized
        message:
          type: string
        reason:
          type: string
          description: Additional context for rate-limit responses.
    Buckets:
      type: object
      description: The 4 KI buckets. Each is 0–100.
      properties:
        team_structure:
          type: number
          format: float
          minimum: 0
          maximum: 100
        market_position:
          type: number
          format: float
          minimum: 0
          maximum: 100
        momentum_tailwinds:
          type: number
          format: float
          minimum: 0
          maximum: 100
        financial_health:
          type: number
          format: float
          minimum: 0
          maximum: 100
    Rank:
      type: object
      properties:
        percentile:
          type: integer
          minimum: 0
          maximum: 100
        cohort_size:
          type: integer
          minimum: 0
    XKeplerMeta:
      type: object
      description: Caller-facing metadata. Stable across versions.
      properties:
        tier:
          type: string
          enum:
            - free
            - starter
            - growth
            - scale
            - enterprise
        cache_status:
          type: string
          enum:
            - cached
          description: >-
            Today only `cached` is emitted — fresh cold-pipeline results are
            written to cache before the response is constructed. The enum is
            reserved for a future sync-cold response shape.
        freshness_window_hours:
          type: number
  responses:
    Unauthorized:
      description: Missing or invalid `X-API-Key`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: >
        Rate-limit response. `error` distinguishes the cause:

        - `cold_budget_exhausted` — monthly cold-call cap hit. Retry-After is
        seconds until the next monthly reset.

        - `rate limited` (`reason: per_key_cold_1h`) —
        200-cold-calls-per-hour-per-key breaker.

        - `rate limited` (`reason: account_cold_24h`) — 2× tier monthly cap
        inside any 24h window.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until you can retry.
      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.

````