# Brickroad API v1

Base URL: `https://brickroad.network/api/v1`

Authentication:

```http
Authorization: Bearer br_live_your_token
```

Set the API root once:

```bash
BASE_URL="https://brickroad.network/api/v1"
```

Public API routes are for external scripts and agents. Internal routes under `/api/dma` are not supported API contracts.

## MCP

The same organization API key authenticates a Streamable HTTP MCP server at `https://brickroad.network/mcp`. Use this from Cursor, Claude, or any MCP client instead of writing fetch calls. Paste the key into the `Authorization` header — there is no separate sign-in.

```json
{
  "mcpServers": {
    "brickroad-api": {
      "url": "https://brickroad.network/mcp",
      "headers": {
        "Authorization": "Bearer <key>"
      }
    }
  }
}
```

Replace `<key>` with your full API key. Cursor also supports `${env:BRICKROAD_API_TOKEN}` if that variable is already set in its shell or system environment. The config key is `brickroad-api`.

The server exposes these tools. Legacy extra-field-profile routes stay on REST only.

| Tool | Use |
| --- | --- |
| `ifa_submit_query` | Submit an information-frontier discovery query. This spends organization credits, returns immediately with a queryId, and the run can take 30 minutes or more. Do not retry on timeout — poll ifa_get_query instead. |
| `ifa_list_queries` | List recent information-frontier queries for the token organization. |
| `ifa_get_query` | Get one query by queryId, including status and links to its sources. Poll this after submit; do not resubmit. |
| `ifa_cancel_query` | Cancel a queued or running query. Cancellation is best-effort; cancelled can be false if the run is already past the cancellable window. |
| `ifa_list_sources` | List sources produced by a completed query. Requires queryId from ifa_get_query. |
| `ifa_get_source` | Get one source with evidence cells. Requires queryId and sourceId from ifa_list_sources. |
| `ifa_get_source_contacts` | Get contact fields for one source. Requires queryId and sourceId. |
| `ifa_task_profile` | List, read, create, update, or delete a task profile. Use action=list|get|create|update|delete. get/update/delete require profileId. create requires scope, taskId, and name. Organization-scoped writes need an owner or admin token. |
| `ifa_prompt_rule` | List, create, update, or delete a token-owner prompt rule. Use action=list|create|update|delete. update/delete require ruleId. create requires instruction. |

The API contract is also an MCP resource at `https://brickroad.network/api/v1/docs.md`.

## Routes

| Method | Path | Use |
| --- | --- | --- |
| GET | `/ifa/extra-field-profile` | read the legacy token-owner extra-field profile |
| PUT | `/ifa/extra-field-profile` | create or replace the legacy token-owner extra-field profile |
| DELETE | `/ifa/extra-field-profile` | remove the legacy token-owner extra-field profile |
| GET | `/ifa/task-profiles` | list organization and token-owner task profiles |
| POST | `/ifa/task-profiles` | create a task profile |
| GET | `/ifa/task-profiles/{profileId}` | read one task profile |
| PATCH | `/ifa/task-profiles/{profileId}` | edit one task profile |
| DELETE | `/ifa/task-profiles/{profileId}` | remove one task profile |
| GET | `/ifa/prompt-rules` | list the token owner IFA prompt rules |
| POST | `/ifa/prompt-rules` | create a token owner IFA prompt rule |
| PATCH | `/ifa/prompt-rules/{ruleId}` | edit one token owner IFA prompt rule |
| DELETE | `/ifa/prompt-rules/{ruleId}` | remove one token owner IFA prompt rule |
| POST | `/ifa/queries` | submit a new information frontier query |
| GET | `/ifa/queries` | list recent information frontier queries |
| GET | `/ifa/queries/{queryId}` | get query status and result links |
| POST | `/ifa/queries/{queryId}/cancel` | cancel a queued or running query |
| GET | `/ifa/queries/{queryId}/sources` | list sources produced by a completed query |
| GET | `/ifa/queries/{queryId}/sources/{sourceId}` | get one source with evidence cells |
| GET | `/ifa/queries/{queryId}/sources/{sourceId}/contacts` | get contact fields for one source |
| GET | `/docs.md` | markdown documentation for agents and scripts |
| GET | `/docs` | markdown documentation alias without a file suffix |

## ID Flow

Use response fields from one route to call the next route:

1. Submit a query or list existing queries.
2. Save `queryId` from the query payload.
3. Use that `queryId` to poll status and list sources.
4. Save `sources[0].id` from the source-list payload.
5. Use both `queryId` and `sourceId` to read source detail or contacts.

```text
POST /ifa/queries
  -> response.queryId
  -> GET /ifa/queries/{queryId}
  -> GET /ifa/queries/{queryId}/sources
       -> response.sources[].id
       -> GET /ifa/queries/{queryId}/sources/{sourceId}
       -> GET /ifa/queries/{queryId}/sources/{sourceId}/contacts
```

