# REDCap — API: Every REDCap Route in CoreModels: the Full HTTP Reference

The CoreModels REDCap connector declares all three capabilities - **Import, Audit, Generate** - which means every integration verb on our HTTP surface is live for it. This article is the complete route-by-route reference: what each endpoint takes, what it returns, which role it requires, and which of the two HTTP surfaces it lives on. If you're writing a script, a service, or an internal tool against the REDCap integration, this is the page to keep open.

# Every REDCap Route in CoreModels: the Full HTTP Reference

The CoreModels REDCap connector declares all three capabilities - **Import, Audit, Generate** - which means every integration verb on our HTTP surface is live for it. This article is the complete route-by-route reference: what each endpoint takes, what it returns, which role it requires, and which of the two HTTP surfaces it lives on. If you're writing a script, a service, or an internal tool against the REDCap integration, this is the page to keep open.

## Two surfaces, one posture

CoreModels exposes vendor integrations on two surfaces:

- **The interactive surface** (`/graph/integrations/...`) authenticates with your normal CoreModels login token and carries the full verb set: import, audit, reaudit, history, badge, generate, status, plus connector discovery.
- **The machine-to-machine surface** (`/v1/...`) accepts user API keys and is what CI and unattended services should call. It deliberately carries a smaller set: **audit** and **badge** only. `reaudit` and `history` live on the interactive surface.

The posture across both is read-authority: import writes to the graph (additively - existing governed nodes are never mutated), while audit and generate never write anything. Recording an audit run in the history is opt-in bookkeeping, and the reaudit verb always records its run.

Roles at a glance:

| Route | Surface | Role |
|---|---|---|
| `GET /graph/integrations/vendors` | interactive | any authenticated user |
| `POST /graph/integrations/redcap/import/{projectId}` | interactive | Admin |
| `POST /graph/integrations/redcap/audit/{projectId}` | interactive | Viewer |
| `POST /graph/integrations/redcap/reaudit/{projectId}` | interactive | Viewer |
| `GET /graph/integrations/redcap/history/{projectId}` | interactive | Viewer |
| `GET /graph/integrations/redcap/badge/{projectId}` | interactive | Viewer |
| `POST /graph/integrations/redcap/generate/{projectId}` | interactive | Viewer |
| `GET /graph/integrations/redcap/status/{projectId}` | interactive | Viewer |
| `POST /v1/{projectId}/integrations/redcap/audit` | API key | Viewer |
| `GET /v1/{projectId}/integrations/redcap/badge` | API key | Viewer |

Everything below uses `https://coremodels.example.com` as the API host and `$TOKEN` for the bearer credential.

## Discovery

```http
GET https://coremodels.example.com/graph/integrations/vendors
Authorization: Bearer $TOKEN
```

Returns every registered connector with its capabilities and expected artifacts. The REDCap entry:

```json
{ "key": "redcap", "displayName": "REDCap",
  "capabilities": "Import, Audit, Generate",
  "artifacts": {
    "data_dictionary": "required - the data dictionary CSV (Project Setup → Data Dictionary download, or the API 'metadata' export as CSV)"
  } }
```

One artifact, always required. Calling any REDCap verb with a vendor key that isn't registered gets you an explicit error: `Unknown vendor '<v>'. Registered: <comma-joined keys>.`

## Import - Admin

```http
POST https://coremodels.example.com/graph/integrations/redcap/import/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "artifacts": { "data_dictionary": "<contents of data_dictionary.csv>" } }
```

