Contract appeals¶
Contract appeals are the decisions of the two boards of contract appeals, exposed at /api/contract_appeals/. For field definitions, see the Contract Appeals Data Dictionary.
A contract appeal is a dispute under a contract that already exists. A contractor and the government disagree over a claim, a termination, delay, or how much is owed; the contracting officer issues a final decision; the contractor appeals it to a board under the Contract Disputes Act. Two boards hear them:
- CBCA — the Civilian Board of Contract Appeals, which hears appeals from most civilian agencies. Coverage runs from 2007 to the present.
- ASBCA — the Armed Services Board of Contract Appeals, which hears appeals from the Department of Defense, NASA and the CIA. Coverage runs from 2010 to the present.
About 8,200 decisions are available across the two boards, and the dataset is updated daily.
Contract appeals are not bid protests
A bid protest challenges an award or a solicitation before or just after award — who should have won. A contract appeal challenges a contracting officer's final decision under a contract already in hand — what is owed under it. They are different proceedings, in different forums, usually with different parties, and Tango keeps them in separate resources.
Bid protests (GAO, the U.S. Court of Federal Claims, and SBA OHA) live at /api/protests/. Nothing appears in both resources.
Authentication: Contract appeal endpoints require authentication (API key or OAuth2). Unauthenticated requests receive HTTP 401.
Plans: every endpoint, every filter and the full-text search are available on every plan, including Free. One field — decision_text, the extracted body of the decision — requires an Enterprise plan. Searching that text is free; reading it is not. See decision_text.
Endpoints¶
GET /api/contract_appeals/— list, with filtering, search, ordering and paginationGET /api/contract_appeals/{uuid}/— one decision
The detail path segment is the decision's uuid, and it must be a valid RFC 4122 UUID — anything else is a 404. The uuid is stable: it survives a rebuild of the dataset, so it is safe to store alongside your own records.
One row is one decision as the board lists it¶
Each board publishes its decisions as a listing, and one row here is one document on that listing.
Three consequences are worth knowing before your first query:
- A consolidated appeal carries several dockets.
docket_numbersis an array, not a string. One decision resolving four consolidated appeals lists all four. - A decision the board stops listing is kept, not deleted.
listedgoes tofalseand the row stays, so a decision you retrieved last month still resolves on itsuuid. Filter?listed=trueif you want only what the boards currently publish. - Not every field is populated on every row, and the gaps follow the boards' own history. ASBCA's listing gained a docket-number column in 2014, so
docket_numbersis empty on most ASBCA decisions before then. CBCA's listing gained a decision-type column in 2015, sodecision_typeis null on CBCA decisions before then — and it is null on every ASBCA decision, because that board publishes no type at all.
Filtering¶
Identifier and classification filters accept | (or the word OR) for OR. Date filters require YYYY-MM-DD; invalid dates or inverted ranges return 400 (see Date filters). search and appellant must be at least 2 characters.
| Param | What it does |
|---|---|
board | cbca or asbca. Case-insensitive. Multi-value: use \| for OR. |
docket | Exact match on one of the decision's docket numbers — see Matching a docket. Multi-value: use \| for OR. |
appellant | Case-insensitive substring match on the appellant's name. Min 2 characters. Multi-value: use \| for OR. |
judge | Case-insensitive exact match on the judge as the board's listing names them. Multi-value: use \| for OR. |
decision_type | CBCA only: Decision, Dismissal, Order, Full Board Order, or the listing's own wording where it matches none of those. Multi-value: use \| for OR. |
decision_date_after, decision_date_before | Decision-date range (YYYY-MM-DD). |
listed | Boolean. false returns decisions the board's newest listing no longer carries. |
document_id | Exact match on the board's document identifier. Useful for pinning one document when several listing rows link it. |
search | Ranked full-text search over the appellant and the decision text — see Search. Min 2 characters. |
An unrecognized query parameter on the list endpoint returns 400 with a did-you-mean suggestion rather than being silently ignored, so a mistyped filter never returns a full, unfiltered page.
Matching a docket¶
?docket= matches one docket exactly, against any element of the decision's docket_numbers array.
Dockets are stored without the board prefix, as the board writes them minus its own name: 59116, 3288-R, 7092-C(6682, 6765, 6767). You do not have to strip anything yourself — a leading CBCA or ASBCA, and No. or Nos., are ignored, so a docket can be pasted straight out of a citation:
GET /api/contract_appeals/?docket=59116
GET /api/contract_appeals/?docket=ASBCA%20No.%2059116
GET /api/contract_appeals/?docket=CBCA%203288-R
All three of those are the same query, and suffix letters are matched case-insensitively (3288-r finds 3288-R).
Two things this filter is not:
- It is not a prefix or substring search.
?docket=5911does not find59116. A quantum or remand docket can name its source dockets in parentheses —7092-C(6682, 6765, 6767)— and that whole string is one docket, so quoting6682alone does not reach it. - The comma is not a separator. Because a docket can contain commas,
|(orOR) is the only way to ask for several:?docket=59116|59117.
Search¶
?search= is a ranked full-text query over the appellant name and the full text of the decision, and it is available on every plan including Free.
Wrap a value in double quotes for a phrase: ?search="differing site conditions". Without quotes, the terms are matched individually.
Under search= with no explicit ordering, results come back by relevance. ordering=rank asks for that explicitly and requires a non-empty search — passing it alone returns 400.
Searching the decision text is free; reading it is not. A search response tells you which decisions matched and carries no fragment of the body, so there is nothing to gate. Reading the body is a separate, Enterprise-only field — see decision_text.
Search reaches the decisions whose text we hold. text_status and text_char_count report, on every plan, whether a given decision has extracted text behind it, so you can tell a decision with no text from one whose text simply did not match.
Ordering¶
ordering= allowlist:
decision_date— the default is-decision_date(newest first)appellantfirst_listed_at— when Tango first observed the decision on a board's listing, which is the field to poll onrank— relevance; requires a non-emptysearch, and is the implicit default under one
decision_date and appellant sort empties last in both directions, so a page is never headed by undated rows. Results are tie-broken deterministically, so paging through a large result set never repeats or skips a decision.
Pagination¶
Standard page-number pagination:
page(default 1)limit(default 25, max 100)
Responses carry count / next / previous / results.
Shaping¶
Contract appeals are a flat resource: every field is a leaf, and there are no expandable relations (so no * expansions). See Response shaping for the syntax.
- List default:
uuid,board,docket_numbers,decision_date,appellant,judge,decision_type,url - Detail default: the list fields plus
docket_raw,docket_source,decision_date_repaired,decision_type_raw,listing_year,first_listed_at,listed,text_status,text_char_count
Every other field — decision_date_raw, document_id, listing_url and decision_text — is available by naming it in ?shape=. The full list is in the data dictionary.
decision_text — the decision body¶
GET /api/contract_appeals/{uuid}/?shape=uuid,board,docket_numbers,decision_text
decision_text is the extracted text of the board's own decision document. It requires an Enterprise plan, and three rules govern it:
- You have to name it. It is in neither default shape, because a single decision runs to roughly 100,000 characters — twenty-five of them is not a list page.
- Below Enterprise, the request still succeeds. You get 200 with the rest of your shape intact, the field omitted, and an entry in
meta.upgrade_hintsnaming it and the plan that unlocks it. It is not a 403, and the rest of the response is unaffected. - The field can be null at any plan.
decision_textis null until the decision's document has been through text extraction, and on a decision whose document yielded no text at all.text_status(completed,empty,failed, or null when not yet attempted) andtext_char_counttell you which case you are in, on every plan — so you can decide whether a body is worth buying before you buy it.
A below-Enterprise response to a shape naming decision_text:
{
"count": 1,
"meta": {
"upgrade_hints": {
"message": "Some requested fields were omitted because they require a higher tier subscription.",
"fields": [
{"field": "decision_text", "required_tier": "Enterprise A"}
],
"upgrade_url": "https://docs.makegov.com/getting-started/pricing/"
}
},
"results": [
{
"uuid": "7a2e4c1b-3d5f-5e8a-9b0c-1d2e3f4a5b6c",
"board": "asbca",
"docket_numbers": ["63456", "63457"]
}
]
}
Every other field on the resource, including the board's own link to the decision document in url, is available on every plan.
Alerts¶
contract_appeal is an alertable query type — alerts.contract_appeal.match, using the same filters documented above. See the webhooks guide.
An alert fires when a matching decision first appears and when a matching decision changes. Two changes are worth calling out, because both are real writes rather than query-time derivations:
- A delisting fires. When a board's newest listing stops carrying a decision,
listedflips tofalseand subscribers hear about it. - Newly extracted text fires. A decision gaining its
decision_textis a change to the record, so a subscription can be used to follow extraction — including on plans that cannot read the text itself.
Examples¶
# One ASBCA docket
GET /api/contract_appeals/?board=asbca&docket=57530
# Differing-site-conditions decisions since 2024
GET /api/contract_appeals/?search=differing+site+conditions&decision_date_after=2024-01-01
# Construction appellants, newest first
GET /api/contract_appeals/?appellant=construction&ordering=-decision_date
# CBCA dismissals in a window, timeline payload
GET /api/contract_appeals/?board=cbca&decision_type=Dismissal&decision_date_after=2025-01-01&shape=uuid,docket_numbers,appellant,decision_date,url
# Everything new since your last poll
GET /api/contract_appeals/?ordering=-first_listed_at&shape=uuid,board,docket_numbers,appellant,decision_date,first_listed_at
# Decisions the boards no longer list
GET /api/contract_appeals/?listed=false&shape=uuid,board,docket_numbers,decision_date
# Detail, with the decision body (Enterprise)
GET /api/contract_appeals/{uuid}/?shape=uuid,board,docket_numbers,appellant,decision_date,decision_text
Notes:
*is only valid inside expansions; contract appeals have none, so use explicit field lists.- Invalid
shapefields return HTTP 400 with structured validation errors.