`queryId` scopes the source list. It is not a source id. A `sourceId` is only meaningful inside a query/org, so detail routes require both ids.

## Submit Query

`POST /ifa/queries`

Request:

```json
{
  "query": "Find alternative data vendors for EV battery supply chain visibility"
}
```

Request fields:

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `query` | string | yes | Non-empty natural-language discovery request. |
| `preset` | string | no | `balanced` or `thorough`. Missing, null, or unknown values normalize to `balanced`. |
| `fastVerify` | boolean | no | Enables the faster verification path when true. |
| `steering` | object | no | Additional free-form query hints forwarded to the discovery worker. |
| `taskProfileIds` | string[] | no | Explicit named task profiles for this run. At most one profile can target each task. |

```bash
curl -sS $BASE_URL/ifa/queries \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"Find alternative data vendors for EV battery supply chain visibility"}'
```

Response is async. A query can take 30 minutes or more. Do not keep an LLM call open while waiting.

```json
{
  "queryId": "e61c14h8thwg2fqb3yqvx5ct",
  "query": "Find alternative data vendors for EV battery supply chain visibility",
  "status": "queued",
  "createdAt": "2026-06-30T19:30:00.000Z",
  "updatedAt": null,
  "nextPollAfterSeconds": 300,
  "links": {
    "self": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct",
    "sources": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources"
  }
}
```

The submit route returns HTTP 202. Depending on available capacity the submitted query may answer `status: "queued"` or `status: "running"` — both are documented enum values; poll the same way in either case. Submitting and cancelling require a personal API token with an active owner.

## Configure IFA

Configuration applies only to future IFA jobs. A personal API token can manage profiles for its owner and can read profiles from its organization. Organization owners and admins can also manage organization profiles. No request accepts an arbitrary organization ID.

IFA has three configuration types:

| Type | Behavior |
| --- | --- |
| Task profiles | Named settings for one task. Save many, but select at most one for each task in a run. |
| Prompt rules | Separate additive instructions. All enabled matching user and organization rules apply. |
| Legacy extra-field profile | One older user field set. It stays active during migration but new integrations should not create it. |

### Task Profiles

Task profiles are named settings for one core task. You can save many profiles for the same task. Only one profile is selected for each task in a run.

Selection order is deterministic:

1. A profile named in `taskProfileIds`.
2. The token owner's default profile for that task.
3. The organization's default profile for that task.
4. No profile.

An explicit profile must belong to the token owner or the token's organization. Prompt rules are separate and remain additive.

`POST /ifa/task-profiles` creates a profile. This ranking example keeps its required Enrich signals and QA ranking policy in one profile:

```bash
curl -sS -X POST $BASE_URL/ifa/task-profiles \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "user",
    "taskId": "ifa-qa",
    "name": "History and delivery fit",
    "enabled": true,
    "isDefault": true,
    "extraFields": [
      {
        "name": "history_breadth",
        "label": "History breadth",
        "type": "number",
        "description": "Score breadth of usable history from 0 to 1.",
        "required": true,
        "min": 0,
        "max": 1,
        "producerTask": "ifa-enrich"
      },
      {
        "name": "delivery_fit",
        "label": "Delivery fit",
        "type": "number",
        "description": "Score delivery latency fit from 0 to 1.",
        "required": true,
        "min": 0,
        "max": 1,
        "producerTask": "ifa-enrich"
      }
    ],
    "rankingPolicy": {
      "version": 1,
      "criteria": [
        { "field": "history_breadth", "weight": 60 },
        { "field": "delivery_fit", "weight": 40 }
      ]
    }
  }'
```

Profile fields:

| Field | Rule |
| --- | --- |
| `scope` | `user` or `organization`. Organization writes require owner or admin role. Scope cannot change after creation. |
| `taskId` | One core task from the checked-in task manifest. Task cannot change after creation. |
| `name` | One to 100 characters. Unique within scope and task, ignoring case. |
| `enabled` | Disabled profiles stay stored but cannot be selected or used as defaults. |
| `isDefault` | At most one default per scope and task. Saving a new default clears the old default in that scope. |
| `taskInputs` | Only manifest-declared user inputs within manifest bounds. Request values and existing organization defaults still override these profile values during migration. |
| `extraFields` | Typed evidence fields. Ranking fields must be numeric, declared in this profile, and bounded from 0 to 1. |
| `rankingPolicy` | Optional weighted ranking for a task that declares support. Criteria must be unique and weights must total 100. |
| `extensions` | Reserved. Must remain empty until the task manifest declares guards for extension data. |

List visible organization and user profiles with `GET /ifa/task-profiles`. Read, edit, or delete one visible profile with `GET`, `PATCH`, or `DELETE /ifa/task-profiles/{profileId}`. A profile response includes `revision` and `profileHash`; each run freezes both before work starts.

Task input order is request value, existing organization default, selected task profile, then the checked-in task default. Existing organization defaults remain in place for backward compatibility. If an operator moves those defaults into task profiles, the old keys must be removed in the same controlled migration.

