# API contract

For whoever builds the client-facing frontend (web, mobile, or
internal tool) consuming this API. All endpoints are also available as
interactive docs at `/docs` when `ENVIRONMENT != production` — this
document is the durable reference for production, where `/docs` is
intentionally disabled (see `docs/architecture.md`).

Base URL: `https://your-domain.com/api/v1`

Every request (except health checks) requires the header:
```
X-API-Key: <your assigned API key>
```

## POST /analyze

Submits a crop analysis request. Returns immediately with a task ID —
the analysis itself runs asynchronously.

**Request body:**
```json
{
  "crop": "rice",
  "location": "Thanjavur",
  "month": "June"
}
```

`month` accepts a full month name, short month name (Jan/Feb/etc.), or
an integer 1-12. `crop` and `location` are case-insensitive and must
match a supported value — see `GET /crops` and `GET /districts`.

**Response (202 Accepted):**
```json
{
  "task_id": "a1b2c3d4-...",
  "status": "queued",
  "poll_url": "/api/v1/tasks/a1b2c3d4-..."
}
```

If an identical request (same crop+location+month) was answered
recently, you may instead get an immediately-completed synthetic task:
```json
{
  "task_id": "cached-a1b2c3d4-...",
  "status": "completed",
  "poll_url": "/api/v1/tasks/cached-a1b2c3d4-..."
}
```
Poll it the same way regardless — the polling contract is identical for
both cases.

**Validation error (422):**
```json
{
  "error_code": "VALIDATION_FAILED",
  "message": "month: 'Smarch' is not a recognized month name...",
  "request_id": "..."
}
```

**Rate limited (429):** returned if the client exceeds
`RATE_LIMIT_PER_MINUTE` configured requests per minute.

## GET /tasks/{task_id}

Poll this until `status` becomes `"completed"` or `"failed"`. A
reasonable polling interval is 1.5-2 seconds; most analyses complete
within 3-8 seconds depending on LLM response time.

**While in progress:**
```json
{ "task_id": "...", "status": "queued", "result": null, "error": null }
```
or
```json
{ "task_id": "...", "status": "running", "result": null, "error": null }
```

**On completion:**
```json
{
  "task_id": "...",
  "status": "completed",
  "result": {
    "request_id": "...",
    "input": { "crop": "rice", "location": "thanjavur", "month": 6 },
    "models": {
      "climate": {
        "forecast_rainfall_mm": 210.5,
        "forecast_temp_c": null,
        "anomaly_score": 0.28,
        "anomaly_label": "high"
      },
      "feasibility": {
        "label": "risky",
        "confidence": 0.71,
        "top_factors": ["rainfall_anomaly", "temp_c", "season_index"]
      },
      "yield_prediction": {
        "expected_yield_ton_ha": 3.2,
        "uncertainty_ton_ha": 0.48,
        "unit": "tonnes per hectare"
      },
      "trend": {
        "trend": "upward",
        "slope": null,
        "confidence": 0.82
      },
      "recommendation": {
        "alternatives": [
          { "crop": "maize", "score": 0.87, "reason": null },
          { "crop": "groundnut", "score": 0.74, "reason": null }
        ]
      },
      "market": {
        "price_forecast_per_quintal": 2400,
        "demand_level": "high",
        "price_volatility": "unknown"
      },
      "conflicts": [
        "Climate anomaly is high but feasibility model says suitable..."
      ]
    },
    "explanation": "1. Crop suitability summary: ...\n2. Climate assessment: ...",
    "model_version": "v1",
    "processing_ms": 4230,
    "cached": false
  },
  "error": null
}
```

Fields that are `null` indicate that specific model had no answer for
this query (e.g. a district with no trained SARIMAX model yet) rather
than an error — check `models.conflicts` and consider the overall
result still usable, just with one signal missing.

**On failure:**
```json
{
  "task_id": "...",
  "status": "failed",
  "result": null,
  "error": "Analysis could not be completed. Please try again or contact support if the problem persists."
}
```

## GET /crops

Returns the list of currently supported crop names, for populating a
dropdown. Updates automatically when `data/lookup/crops.json` is edited
and the service is redeployed.

```json
{ "crops": ["cotton", "groundnut", "maize", "rice", "sorghum", "sugarcane", "sunflower", "wheat"] }
```

## GET /districts

Same pattern as `/crops`, for supported districts.

```json
{ "districts": ["coimbatore", "erode", "madurai", "salem", "thanjavur", "tirunelveli", "trichy", "vellore"] }
```

## GET /health/live and GET /health/ready

No API key required — used by infrastructure (load balancers, uptime
monitors), not application clients. See `docs/architecture.md` for the
distinction between these two checks.

## Error response shape (all endpoints)

Every 4xx/5xx response has this consistent shape:
```json
{
  "error_code": "MACHINE_READABLE_CODE",
  "message": "Human readable message safe to show the user",
  "request_id": "uuid-for-support-correlation"
}
```

If a user reports a problem, ask for the `request_id` — it can be
grepped directly in the server's structured JSON logs to find exactly
what happened for that specific request.
