Integration API

The Integration API is how a system other than the Reader reads appeal data — an intake system handing a file in, a drafting tool pulling citations out. It is distinct from how the Reader itself runs: in production the Reader talks to the datastore directly via scoped IAM (see Users & teams → Two access models). The API is the contract for everyone else.

It supports reads (appeals, documents, search) and writes (annotations and tags), authenticated on every request, and is served from an isolated environment over synthetic data. That isolation is structural, not procedural: it runs as a separate codebase in a dedicated cloud account that has no access path to any real-data system. So this is a live, hands-on developer sandbox — it can be exercised freely, by anyone, including the write endpoints, without the possibility of touching a real record.

Base URL

$INTEGRATION_API   # your integration endpoint

The sandbox is invite-only: you’re provisioned a base URL plus a username + password (see the Quickstart). The base URL is a per-environment API Gateway endpoint, freely shareable — every environment serves only synthetic data. All paths below are relative to it.

Authentication

Every request carries a bearer token, and a request with no token (or an expired one) returns 401 before it reaches any data.

Authorization: Bearer <token>

Get a token

Exchange the username + password you were provisioned for a token at the token endpoint:

POST /auth/token
curl -s -X POST "$INTEGRATION_API/auth/token" \
  -H "Content-Type: application/json" \
  -d '{"username": "<you>", "password": "<your-password>"}'
{
  "token": "<bearer-token>",
  "refreshToken": "<refresh-token>",
  "expiresIn": 3600
}

Tokens are short-lived; keep the refreshToken to mint a new one without re-prompting. Invalid credentials return 401 (with a generic message — no account enumeration). There is no long-lived API key; authorization is always a fresh, expiring token bound to a provisioned identity. (Under the hood the token is a standard JWT, the same one the Reader’s web sign-in produces — see Users & teams — but you don’t need to know that to use the API.)

Quickstart

TOKEN=$(curl -s -X POST "$INTEGRATION_API/auth/token" \
  -H "Content-Type: application/json" \
  -d '{"username": "<you>", "password": "<your-password>"}' | jq -r .token)

curl -s -H "Authorization: Bearer $TOKEN" "$INTEGRATION_API/appeals" | jq
# → { "appeals": [ { "id": "...", "docketNumber": "...", "veteran": {...}, "uploadedAt": "..." }, … ] }

Resources

List appeals

GET /appeals

Returns every appeal in the environment, most-recently-uploaded first.

{
  "appeals": [
    {
      "id": "demo-3247821",
      "docketNumber": "240118-3247821",
      "veteran": {"firstName": "Marcus", "middleName": "T", "lastName": "Rivera"},
      "uploadedAt": "2024-12-04T19:22:11Z"
    }
  ]
}

Get one appeal

GET /appeals/{id}
{
  "appeal": {
    "id": "demo-3247821",
    "docketNumber": "240118-3247821",
    "veteran": {"firstName": "Marcus", "middleName": "T", "lastName": "Rivera"},
    "uploadedAt": "2024-12-04T19:22:11Z",
    "originalPdfBytes": 87422155
  },
  "documentCount": 24
}

Returns 404 if no appeal has that id.

List documents in an appeal

GET /appeals/{id}/documents

Documents in attorney-arranged order, each with its categories and tags folded in.

{
  "documents": [
    {
      "id": "doc-9c2b41",
      "name": "VA Form 21-526EZ — Application for Disability Compensation",
      "startPage": 1,
      "endPage": 7,
      "position": 0,
      "receiptDate": "2023-08-14",
      "description": "Initial claim for service connection — tinnitus, lumbar strain",
      "categories": ["procedural"],
      "tags": ["Issue: Tinnitus"]
    }
  ]
}

Get one document

GET /appeals/{id}/documents/{docId}

Returns the single document with its categories, tags, and annotations. 404 if the document isn’t in that appeal.

Search within an appeal

GET /appeals/{id}/search?q=<text>

Full-text search across every page of the appeal’s documents. Returns the page number and a surrounding snippet for each hit (capped at 50).

{
  "query": "tinnitus",
  "hits": [
    {"page": 3, "snippet": "…reported ringing in both ears (tinnitus) beginning during active service…"}
  ]
}

Search is appeal-scoped — each query runs against one appeal (see Data & isolation). Finding evidence across cases means querying each appeal.

Writes

Writes are scoped to a document within an appeal, and the created record is stamped with the authenticated caller as createdBy.

Add a tag

POST /appeals/{id}/documents/{docId}/tags
// request
{ "text": "Issue: Tinnitus" }
// → 201
{ "tag": { "text": "Issue: Tinnitus", "createdBy": "you@org", "createdAt": "…" } }

Remove a tag

DELETE /appeals/{id}/documents/{docId}/tags/{text}

Returns 204. {text} is URL-encoded.

Create an annotation

POST /appeals/{id}/documents/{docId}/annotations
// request — page is required; x/y are normalized 0–1 coordinates
{ "page": 3, "x": 0.41, "y": 0.18, "comment": "Tinnitus reported in 2018 STRs", "relevantDate": "2018-04-12" }
// → 201
{ "annotation": { "id": "ann-…", "page": 3, "x": 0.41, "y": 0.18, "comment": "…", "relevantDate": "2018-04-12", "createdBy": "you@org", "createdAt": "…" } }

Created annotations appear on the document’s annotations array on subsequent reads.

Delete an annotation

DELETE /appeals/{id}/documents/{docId}/annotations/{annId}

Returns 204.

Errors

Errors return a JSON body with an error field and the matching HTTP status:

StatusMeaning
400Malformed request (e.g. search with no q)
401Missing or invalid token
404Appeal or document not found
500Internal error
{"error": "appeal not found"}