During migration, enabled legacy extra fields and selected task-profile extra fields are combined. Field names must be unique across both sources. Do not copy a legacy field into a task profile until the old row is disabled, or the query will fail validation instead of choosing one value silently.

The create response uses HTTP 201:

```json
{
  "taskProfile": {
    "id": "profile_id_here",
    "scope": "user",
    "taskId": "ifa-qa",
    "name": "History and delivery fit",
    "enabled": true,
    "isDefault": true,
    "schemaVersion": 1,
    "revision": 1,
    "taskInputs": {},
    "extraFields": [],
    "rankingPolicy": null,
    "extensions": {},
    "profileHash": "sha256:1234567890abcdef",
    "createdAt": "2026-09-02T20:00:00.000Z",
    "updatedAt": "2026-09-02T20:00:00.000Z"
  }
}
```

Select non-default profiles when starting a query:

```bash
curl -sS $BASE_URL/ifa/queries \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Find alternative data sources for freight activity",
    "taskProfileIds": ["profile_id_here"]
  }'
```

### Legacy Extra Field Profile

New clients should put extra fields in a task profile. This older endpoint remains available for existing integrations during migration.

This endpoint still reads and writes the legacy extra-field store. It does not write through to `/ifa/task-profiles` in this release. One global legacy field set cannot map safely to one of many task-specific profiles without choosing a task and possibly replacing an existing default. Existing legacy rows and API clients therefore keep their current behavior after deployment.

Use one profile to define extra result fields. The profile is validated before storage. When a query starts, Brickroad copies a frozen profile snapshot into its IFA jobs. The DMA must return matching values when it phones results home.

`PUT /ifa/extra-field-profile` creates or replaces the owner profile:

```bash
curl -sS -X PUT $BASE_URL/ifa/extra-field-profile \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "jobTypes": ["ifa"],
    "fieldDefinitions": [
      {
        "name": "coverage_regions",
        "label": "Coverage regions",
        "type": "text",
        "description": "Regions where this source provides data coverage.",
        "required": true,
        "maxLength": 500
      },
      {
        "name": "history_years",
        "label": "History years",
        "type": "integer",
        "description": "Estimated years of historical data available.",
        "required": false,
        "min": 0,
        "max": 100
      }
    ]
  }'
```

Response:

```json
{
  "profile": {
    "id": "a5wicze8iqj8md8q8fkvlm1d",
    "enabled": true,
    "jobTypes": ["ifa"],
    "fieldDefinitions": [
      {
        "name": "coverage_regions",
        "label": "Coverage regions",
        "type": "text",
        "description": "Regions where this source provides data coverage.",
        "required": true,
        "maxLength": 500
      },
      {
        "name": "history_years",
        "label": "History years",
        "type": "integer",
        "description": "Estimated years of historical data available.",
        "required": false,
        "min": 0,
        "max": 100
      }
    ],
    "profileHash": "sha256:1234567890abcdef",
    "createdAt": "2026-08-10T20:00:00.000Z",
    "updatedAt": "2026-08-10T20:00:00.000Z"
  }
}
```

Profile fields:

| Field | Rule |
| --- | --- |
| `enabled` | Optional boolean. Default is `true`. Disabled profiles are stored but not applied. |
| `jobTypes` | Must be exactly `["ifa"]` in API v1. This scopes fields to the complete IFA query. |
| `fieldDefinitions` | One or more fields. Names must be unique and must not replace a core IFA field. |
| `fieldDefinitions[].name` | Starts with a letter. Uses letters, numbers, and underscores only. Maximum 64 characters. |
| `fieldDefinitions[].label` | Display label. One to 100 characters. |
| `fieldDefinitions[].description` | Research instruction for the DMA. One to 500 characters. |
| `fieldDefinitions[].type` | One of `text`, `integer`, `number`, `boolean`, or `enum`. |
| `fieldDefinitions[].required` | Boolean. Required fields must be returned for enriched sources. |
| `min` and `max` | Optional finite numeric limits for `integer` and `number` only. |
| `maxLength` | Optional maximum length for `text` only. Maximum 10000. |
| `options` | Required array of one to 100 unique strings for `enum` only. |

Read the current profile:

`GET /ifa/extra-field-profile`

```bash
curl -sS $BASE_URL/ifa/extra-field-profile \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "profile": {
    "id": "a5wicze8iqj8md8q8fkvlm1d",
    "enabled": true,
    "jobTypes": ["ifa"],
    "fieldDefinitions": [
      {
        "name": "coverage_regions",
        "label": "Coverage regions",
        "type": "text",
        "description": "Regions where this source provides data coverage.",
        "required": true,
        "maxLength": 500
      },
      {
        "name": "history_years",
        "label": "History years",
        "type": "integer",
        "description": "Estimated years of historical data available.",
        "required": false,
        "min": 0,
        "max": 100
      }
    ],
    "profileHash": "sha256:1234567890abcdef",
    "createdAt": "2026-08-10T20:00:00.000Z",
    "updatedAt": "2026-08-10T20:00:00.000Z"
  }
}
```