The body is an artifacts request: `artifacts` maps artifact name to raw content (required - omitting it returns `success: false` with an error spelling out the expected `'artifacts': { "<name>": "<content>" }` shape), and an optional `spaces` array targets specific space ids (empty means the project's main space). The response reports counts, whether the parsed dictionary snapshot was stored for later re-audits, and the two honesty channels - `lossiness` (what a successful import approximated or dropped) and `errors` (what made it unable to proceed).

The counts are most instructive on a *re*-import. Say three instruments (64 variables) are already governed, and the dictionary you upload now adds a fourth instrument with twelve variables plus two new variables on an existing instrument:

```json
{ "success": true, "vendor": "redcap", "projectName": "redcap-project",
  "datasetsAdded": 1, "datasetsSkippedExisting": 3, "fieldsAdded": 2,
  "lineageEdgesAdded": 0, "lineageEdgesSkipped": 0, "nodesEnriched": 82,
  "snapshotStored": true, "lossiness": [], "errors": [] }
```

Four counters, four different questions. `datasetsAdded: 1` - the new instrument, written whole; its twelve variables arrive with it, which is exactly why they do not show up in `fieldsAdded`. `fieldsAdded: 2` - that counter is specifically the additive pass over datasets that already existed, so it means "two variables were grafted onto an already-governed instrument". `datasetsSkippedExisting: 3` - already governed, left untouched: the additive contract in numbers. `nodesEnriched: 82` is one write per instrument plus one per variable (4 + 78), because vendor-metadata bookkeeping - native types, checks, PHI marks, branching logic - is refreshed on every import even for nodes that already existed; governed meaning is not. `lineageEdgesAdded` is always 0 for REDCap: a data dictionary carries structure, not pipelines.

One thing to watch on that additive pass: if a variable grafted onto an already-governed instrument carries a choice list, the Element is created but its Taxonomy is not - governed value sets are a human decision, not an import side effect. You get a `ConstraintRelaxation` entry in `lossiness` saying so, and the audit surfaces the same gap afterwards.

## Audit - Viewer

```http
POST https://coremodels.example.com/graph/integrations/redcap/audit/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "artifacts": { "data_dictionary": "<data_dictionary.csv>" },
  "recordHistory": true }
```

`recordHistory` defaults to **false** - the audit verb stays strictly read-only unless asked. When true, the run is appended to the rolling history with trigger `audit`. The response is the full audit report:

```json
{ "success": true, "vendor": "redcap", "projectName": "redcap-project",
  "errorCount": 0, "warningCount": 1, "infoCount": 4,
  "codes": { "phi-fields": 2, "text-no-validation": 2, "duplicate-choice-codes": 1 },
  "driftedObjects": [],
  "fingerprint": "9b1c2a7e0d4f8a31",
  "metrics": {
    "Datasets (estate)": "3",
    "Datasets governed": "3 / 3",
    "Fields governed": "64 / 64",
    "Governed nodes with canonical mappings": "0 / 67 (0%)",
    "Last import": "2026-07-27T08:55:10.4120000+00:00"
  },
  "findings": [
    { "section": "Conformance", "severity": "Warning", "code": "duplicate-choice-codes",
      "subject": "demographics.consent_status",
      "message": "Choice list reuses a code - exported data for this field is ambiguous.",
      "detail": null }
  ],
  "markdown": "…PR-comment-ready report…",
  "historyRecorded": true,
  "lossiness": [] }
```

(The `findings` array is abridged to one entry here; a real response carries one object per finding, and `codes` is its per-code tally.) `metrics` holds the headline numbers that lead the markdown report - how much of the estate is governed, and how many governed nodes also carry a `mapsTo` to something *other* than REDCap, which is the canonical-mapping coverage figure. It reads `0 / 67 (0%)` until you start binding ontology or standard terms to those nodes.

Findings fall into `Coverage` (`dataset-unmapped`, `field-unmapped`), `Drift` (`dataset-removed`, `field-removed`, `field-type-drift`, `enum-constraint-removed`, `enum-narrowed`, `enum-widened`, `contract-drift`) and `Conformance`, where the REDCap-specific rules live: `phi-fields` (Info - aggregated PHI inventory per instrument), `text-no-validation` (Info), `duplicate-choice-codes` (Warning). The contract that matters to automation: **`errorCount > 0` means the artifact violates governed meaning.**

## Reaudit - Viewer

```http
POST https://coremodels.example.com/graph/integrations/redcap/reaudit/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json

{}
```

Reaudit is the audit's mirror image. A live audit asks "do these fresh artifacts still conform to the governed model?"; reaudit asks "does the governed model still match the last-known estate?" It runs the same audit engine over the dictionary snapshot stored at import time against the *current* governed model - no artifacts, no REDCap access, no credentials. The optional body property `projectName` selects a specific vendor-side project (null means the latest stored snapshot), and `spaces` narrows scope. Unlike audit, the run is **always** recorded in the history, with trigger `reaudit`. The response shape is the same audit report shown above, plus one extra metric - `Snapshot stored`, the timestamp of the import whose dictionary is being replayed, so you always know how old the estate side of the comparison is. If nothing has been imported, or the dictionary was over the ~1.5 MB encoded snapshot cap, there is nothing to replay and the call says so: `No stored estate snapshot for vendor 'redcap' - import the vendor project first (imports persist the parsed snapshot).`

## History - Viewer

```http
GET https://coremodels.example.com/graph/integrations/redcap/history/{projectId}
Authorization: Bearer $TOKEN
```

```json
{ "success": true, "vendor": "redcap",
  "projects": [
    { "projectName": "redcap-project",
      "runs": [
        { "at": "2026-07-27T09:12:44.7130000+00:00", "trigger": "reaudit",
          "errorCount": 1, "warningCount": 0, "infoCount": 3,
          "codes": { "field-type-drift": 1, "phi-fields": 3 },
          "fingerprint": "9b1c2a7e0d4f8a31" } ] } ] }
```

The trail is rolling: one trail per vendor-side project, newest run first, capped at the 50 most recent (an append past the cap drops the oldest). Each run records its trigger (`audit`, `reaudit`, `ci`, or `scheduled` from the opt-in server-side heartbeat), the three counters, the per-code counts, and the artifact fingerprint, which lets you see at a glance whether two runs audited the same dictionary bytes.

## Badge - Viewer

```http
GET https://coremodels.example.com/graph/integrations/redcap/badge/{projectId}
Authorization: Bearer $TOKEN
```

Returns `image/svg+xml`: a self-contained shields-style badge labeled `redcap audit`, rendered from the latest recorded run. Green (`#4c1`) is clean, yellow (`#dfb317`) warnings only, red (`#e05d44`) errors, gray (`#9f9f9f`) no recorded runs.

## Generate - Viewer

```http
POST https://coremodels.example.com/graph/integrations/redcap/generate/{projectId}
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "typeNames": [] }
```

Generate reverses the flow: governed model out, vendor artifact in hand. `typeNames` restricts output to the named types (empty means everything eligible); `targetVersion` and `extra` exist on the request shape for vendors that need dialect switches - REDCap doesn't. The response:

```json
{ "success": true,
  "artifacts": [
    { "name": "coremodels_data_dictionary.csv", "kind": "csv",
      "content": "\"Variable / Field Name\",\"Form Name\",…" } ],
  "lossiness": [], "errors": [] }
```

One artifact: an upload-ready data dictionary CSV with the full 18-column REDCap header. Governed taxonomies become dropdowns with minted numeric codes, NotNull checks become `Required Field? = y`, and governed types become validation types (`integer`, `number`, `date_ymd`) - Boolean becomes a `yesno` field. Cross-form references have no REDCap slot and are carried as plain text fields - a documented limit of the format, not silent loss. If no governed fields are eligible, the call fails explicitly with `No eligible governed fields found to emit as a data dictionary.` rather than emitting an empty file.

## Status - Viewer

```http
GET https://coremodels.example.com/graph/integrations/redcap/status/{projectId}
Authorization: Bearer $TOKEN
```

Returns `{ "success": true, "vendor": "redcap", "imported": true, "state": { … }, "governedDatasets": 3 }` - whether an import has happened, the last-import state recorded on the integration state node (timestamps, fingerprint, counts, facts), and how many governed datasets currently resolve to Types in the graph.

## The v1 surface - audit and badge for machines

User API keys authenticate here, which is why CI calls it. The audit takes the identical artifacts request, but the response is wrapped in the standard `ApiResponse` envelope, so every field above lives under `data.*`:

```bash
curl -sS -X POST \
  "https://coremodels.example.com/v1/$PROJECT_ID/integrations/redcap/audit" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  --data-binary @audit-request.json | jq '.data.errorCount'
```

With `recordHistory: true`, runs recorded through this surface carry the trigger `ci` - so your history distinguishes gate runs from interactive ones. The badge is also here (`GET /v1/{projectId}/integrations/redcap/badge`), embeddable wherever you can attach a user API key. That's the entire v1 vendor surface by design: the machine surface audits and reports; it never imports, never generates, never writes.

## One honest limitation

There is no live REDCap connection on any route. `LiveSync` is a declared-but-deferred capability across our connectors: you upload the dictionary CSV your REDCap tooling already produces, and your REDCap API token never reaches CoreModels.

The compact version of this reference, with extraction recipes, lives in the REDCap quickstart in the CoreModels docs (`docs/quickstarts/redcap`).
