# SBIR/STTR Grant Intelligence — REST API Reference

Base URL (local): `http://127.0.0.1:8731`
Run: `python api.py [port]`

All responses are `application/json`. CORS is open (`Access-Control-Allow-Origin: *`)
for local/dev use — tighten the header before any public deployment.

Auth: an optional `X-API-Key` header is accepted and logged but NOT required yet.
The hook exists so billing/auth can be added without an API rewrite. No accounts are
created and no credentials are stored in this build.

All data derives from the official Grants.gov public API (search2 + fetchOpportunity),
used under its Terms of Service with required attribution. No scraping, no auth bypass.

---

## Endpoints

### `GET /health`
Liveness + dataset counts.
```json
{ "status": "ok", "version": "2.0.0",
  "grants": 973, "sbir": 5, "sttr": 3, "closing_14d": 0 }
```

### `GET /stats`
Full dataset statistics.
```json
{ "total": 973, "sbir": 5, "sttr": 3, "closing_14d": 0, "enriched": 120 }
```

### `GET /attribution`
Required HHS attribution notice.
```json
{ "notice": "This product uses the Grants.gov API but is not endorsed or certified by the U.S. Department of Health and Human Services.",
  "source": "Official Grants.gov public API (search2 + fetchOpportunity). No scraping, no paywall bypass." }
```

### `GET /agencies`
Coverage breakdown by agency (top-level agency code, count, SBIR/STTR counts).

### `GET /grants`  (also `GET /search`)
Paginated, filterable search.

Query params:
| param | type | notes |
|-------|------|-------|
| `keyword` | string | matches title / synopsis / eligibility |
| `agency` | string | agency name fragment |
| `top_agency` | string | agency code, e.g. `HHS`, `NSF`, `DOD`, `NASA` |
| `program` | enum | `ALL` (default), `SBIR`, `STTR`, `BOTH`, `OTHER` |
| `status` | string | e.g. `posted`, `forecasted` |
| `min_composite` | int 0–100 | minimum fit score |
| `is_sbir` | bool | `true` to filter SBIR only |
| `is_sttr` | bool | `true` to filter STTR only |
| `sort` | enum | `composite` (default), `days_to_close`, `urgency`, `relevance` |
| `order` | enum | `desc` (default), `asc` |
| `limit` | int 1–100 | page size (default 25, capped at 100) |
| `offset` | int | pagination offset (default 0) |

Response:
```json
{ "query": { ...echoed params... },
  "total": 973, "count": 25, "offset": 0,
  "grants": [ { ...grant record... } ],
  "pagination": { "next_offset": 25, "limit": 25 } }
```

Grant record fields: `opp_id, number, title, agency, agency_code, top_agency, status,
close_date, open_date, cfda (semicolon list), is_sbir, is_sttr, program, agency_focus,
synopsis, eligibility, applicant_types, funding_instruments, award_ceiling, award_floor,
cost_sharing, contact_name, contact_email, contact_phone, link, category, enriched,
days_to_close, relevance, eligibility_score, urgency, agency_fit, tech_fit, composite,
first_seen, last_seen, attribution`.

### `GET /grant/{opp_id}`
Full detail for one opportunity. `404` if not found. `400` on a malformed id
(path-traversal / non-numeric rejected).

### `POST /match`
Score **all** grants against a JSON user profile and return the top-N with explanations.

Body:
```json
{ "profile": { "keywords": ["AI","diagnostics"],
               "agencies": ["NIH","NSF"],
               "tech_domains": ["health"] },
  "limit": 10, "min_composite": 0 }
```
Response:
```json
{ "profile_received": { "keywords": [...], "agencies": [...], "tech_domains": [...] },
  "count": 10,
  "results": [ { "opp_id": "...", "title": "...", "agency": "...", "program": "SBIR",
                 "days_to_close": 120, "link": "...",
                 "score": { "relevance": 85, "eligibility": 90, "urgency": 35,
                            "agency_fit": 100, "tech_fit": 90, "composite": 87,
                            "days_to_close": 120,
                            "reasons": ["Eligibility: SBIR/STTR program...", "..."],
                            "warnings": [] } } ] }
```

### `GET /closing`
Grants closing within `within_days` (default 14, range 1–365).

### `GET /digest?format=json|markdown`
Weekly opportunity digest: stats, closing-soon list, and top matches. `format=markdown`
returns a ready-to-send digest body.

---

## Errors
- `400` invalid input (bad id, non-numeric params where numeric expected).
- `404` unknown endpoint or grant not found.
- `401` returned if `X-API-Key` enforcement is enabled in a future build (not active now).

## Rate limits / abuse
The API is a thin local service. Put it behind a reverse proxy (nginx/Caddy) with rate
limiting before exposing publicly. Do not point it directly at the internet.

## Determinism
Given the same database, the same query returns the same result (no random ordering,
no timestamps embedded in payloads). This makes responses cacheable and testable.