When no profile exists, the response is:

```json
{
  "profile": null
}
```

Remove the profile:

`DELETE /ifa/extra-field-profile`

```bash
curl -sS -X DELETE $BASE_URL/ifa/extra-field-profile \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "deleted": true
}
```

Deletion is idempotent. It returns `{ "deleted": false }` when no profile exists.

Migration is an operator action after deployment:

1. Inventory the legacy user or organization field set.
2. Create the matching task profile as disabled. For a field-only profile, use `ifa-enrich`; a ranking profile that contains its own Enrich signals uses `ifa-qa`.
3. Compare names, types, limits, required flags, and producer tasks.
4. In one controlled change, enable the new default and disable the legacy row. Do not leave duplicate field names active.
5. Inspect the next normal run trace for the selected profile ID, revision, fields, and final status.

Prompt rules do not move. They stay in the prompt-rule API and remain additive. After all legacy rows and callers are migrated, a later release can remove this endpoint or turn it into a narrow compatibility adapter.

Extra fields appear in `source.evidence` from the source-detail route. The source-list response keeps its fixed summary shape. A custom evidence key can be absent for a sparse, deferred, failed, or older source. A key with `value: null` means the field was evaluated but no value was found.

### Prompt Rules

Prompt rules add persistent task instructions. They are automatically injected into every new matching IFA task for the token owner. Do not send a `promptRules` value to `POST /ifa/queries`; create a stored rule first.

`taskId` selects where the rule applies:

| Task ID | Applies to | Generic example |
| --- | --- | --- |
| `ifa-query` | Original monolith task and, as a compatibility alias, every V2 phase | Prefer primary sources over aggregators throughout the run. |
| `ifa-discover` | Candidate search and discovery | Search operational software and supply-chain companies before established data vendors. |
| `ifa-enrich` | Candidate profiling and enrichment | Record the operational event that creates each proposed data asset. |
| `ifa-verify` | Evidence grounding and verification | Verify ownership and update cadence from official sources. |
| `ifa-qa` | Final inclusion and quality review | Exclude candidates whose ownership or update cadence remains unknown. |

Omitting `taskId` keeps the original behavior and stores the rule as `ifa-query`. An unsupported task ID returns `invalid_prompt_rule_task_id` with the complete `supportedTaskIds` list.

For example, this discovery-only rule limits candidate generation to selected regions:

```bash
curl -sS -X POST $BASE_URL/ifa/prompt-rules \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "ifa-discover",
    "instruction": "Only return sources with data coverage in Canada, the United States, or Western Europe. Exclude all other regions.",
    "enabled": true
  }'
```

Response:

```json
{
  "promptRule": {
    "id": "b6xjd3f9jzq9ne0f9glwm2nd",
    "taskId": "ifa-discover",
    "instruction": "Only return sources with data coverage in Canada, the United States, or Western Europe. Exclude all other regions.",
    "enabled": true,
    "createdAt": "2026-08-10T20:00:00.000Z",
    "updatedAt": "2026-08-10T20:00:00.000Z"
  }
}
```

`instruction` must contain one to 2000 characters after trimming. Each owner can store up to 10 prompt rules across all supported IFA task IDs.

List stored rules:

`GET /ifa/prompt-rules`

```bash
curl -sS $BASE_URL/ifa/prompt-rules \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "promptRules": [
    {
      "id": "b6xjd3f9jzq9ne0f9glwm2nd",
      "taskId": "ifa-discover",
      "instruction": "Only return sources with data coverage in Canada, the United States, or Western Europe. Exclude all other regions.",
      "enabled": true,
      "createdAt": "2026-08-10T20:00:00.000Z",
      "updatedAt": "2026-08-10T20:00:00.000Z"
    }
  ],
  "supportedTaskIds": [
    "ifa-query",
    "ifa-discover",
    "ifa-enrich",
    "ifa-verify",
    "ifa-qa"
  ]
}
```

Use `PATCH /ifa/prompt-rules/{ruleId}` to change `taskId`, `instruction`, `enabled`, or any combination:

```bash
curl -sS -X PATCH \
  $BASE_URL/ifa/prompt-rules/b6xjd3f9jzq9ne0f9glwm2nd \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"taskId":"ifa-qa","enabled":false}'
```

Response:

```json
{
  "promptRule": {
    "id": "b6xjd3f9jzq9ne0f9glwm2nd",
    "taskId": "ifa-qa",
    "instruction": "Only return sources with data coverage in Canada, the United States, or Western Europe. Exclude all other regions.",
    "enabled": false,
    "createdAt": "2026-08-10T20:00:00.000Z",
    "updatedAt": "2026-08-10T20:15:00.000Z"
  }
}
```

Remove a rule:

`DELETE /ifa/prompt-rules/{ruleId}`

