# Protests

Protests are bid protest records exposed at `/api/protests/`. For field definitions, see the [Protests Data Dictionary](../data-dictionary/protests.md).

**Authentication**: Protests endpoints require authentication (API key or OAuth2). Unauthenticated requests receive HTTP 401.

## Endpoints

- `GET /api/protests/` (list + filtering + search + pagination)
- `GET /api/protests/{case_id}/` (detail by deterministic case UUID)

Both list and detail return **case-level** objects identified by `case_id` (a deterministic UUID derived from `source_system` + `base_case_number`). Subordinate dockets are available via `?shape=...,dockets(...)` expansion. The path segment must be a valid RFC 4122 UUID (version 1–5, variant 8/9/a/b).

## Filtering

Core filters:

| Param | What it does |
| --- | --- |
| `source_system` | Filter by source system (for example `gao`). Multi-value: use `|` for OR. |
| `outcome` | Filter by protest outcome (for example `Denied`, `Dismissed`, `Withdrawn`, `Sustained`). Multi-value: use `|` for OR. |
| `case_type` | Filter by case type (for example `Bid Protest`, `Bid Protest: Cost`). Multi-value: use `|` for OR. |
| `agency` | Filter by protested agency text. Multi-value: use `|` for OR. |
| `case_number` | Filter by case number (B-number, for example `b-423274`). Multi-value: use `|` for OR. |
| `solicitation_number` | Filter by exact solicitation number. |
| `protester` | Filter by protester name text. Multi-value: use `|` for OR. |
| `filed_date_after`, `filed_date_before` | Filed date range filters. |
| `decision_date_after`, `decision_date_before` | Decision date range filters. |
| `search` | Full-text search over protest searchable fields. |

## Ordering

Protests do not support custom ordering via `ordering=`.

Results are returned in a fixed timeline order:

- `decision_date` when present, otherwise `filed_date` (newest first)
- UUID descending as a deterministic tie-breaker

If `ordering` is provided, the API returns HTTP 400.

## Pagination

Protests use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## List response: one result per case

The list endpoint returns **one result per case** (distinct `base_case_number`), not one per docket. Each result includes case-level fields from the most recent docket in the case. `case_id` is a deterministic UUID from `(source_system, base_case_number)` used for grouping and detail lookup. Pagination and filters apply to dockets first; results are then grouped, so `count` is the total number of matching **cases** across all pages.

The default (unshaped) response does **not** include nested dockets. To get dockets, use the `dockets` expansion via `?shape=...,dockets(...)`.

## Detail response: case-level

The detail endpoint (`GET /api/protests/{case_id}/`) returns a single **case-level** object, the same structure as a list item. Use `?shape=...,dockets(...)` to include nested dockets.

## Shaping

Protests support `shape` on both list and detail endpoints:

- `GET /api/protests/?shape=...`
- `GET /api/protests/{case_id}/?shape=...`

Both list and detail return case-level objects. You can request case-level fields and the `dockets` expansion (e.g. `shape=case_id,case_number,title,dockets(case_number,docket_number,filed_date)`).

Allowed shape fields (case-level for both list and detail):

- `case_id` (deterministic case UUID; use for detail lookup)
- `source_system`, `case_number` (base B-number), `title`, `protester`, `agency`
- `solicitation_number`, `case_type`, `outcome`
- `filed_date`, `posted_date`, `decision_date`, `due_date`
- `docket_url`, `decision_url`
- `digest` (opt-in only; from `raw_data.digest`, e.g. decision summary text)
- `dockets` (expand with docket fields, e.g. `dockets(case_number,docket_number,filed_date)`)
- `organization` (expand: deterministic resolution of the protested agency to a canonical Tango organization — included in the **default** response shape; canonical 7-key office payload)
- `resolved_protester` (expand: Bayesian entity resolution for the protester name; **Pro+ tier**)
- `resolved_agency` (expand: Bayesian organization resolution for the agency name; **Pro+ tier**)

Allowed shape fields inside `dockets(...)`:

- `source_system`, `case_number` (base B-number), `docket_number` (specific docket, e.g. `b-424046.1`)
- `title`, `protester`, `agency`, `solicitation_number`, `case_type`, `outcome`
- `filed_date`, `posted_date`, `decision_date`, `due_date`
- `docket_url`, `decision_url`
- `digest`

The `case_number` field (e.g. `b-424214`) identifies the case. The `docket_number` field (e.g. `b-424214.1`, `b-424214.2`) identifies the specific sub-docket within a case and is only available inside `dockets(...)`.

### Organization (deterministic): `organization(...)`

The default response shape includes an `organization(*)` expansion: the canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`) resolved deterministically at ingest time to a canonical Tango organization. This is the same payload used on awards, opportunities, vehicles, forecasts, grants, and IT Dashboard responses.

The deterministic `organization` expand and the Bayesian `resolved_agency` expand **coexist** intentionally:

- `organization` is the canonical "what does our matching pipeline think?" answer — fast, cached, and suitable for joining against the rest of your data.
- `resolved_agency` (Pro+) carries `match_confidence` (`"confident"` / `"review"`) and a human-readable `rationale`. Useful when you need to know *why* a particular org was chosen or to surface review-worthy matches to a human.

When the two disagree, prefer `organization` for canonical use and consult `resolved_agency.match_confidence` / `.rationale` to understand the discrepancy.

### Entity Resolution: `resolved_protester(...)`

When a protester name has been resolved to a canonical entity in Tango, you can expand the match via `resolved_protester(...)`. Only high-confidence (`confident`) and medium-confidence (`review`) matches are returned; unresolved or low-confidence names return `null`.

Allowed fields inside `resolved_protester(...)`:

- `uei` — Unique Entity Identifier of the matched entity
- `name` — Display name of the matched entity
- `match_confidence` — `"confident"` (auto-linkable) or `"review"` (needs human review)
- `rationale` — Human-readable explanation of why the match was made

### Organization Resolution: `resolved_agency(...)`

When an agency name has been resolved to a canonical organization in Tango, you can expand the match via `resolved_agency(...)`. Same confidence rules as `resolved_protester`.

Allowed fields inside `resolved_agency(...)`:

- `key` — UUID key of the matched organization
- `name` — Display name of the matched organization
- `match_confidence` — `"confident"` or `"review"`
- `rationale` — Human-readable explanation

Not exposed in the API (internal only):

- `raw_data`, `field_provenance`, `external_id`, `data_quality`, `source_last_updated`, `created`, `modified`
- internal search/index fields (for example `search_vector`)

Examples:

```bash
# Case-level fields and nested dockets
GET /api/protests/?shape=case_id,case_number,title,dockets(case_number,docket_number,filed_date,outcome)

# Case-level only
GET /api/protests/?shape=case_id,source_system,case_number,title,outcome,filed_date
```

More examples:

```bash
# Timeline-focused list payload
GET /api/protests/?source_system=gao&shape=case_id,title,outcome,filed_date,decision_date

# Link-focused payload
GET /api/protests/?shape=case_id,docket_url,decision_url

# Detail by case UUID with shaped response
GET /api/protests/550e8400-e29b-41d4-a716-446655440000/?shape=case_id,source_system,title,agency,protester,filed_date,decision_date

# Detail with nested dockets
GET /api/protests/550e8400-e29b-41d4-a716-446655440000/?shape=case_id,title,dockets(docket_number,filed_date,outcome)

# Entity resolution for protester
GET /api/protests/?shape=case_id,protester,resolved_protester(*)

# Organization resolution for agency
GET /api/protests/?shape=case_id,agency,resolved_agency(*)

# Selective resolution fields
GET /api/protests/?shape=case_id,resolved_protester(uei,match_confidence)
```

Notes:

- Use the `dockets` expansion to request nested docket fields (e.g. `dockets(docket_number,case_number)`).
- `*` is only valid inside expansions. Use explicit field lists at the root.
- Invalid `shape` fields return HTTP 400 with structured validation errors.