```bash
curl -sS -X DELETE \
  $BASE_URL/ifa/prompt-rules/b6xjd3f9jzq9ne0f9glwm2nd \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "deleted": true
}
```

Deletion is idempotent. It returns `{ "deleted": false }` when no matching rule exists.

## List Queries

`GET /ifa/queries`

Use the optional `limit` query parameter to return 1 to 100 recent queries. The default is 25. Invalid values use the default; values outside the range are clamped.

```bash
curl -sS "$BASE_URL/ifa/queries?limit=25" \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "queries": [
    {
      "queryId": "e61c14h8thwg2fqb3yqvx5ct",
      "query": "Find alternative data vendors for EV battery supply chain visibility",
      "status": "completed",
      "createdAt": "2026-06-30T19:30:00.000Z",
      "updatedAt": "2026-06-30T19:58:00.000Z",
      "nextPollAfterSeconds": 0,
      "links": {
        "self": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct",
        "sources": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources"
      }
    }
  ]
}
```

## Poll Status

```bash
curl -sS $BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Use `nextPollAfterSeconds` from the response. Polling is cheap JSON. Your script should sleep outside the LLM thread.

Response:

```json
{
  "queryId": "e61c14h8thwg2fqb3yqvx5ct",
  "query": "Find alternative data vendors for EV battery supply chain visibility",
  "status": "completed",
  "createdAt": "2026-06-30T19:30:00.000Z",
  "updatedAt": "2026-06-30T19:58:00.000Z",
  "nextPollAfterSeconds": 0,
  "links": {
    "self": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct",
    "sources": "/api/v1/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources"
  }
}
```

Statuses:

- `queued`: accepted, waiting for an agent slot
- `running`: dispatched to the data-discovery worker
- `completed`: source results are ready
- `failed`: query failed before or during discovery
- `cancelled`: user cancelled the query

### Query Fields

| Field | Type | Meaning |
| --- | --- | --- |
| `queryId` | string | Query id used by status, cancel, source-list, source-detail, and contact routes. |
| `query` | string | Original query text. |
| `status` | string | One of the query statuses above. |
| `createdAt` | ISO 8601 string | When the query was accepted. |
| `updatedAt` | ISO 8601 string or null | Latest known source or completed-run update. |
| `nextPollAfterSeconds` | number | Suggested wait before the next status poll. Zero means no more polling is needed. |
| `links.self` | string | API-root-relative status URL. |
| `links.sources` | string | API-root-relative source-list URL. |

## Read Sources

```bash
curl -sS "$BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources" \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

The default response contains one representative source for each canonical entity key. Brickroad stores the key and performs grouping in the database. Known company subdomains and exact owner-matched GitHub pages can share a key. Package pages also need matching GitHub owner evidence. Shared-site and ambiguous pages stay separate. The API prefers a Final source, then an enriched source, then the best available contact level and scores.

Use `final=true` to return only sources that survived final selection. Brickroad applies this filter in the database before entity grouping:

```bash
curl -sS "$BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources?final=true" \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Use `raw=true` only when you need every discovered page:

```bash
curl -sS "$BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources?raw=true" \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

`raw` and `final` are independent controls:

| Parameters | Result |
| --- | --- |
| none | One representative per canonical entity across all candidates. |
| `final=true` | One representative per canonical entity from final delivery. |
| `raw=true` | Every discovered source page. |
| `raw=true&final=true` | Every final-delivery source row without entity grouping. |

A completed query can return `{ "sources": [] }` with `final=true` when no source survived final selection.

Response:

```json
{
  "sources": [
    {
      "id": "a5wicze8iqj8md8q8fkvlm1d",
      "name": "Example Data Co",
      "description": "Tracks specialty logistics signals.",
      "url": "https://example.com",
      "whyMatched": "mentions battery logistics coverage",
      "scores": {
        "ranking": 0.91,
        "novelty": 0.7,
        "relevance": 0.8,
        "accessibility": 0.6,
        "alpha": 0.75
      },
      "tags": {
        "geography": ["US"],
        "gics": [
          {
            "sector": "Industrials",
            "industryGroup": "Capital Goods",
            "industry": "Electrical Equipment",
            "subIndustry": "Electrical Components & Equipment"
          }
        ],
        "exchanges": ["NASDAQ"],
        "instruments": ["equities"],
        "sampleTickers": ["TSLA"],
        "tickers": ["TSLA", "equities"],
        "categories": ["supply_chain"]
      },
      "contactAvailable": true
    }
  ]
}
```

Use `sources[].id` from this response as `sourceId` for source detail and contacts.

The list route returns a compact summary for each grouped entity by default. With `final=true`, it limits the list to final delivery. With `raw=true`, it returns a summary for every source page. Use both parameters together for ungrouped final-delivery rows. Use the detail route for the full enriched evidence cells.

### Source Summary Fields

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | string | Source id scoped to this query and organization. |
| `name` | string | Best available source or company name. Falls back to the entity key or URL. |
| `description` | string or null | Short source description when available. |
| `url` | string | Source website or product URL. |
| `whyMatched` | string or null | First available value from `why_matched`, `reasoning`, or the source discovery query. |
| `scores` | object | Public ranking, novelty, relevance, accessibility, and alpha scores. |
| `tags` | object | Public coverage classifications. Arrays are present even when empty. |
| `contactAvailable` | boolean | Whether a usable level 1-3 contact or contact evidence exists. |

### Scores

All score values are numbers or null. Component scores are normally from 0 to 1. The composite `ranking` score can exceed 1 for general-intent queries.

| Field | Meaning |
| --- | --- |
| `ranking` | Intent-aware ordering score. Explicit queries favor relevance, exploratory queries favor alpha, and general queries combine relevance and novelty. It can be null before ranking is materialized or for older/direct-read results. |
| `novelty` | How hard the source is to find through conventional research. |
| `relevance` | Topical fit to the interpreted query. |
| `accessibility` | Estimated procurement/contact feasibility. It does not guarantee that a contact or license is available. |
| `alpha` | Novelty multiplied by relevance. |

### Coverage Tags

Coverage tags are AI-assisted classifications grounded in real market taxonomies and identifiers. They describe the data a source covers. They are not hidden inputs used to calculate the scores above.

| Field | Type | Meaning |
| --- | --- | --- |
| `geography` | string array | Legacy geography classifications when available. |
| `gics` | object array | Four-level GICS classifications. Every object contains `sector`, `industryGroup`, `industry`, and `subIndustry`. |
| `exchanges` | string array | Exchanges where the covered public companies trade. |
| `instruments` | string array | Asset types covered by the data. |
| `sampleTickers` | string array | Real example tickers the data may provide signal for. |
| `categories` | string array | Public name for the coverage-tag `dataCategory` array, such as `alternative`, `geospatial`, or `supply_chain`. |
| `tickers` | string array | Backward-compatible combination of `sampleTickers` and `instruments`. New integrations should use those fields directly. |

Tags can be empty when a source has not reached enrichment or when an older result predates coverage tagging.

## Read Source Detail

```bash
curl -sS $BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources/a5wicze8iqj8md8q8fkvlm1d \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "source": {
    "id": "a5wicze8iqj8md8q8fkvlm1d",
    "name": "Example Data Co",
    "description": "Tracks specialty logistics signals.",
    "url": "https://example.com",
    "whyMatched": "mentions battery logistics coverage",
    "scores": {
      "ranking": 0.91,
      "novelty": 0.7,
      "relevance": 0.8,
      "accessibility": 0.6,
      "alpha": 0.75
    },
    "tags": {
      "geography": ["US"],
      "gics": [
        {
          "sector": "Industrials",
          "industryGroup": "Capital Goods",
          "industry": "Electrical Equipment",
          "subIndustry": "Electrical Components & Equipment"
        }
      ],
      "exchanges": ["NASDAQ"],
      "instruments": ["equities"],
      "sampleTickers": ["TSLA"],
      "tickers": ["TSLA", "equities"],
      "categories": ["supply_chain"]
    },
    "contactAvailable": true,
    "status": "enriched",
    "evidence": {
      "why_matched": {
        "value": "mentions battery logistics coverage",
        "status": "verified",
        "confidence": 0.92,
        "evidence": {
          "url": "https://example.com/product",
          "quote": "[redacted short supporting quote]"
        }
      },
      "dataset_type": {
        "value": "Shipment and inventory event data",
        "status": "verified",
        "confidence": 0.9,
        "evidence": {
          "url": "https://example.com/product"
        }
      },
      "data_format": {
        "value": "API and CSV",
        "status": "verified",
        "confidence": 0.9,
        "evidence": {
          "url": "https://example.com/docs"
        }
      },
      "email": {
        "value": "contact@example.com",
        "status": "verified",
        "confidence": 0.88,
        "evidence": {
          "url": "https://example.com/contact"
        }
      }
    }
  }
}
```

### Detail-only Fields

The detail response contains every summary field plus:

| Field | Type | Meaning |
| --- | --- | --- |
| `status` | string | Current source pipeline status. |
| `evidence` | object | Dynamic map keyed by IFA column name. |

### Evidence Cell Shape

Every key inside `evidence` uses this shape:

| Field | Type | Meaning |
| --- | --- | --- |
| `value` | string or null | Enriched value. Null means the column exists but no value was found. |
| `status` | string | Pipeline-defined verification or inference status. Handle unknown future values. |
| `confidence` | number | Confidence from 0 to 1. |
| `evidence` | any JSON value | Supporting URLs, quotes, metadata, or null. The exact proof shape can vary by column. |

### Canonical Business Evidence Fields

These are the stable business fields clients can use when present:

| Category | Key | Meaning |
| --- | --- | --- |
| Identification | `company_name` | Normalized company or source name. |
| Dataset profile | `dataset_type` | Inferred kind of dataset and its time or subject coverage. |
| Dataset profile | `data_format` | Known or inferred delivery and storage formats. |
| Dataset profile | `implied_labels` | Labels, dimensions, or structure implied by the dataset. |
| Dataset profile | `use_cases` | Potential research, ML, or operational uses. |
| Scale | `registered_users` | Known or estimated registered user or customer count. |
| Scale | `mau_dau` | Known or estimated monthly and daily active users. |
| Scale | `content_volume` | Known or estimated volume of records, files, or content. |
| Scale | `growth_status` | Current activity and growth signal. |
| Historical depth | `product_launch` | Earliest known product or dataset launch. |
| Historical depth | `significant_scale` | When the product or dataset reached material scale. |
| Historical depth | `archive_depth` | Estimated historical lookback or archive coverage. |
| Contacts | `primary_contact` | Best available named contact. |
| Contacts | `primary_title` | Role or title of the primary contact. |
| Contacts | `primary_linkedin` | LinkedIn URL for the primary contact. |
| Contacts | `email` | Best available contact email. |
| Qualification | `scale_estimate` | Overall company or dataset scale assessment. |
| Qualification | `feasibility` | Estimated feasibility of contacting or licensing the data. |
| Qualification | `qualification_notes` | Important licensing, privacy, IP, or procurement notes. |
| Qualification | `recommended_next_step` | Suggested next research or outreach action. |
| Qualification | `found_in_catalogues` | Whether the source is already an established catalogue supplier. |

Coverage classifications belong in the top-level `tags` object. Public scores belong in the top-level `scores` object. The pipeline can return extra diagnostic evidence keys, but those keys are not a stable public contract and should not be required by integrations.

### Source Status and Sparse Results

- `discovered`: basic metadata only; business evidence and coverage tags can be empty.
- `enriched`: enrichment completed; unavailable individual fields can still be absent or null.
- `deferred`: the source was not selected for enrichment in this run.
- `enrich_failed`: enrichment was attempted but failed.
- `dedup_demoted`: the source was removed as a duplicate.
- Future status strings can be added. Clients should preserve unknown values instead of failing.

Absence and null are different:

- Missing evidence key: the pipeline did not return that column for this source.
- Evidence key with `value: null`: the column exists, but no value was found.
- Empty tag array: that classification was unavailable or not yet produced.
- Null score: that score was unavailable, not materialized, or from an older/direct-read result.

The API does not invent missing evidence. Discovered, deferred, failed, older, and partially enriched sources can all be sparse.

## Read Contacts

```bash
curl -sS $BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/sources/a5wicze8iqj8md8q8fkvlm1d/contacts \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "sourceId": "a5wicze8iqj8md8q8fkvlm1d",
  "primaryContact": {
    "name": "Jordan Lee",
    "title": "Head of Data Partnerships",
    "linkedinUrl": "https://www.linkedin.com/in/jordan-lee",
    "email": "contact@example.com"
  },
  "contacts": {
    "primary_contact": "Jordan Lee",
    "primary_title": "Head of Data Partnerships",
    "primary_linkedin": "https://www.linkedin.com/in/jordan-lee",
    "email": "contact@example.com"
  }
}
```

`primaryContact` is the stable normalized contact shape. Its four keys are always present and use null when unavailable. Values are selected by exact fallback order:

| Normalized field | Evidence fallback order |
| --- | --- |
| `name` | `primary_contact`, `contact_name`, `contact` |
| `title` | `primary_title`, `contact_title`, `procurement_contact` |
| `linkedinUrl` | `primary_linkedin`, `contact_linkedin`, `linkedin` |
| `email` | `email`, `contact_email` |

`contacts` preserves non-empty raw values under their original column names for backward compatibility and debugging. Possible raw keys are `primary_contact`, `primary_title`, `primary_linkedin`, `email`, `contact_name`, `contact_title`, `contact_linkedin`, `contact_email`, `contacts`, `contact`, `contact_info`, and `procurement_contact`. No fuzzy or regex-based guessing is used.

`contactAvailable` on source responses is true for contact levels 1 through 3 or when contact evidence exists. Level 4 means no usable contact was found.

If no contacts exist yet:

```json
{
  "sourceId": "a5wicze8iqj8md8q8fkvlm1d",
  "primaryContact": {
    "name": null,
    "title": null,
    "linkedinUrl": null,
    "email": null
  },
  "contacts": {}
}
```

## Cancel Query

```bash
curl -sS -X POST $BASE_URL/ifa/queries/e61c14h8thwg2fqb3yqvx5ct/cancel \
  -H "Authorization: Bearer $BRICKROAD_API_TOKEN"
```

Response:

```json
{
  "queryId": "e61c14h8thwg2fqb3yqvx5ct",
  "status": "cancelled",
  "cancelled": true
}
```

Cancellation is best-effort and `cancelled` can be `false`: the query may already be terminal, may have no active run to cancel, or may be past the point where its processing pipeline supports cancellation (some runs are only cancellable in their early phase, before results start materializing). A `false` answer means the run keeps going — poll the query status as usual.

## JavaScript / TypeScript Helper Pattern

```ts
type BrickroadQuery = {
  queryId: string;
  status: "queued" | "running" | "completed" | "failed" | "cancelled";
  nextPollAfterSeconds: number;
  links: { self: string; sources: string };
};

const baseUrl = process.env.BRICKROAD_API_BASE_URL ?? "https://brickroad.network/api/v1";
const token = process.env.BRICKROAD_API_TOKEN;
if (!token) throw new Error("missing BRICKROAD_API_TOKEN");

async function brickroad<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${baseUrl}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
      ...init.headers,
    },
  });
  const body = await response.json();
  if (!response.ok) {
    throw new Error(`${response.status} ${body.error?.code}: ${body.error?.message}`);
  }
  return body as T;
}

const created = await brickroad<BrickroadQuery>("/ifa/queries", {
  method: "POST",
  body: JSON.stringify({
    query: "Find alternative data vendors for EV battery supply chain visibility",
  }),
});

let current = created;
while (["queued", "running"].includes(current.status)) {
  await new Promise((resolve) =>
    setTimeout(resolve, current.nextPollAfterSeconds * 1000),
  );
  current = await brickroad<BrickroadQuery>(`/ifa/queries/${created.queryId}`);
}

const sources = await brickroad(`/ifa/queries/${created.queryId}/sources`);
console.log(sources);
```

## Python Helper Pattern

```python
import os
import time
import requests

BASE_URL = "https://brickroad.network/api/v1"
TOKEN = os.environ["BRICKROAD_API_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

created = requests.post(
    f"{BASE_URL}/ifa/queries",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={"query": "Find alternative data vendors for EV battery supply chain visibility"},
    timeout=30,
).json()

query_id = created["queryId"]

while True:
    status = requests.get(
        f"{BASE_URL}/ifa/queries/{query_id}",
        headers=HEADERS,
        timeout=30,
    ).json()
    if status["status"] in ["completed", "failed", "cancelled"]:
        break
    time.sleep(status.get("nextPollAfterSeconds", 300))

sources = requests.get(
    f"{BASE_URL}/ifa/queries/{query_id}/sources",
    headers=HEADERS,
    params={"view": "summary"},
    timeout=30,
).json()
print(sources)
```

## Errors

All errors use one shape:

```json
{
  "success": false,
  "error": {
    "code": "query_not_found",
    "message": "Query not found."
  }
}
```

Common errors:

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_body` | Request body must be JSON. |
| 400 | `invalid_task_profile` | Task profile fields did not pass validation. See `error.details.issues`. |
| 400 | `invalid_task_profile_ids` | Query profile IDs are malformed or repeated. |
| 400 | `invalid_task_profile_selection` | More than one selected profile targets the same task. |
| 400 | `missing_query` | `query` is required for submit. |
| 400 | `invalid_extra_field_profile` | Profile field definitions did not pass validation. See `error.details.issues`. |
| 400 | `invalid_prompt_rule` | Prompt instruction did not pass validation. See `error.details.issues`. |
| 400 | `invalid_prompt_rule_task_id` | Prompt-rule task ID is unsupported. See `error.details.supportedTaskIds` for the complete list. |
| 401 | `missing_token` | Missing `Authorization: Bearer ...`. |
| 401 | `invalid_token` | Token is malformed, unknown, or wrong secret. |
| 401 | `revoked_token` | Token was revoked. Create a new API key. |
| 403 | `inactive_owner` | Personal token owner is no longer active in the org. |
| 403 | `owner_required` | Route needs a personal-token owner, not a service token. |
| 403 | `owner_not_found` | Personal token owner record could not be loaded. |
| 403 | `task_profile_forbidden` | Only an organization owner or admin can change an organization profile. |
| 403 | `credit_limit_reached` | Org has no credits for another IFA query. |
| 404 | `query_not_found` | Query id does not exist in your token org, or belongs to another org. |
| 404 | `task_profile_not_found` | Profile id is disabled, outside the token scope, or does not exist. |
| 404 | `source_not_found` | Source id does not exist for that query/org. |
| 404 | `prompt_rule_not_found` | Prompt rule id does not belong to the token owner. |
| 409 | `no_active_agent` | Org has no active IFA agent. |
| 409 | `task_profile_conflict` | A profile name or default slot conflicts in the same scope and task. |
| 409 | `prompt_rule_limit_reached` | Token owner already has 10 IFA prompt rules. |
| 409 | `results_not_ready` | Query exists, but sources are not ready. Poll status first. |
| 500 | `server_misconfigured` | API token auth is not configured. |
| 500 | `internal_error` | Unexpected server error. |

Security rules:

- API tokens are org-scoped.
- You cannot pass `orgId` in the payload or URL to switch orgs.
- Query and source ids from another org return 404.
- Query text can contain alpha, so logs never include raw query text.
- Raw tokens are shown once in the app and are never returned by public API routes.
