# Black Ice — Workspace API Reference

Black Ice exposes two HTTP endpoints that let external tools (Claude Code, agents, scripts) **read** and **write** your ontology — concepts, terms, relationships, classes, and market constraints.

- **Base URL:** `https://oosdjmpzvcljxwaqxcko.supabase.co/functions/v1`
- **Auth:** every request must include header `x-api-key: bice_xxxxxxxx`
- **Key scopes:** each key is created as either **read only** (may call `GET /query-ontology` only) or **read and write**. Use a read-only key for one-directional integrations that pull the ontology as reference data. A read-only key used against `POST /mutate-ontology` returns `403` with `"code": "insufficient_scope"`.
- **Tier required:** Pro or Builder (writes); Pro+ for reads
- **Rate limits (per workspace, per minute):** Free 60 reads / 30 writes · Pro 300 / 120 · Builder 1,000 / 400 — see [Rate limits](#rate-limits)
- **Monthly cap:** Free workspaces are capped at **10,000 API + MCP calls per calendar month**. Pro and Builder have no monthly cap — see [Rate limits](#rate-limits)
- **Max ops per write request:** 50

Generate a key in **Settings → API Access** in the Black Ice UI.

---

## 1. Read endpoint — `GET /query-ontology`

Returns the entire workspace ontology in a single JSON document.

### Request
```bash
curl --header "x-api-key: bice_YOUR_KEY_HERE" \
  "https://oosdjmpzvcljxwaqxcko.supabase.co/functions/v1/query-ontology"
```

### Optional filter
| Query param | Description |
|-------------|-------------|
| `locale`    | ISO code (e.g. `es-es`). Returns only that target market instead of all targets. |

### Markdown bundle (AI Agent Bundle) download
> **Need Markdown? Use `format=markdown-zip`.** Do not rebuild the bundle from the JSON response yourself — the server builds it with the exact same code as the app and GitHub Sync.

The same Markdown files as the in-app ZIP download and GitHub Sync (`CLAUDE.md`, `classes/`, `markets/`, `terminology/`, `relationships/`).

```bash
# Whole bundle as a ZIP (recommended)
curl --header "x-api-key: bice_YOUR_KEY_HERE" -o black-ice-ontology.zip \
  "https://oosdjmpzvcljxwaqxcko.supabase.co/functions/v1/query-ontology?format=markdown-zip"

# One file as plain Markdown
curl --header "x-api-key: bice_YOUR_KEY_HERE" \
  "https://oosdjmpzvcljxwaqxcko.supabase.co/functions/v1/query-ontology?format=markdown&path=markets/es-es.md"
```

| Query param | Description |
|-------------|-------------|
| `format` | `markdown-zip` (ZIP), `markdown` (one file, needs `path`), `markdown-files` (JSON `{ version, generatedAt, files: [{ path, content }] }`). |
| `path` | File path inside the bundle, e.g. `CLAUDE.md`, `markets/es-es.md`, `terminology/feat_00000016.md`. Unknown paths return 404 with `availablePaths`. |
| `locale` | Limit markets and target terms to one market. |
| `approved_only` | `true` to include only approved target terms. |

**Snapshots and freshness.** Every file starts with a comment giving the bundle version and generation time, and noting that the live workspace is the source of truth. Responses carry `ETag` / `X-Bundle-Version` / `X-Generated-At`. Send `If-None-Match: "<version>"` to get `304 Not Modified` when nothing changed, or use `HEAD` to read just the version. Each request counts as one API call. For AI assistants prefer the live MCP connection; use the ZIP or GitHub Sync for pipelines.

### Response shape
Each market includes `definitions` — its market guidelines, with the saved section order (`sections`) and custom section text (`custom`). `cultureDna` holds the same value and is kept only as an older name; use `definitions`.

```json
{
  "exportedAt": "2026-04-18T10:00:00.000Z",
  "version": "1.0",
  "scope": "full",
  "brandIdentity": { "name": "...", "tagline": "...", "positioning": "..." },
  "markets": [{ "code": "en-us", "name": "English (US)", "nativeName": "...", "direction": "ltr", "brandIdentity": {...} }],
  "classes": [{ "id": "...", "name": "Feature", "description": "...", "color": "#0ea5e9", "translationPolicy": "prefer_adapt" }],
  "terminology": [
    {
      "conceptId": "feat_00000001",
      "conceptName": "MIDI Editor",
      "definition": "...",
      "className": "Feature",
      "classColor": "#0ea5e9",
      "sourceTerm": { "locale": "en-us", "term": "MIDI Editor", "availability": "available", "customFields": [] },
      "targetTerm": { "locale": "es-es", "term": "Editor MIDI", "availability": "available", "allowedVariants": [], "forbiddenVariants": [], "notes": null, "status": "approved", "customFields": [] }
    }
  ],
  "relationships": [
    { "sourceConceptId": "...", "sourceConceptName": "...", "relationshipType": "hasfeature", "relationshipLabel": "hasFeature", "targetConceptId": "...", "targetConceptName": "...", "conditions": null }
  ]
}
```

---

## 2. Write endpoint — `POST /mutate-ontology`

Submit a batch of operations. Each operation is processed independently and returns its own status — partial success is normal.

### Request envelope
```json
{
  "operations": [
    { "action": "...", "entity": "...", "id": "...", "match": {...}, "data": {...} }
  ]
}
```

### Response envelope
```json
{
  "results": [
    { "index": 0, "status": "created", "id": "feat_00000042" },
    { "index": 1, "status": "error", "error": "Concept not found in workspace" }
  ]
}
```

`status` ∈ `created` | `updated` | `deleted` | `upserted` | `error`.

---

### Submitting term candidates (review queue)

Send `{ "candidates": [...] }` instead of `operations` to add entries to the Candidates review queue. They never go directly into the ontology; a person decides on each one.

- Required: `term` (1-200 chars), `locale_code`. Optional: `definition` (up to 2,000), `class_name`, `source` (default `api`), `frequency`, `confidence` (only orders the queue), `provenance` (object or array).
- Up to 500 per request. Requires a read-and-write key.
- Response: `{ "candidates": { "created", "updated", "suppressed", "resurfaced", "errors" } }`. `suppressed` means the word is on the rejection list.
- AI assistants can do the same through the MCP tool `suggest_candidates` (50 per call, read-and-write key or editor sign-in). Off by default: a workspace owner or admin must turn on candidate suggestions in Integrations.

## 3. Operations matrix

| Entity              | create | update | delete | upsert |
|---------------------|:-----:|:-----:|:-----:|:-----:|
| `concept`           | ✅ | ✅ | ✅ | — |
| `term`              | ✅ | ✅ | ✅ | ✅ |
| `relationship`      | ✅ | — | ✅ | — |
| `class`             | ✅ | ✅ | ✅ | — |
| `market_constraint` | ✅ | ✅ | ✅ | ✅ |
| `market` (locale)   | ✅ | ✅ | ✅ | — |

---

## 4. Entity schemas

### 4.1 `concept`
Concepts are the global, language-neutral nodes in your ontology. IDs are auto-generated as `{class_prefix}_{8-digit}` (e.g. `feat_00000001`).

**create** — `data`:
| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | ✅ | ≤ 500 chars |
| `className` | string | ✅* | Class name (case-insensitive). Either this or `category_id`. |
| `category_id` | uuid | ✅* | Internal class UUID. |
| `description` | string | — | ≤ 2000 chars |

**update** — requires `id` (the concept ID like `feat_00000001`); same fields as create, all optional.
**delete** — requires `id`. Cascades to terms, market constraints, and relationships.

### 4.2 `term`
Multilingual terms attached to a concept for a specific locale.

**Allowed fields:** `preferred_term`, `status`, `notes`, `market_availability`, `allowed_variants`, `forbidden_variants`, `is_source`, `concept_id`, `locale_code`.

**create** — `data` requires:
- `concept_id` (string), `locale_code` (string, lowercased), `preferred_term` (string)
- Optional: `status` (default `pending`), `is_source` (default `false`), `market_availability` (default `not_set`), `notes`, `allowed_variants[]`, `forbidden_variants[]`
- `status` ∈ `pending` | `approved` | `rejected` | `deprecated`. Rejected means reviewed but not accepted; Deprecated means retired from use.

**update** — requires `id` (term UUID); pass any allowed fields in `data`.

**delete** — requires `id`.

**upsert** — requires `match: { concept_id, locale_code }` + `data`. Updates if a term exists for that pair, otherwise creates one. **Recommended for sync workflows.**

`market_availability` ∈ `available` | `planned` | `not_available` | `unapproved` | `not_set`.

### 4.3 `relationship`
Edges between two concepts. Bidirectional types automatically create the inverse edge (and delete it when deleted).

**create** — `data`:
| Field | Type | Required | Notes |
|---|---|---|---|
| `source_concept_id` | string | ✅ | |
| `target_concept_id` | string | ✅ | |
| `relationship_type` | string | ✅* | Type `value` like `hasfeature`. Either this or `relationship_type_id`. |
| `relationship_type_id` | uuid | ✅* | Internal UUID. |
| `conditions` | string | — | Free-form qualifier. |

**delete** — requires `id`. If the type is bidirectional, the inverse record is deleted automatically. (No `update` action — delete + recreate.)

### 4.4 `class`
Categories that group concepts and define translation policy.

**create** — `data`:
| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | ✅ | ≤ 255 chars |
| `description` | string | — | ≤ 1000 chars |
| `color` | string | — | hex `#rrggbb`, default `#6366f1` |
| `translation_policy` | enum | — | `prefer_adapt` (default) or `do_not_translate` |

**update** — requires `id`; same fields, all optional.
**delete** — requires `id`. Fails if any concepts are still linked.

### 4.5 `market_constraint`
Per-market availability metadata for a concept.

**create** — `data`: `concept_id` (req), `market` (req), `available` (default `true`), optional `tier`, `disclaimer`, `local_plan_name`, `platforms[]`.
**update** — requires `id`; partial update of the optional fields.
**delete** — requires `id`.
**upsert** — requires `match: { concept_id, market }` + `data`.

### 4.6 `market` (locale)
Markets are the locale rows that hold language metadata **and** the rich "market definitions" (culture DNA) used to ground translation and content generation. The market `code` (e.g. `es-es`) is the identifier — pass it as `id` for `update`/`delete`.

**create** — `data`:
| Field | Type | Required | Notes |
|---|---|---|---|
| `code` | string | ✅ | Locale code, ≤ 20 chars, lowercased server-side (e.g. `es-es`, `pt-br`). |
| `name` | string | ✅ | English name, ≤ 100 chars (e.g. `Spanish (Spain)`). |
| `native_name` | string | ✅ | Native-language name, ≤ 100 chars (e.g. `Español`). |
| `direction` | enum | — | `ltr` (default) or `rtl`. |
| `status` | enum | — | `active` (default) or `inactive`. |
| `market_definitions` | object | — | 10-section culture DNA, see below. |

**update** — requires `id` = the market code; same fields, all optional. `market_definitions` is **shallow-merged** onto the existing object — pass only the keys you want to change, untouched keys are preserved.

**delete** — requires `id`. Fails if the market is the workspace's source market — change the source market first.

#### `market_definitions` schema
All 10 fields are optional free-form strings (Markdown-friendly, used verbatim as AI grounding context). Send the keys you want to set/overwrite; omit the rest.

| Key | Purpose |
|---|---|
| `marketSnapshot` | Market reality, audience, competitive mental model. |
| `voiceLocaleDNA` | Core voice traits, keywords, what to avoid. |
| `toneStyleRules` | Sentence shape, preferred vs forbidden phrasing. |
| `culturePragmatics` | Cultural norms, implicit expectations. |
| `grammarMechanics` | Spelling variant, punctuation, sentence-length rules. |
| `uxMicrocopyRules` | CTA patterns, system-message tone, button labels. |
| `localeConventions` | Currency, date/time format, pricing framing. |
| `terminologyRules` | Preferred vs avoided terms specific to the market. |
| `goldenExamples` | Reference snippets (hero, subhead, feature framing, AI framing). |
| `outputConstraints` | Hard limits / never-do rules for generated copy. |

> Read the current values first via `GET /query-ontology` — they are returned on each item in `markets[].market_definitions`.

---

## 5. Worked examples

### 5.1 Create a concept
```bash
curl -X POST "https://oosdjmpzvcljxwaqxcko.supabase.co/functions/v1/mutate-ontology" \
  -H "x-api-key: bice_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {
        "action": "create",
        "entity": "concept",
        "data": {
          "name": "Stem Splitter",
          "className": "Feature",
          "description": "Separates a track into vocals, drums, bass, and other stems."
        }
      }
    ]
  }'
```
Response: `{"results":[{"index":0,"status":"created","id":"feat_00000042"}]}`

### 5.2 Add a Spanish translation (upsert — safe to retry)
```bash
curl -X POST ".../mutate-ontology" \
  -H "x-api-key: bice_..." -H "Content-Type: application/json" \
  -d '{
    "operations": [{
      "action": "upsert",
      "entity": "term",
      "match": { "concept_id": "feat_00000042", "locale_code": "es-es" },
      "data": {
        "preferred_term": "Separador de pistas",
        "status": "pending",
        "market_availability": "available",
        "allowed_variants": ["Divisor de stems"],
        "forbidden_variants": ["Stem Splitter"]
      }
    }]
  }'
```

### 5.3 Link two concepts (bidirectional auto-handled)
```bash
curl -X POST ".../mutate-ontology" \
  -H "x-api-key: bice_..." -H "Content-Type: application/json" \
  -d '{
    "operations": [{
      "action": "create",
      "entity": "relationship",
      "data": {
        "source_concept_id": "plan_00000002",
        "target_concept_id": "feat_00000042",
        "relationship_type": "hasfeature"
      }
    }]
  }'
```
If `hasfeature` is bidirectional, the inverse `(feat_00000042 → plan_00000002)` is created automatically.

### 5.4 Batch — create concept + source term in one call
```json
{
  "operations": [
    { "action": "create", "entity": "concept",
      "data": { "name": "Auto-Mastering", "className": "Feature", "description": "AI-driven mastering chain." }
    },
    { "action": "upsert", "entity": "term",
      "match": { "concept_id": "REPLACE_WITH_ID_FROM_PREVIOUS", "locale_code": "en-us" },
      "data": { "preferred_term": "Auto-Mastering", "is_source": true, "status": "approved", "market_availability": "available" }
    }
  ]
}
```
> ⚠️ Operations execute sequentially but the concept ID isn't available until the first response. For chained creates, do two requests.

### 5.5 Patch market definitions (edit two sections of `es-es`)
```bash
curl -X POST ".../mutate-ontology" \
  -H "x-api-key: bice_..." -H "Content-Type: application/json" \
  -d '{
    "operations": [{
      "action": "update",
      "entity": "market",
      "id": "es-es",
      "data": {
        "market_definitions": {
          "toneStyleRules": "Short sentences. Active voice. Address the creator directly (tú, not usted).",
          "terminologyRules": "Preferred: Estudio (not Espacio de trabajo). Pista (not Activo). Avoid English loanwords when a clean Spanish term exists."
        }
      }
    }]
  }'
```
Only `toneStyleRules` and `terminologyRules` are overwritten; the other 8 sections are preserved.

---

## 6. Error reference

| HTTP | Meaning |
|---|---|
| 401 | Missing/invalid `x-api-key` |
| 403 | Read-only key used for a write (`"code": "insufficient_scope"`), or workspace tier doesn't include API writes |
| 405 | Wrong method (use GET for read, POST for write) |
| 429 | Rate limit exceeded (see [Rate limits](#rate-limits)) |
| 200 | Always returned for write requests — inspect `results[i].status` |

Common per-op errors: `Concept not found in workspace`, `Class "X" not found`, `Unknown field(s) for term: ...`, `market_availability must be one of: ...`.

---

## 6b. Rate limits
<a id="rate-limits"></a>

Limits are applied **per workspace, per minute**, and scale with the subscription plan. Reads cover `GET /query-ontology` and every MCP read tool; writes cover `POST /mutate-ontology`.

| Plan | Reads / min | Writes / min |
|---|---|---|
| Free | 60 | 30 |
| Pro | 300 | 120 |
| Builder | 1,000 | 400 |

Every response carries the current state of your window:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1755612480     # epoch seconds when the window resets
```

A throttled request returns `429` with `Retry-After` (seconds) and a body naming your plan and cap:

```json
{
  "error": "Rate limit exceeded: 300 requests/minute on the Pro plan. Retry in 24s.",
  "tier": "pro",
  "limit": 300
}
```

Back off until `Retry-After` elapses rather than retrying immediately. The throttle is a fair-use guard — it never throttles below the advertised number, and sustained higher throughput can be arranged via support.

### Monthly call cap (Free plan)

In addition to the per-minute throttle, **Free workspaces have a hard cap of 10,000 API + MCP calls per calendar month**. Pro and Builder have **no monthly cap**.

- Every REST call and every MCP tool call counts as **one call**, reads and writes alike.
- The counter resets at `00:00 UTC` on the 1st of each month.
- Free-plan responses carry the monthly state alongside the per-minute headers:

```
X-RateLimit-Scope: month
X-RateLimit-Month-Limit: 10000
X-RateLimit-Month-Remaining: 6588
X-RateLimit-Month-Reset: 1756684800   # epoch seconds — 1st of next month, 00:00 UTC
```

- Once the cap is reached, calls return `429` with `Retry-After` and this body:

```json
{
  "error": "Monthly call limit reached: 10,000 calls/month on the Free plan. Resets on 1 September.",
  "code": "monthly_cap_reached",
  "tier": "free",
  "limit": 10000,
  "used": 10000
}
```

Branch on `code === "monthly_cap_reached"` to distinguish this from a per-minute throttle: retrying in a few seconds will not help — the allowance only resets next month, or immediately on upgrading the plan. Current usage is visible in the app under **Integrations → Usage & Analytics**. Using the app's UI, imports, and GitHub Sync does **not** consume the allowance; only programmatic API and MCP calls do.


---

## 7. Tips for agents (Claude Code etc.)

1. **Always read before you write.** Call `GET /query-ontology` first to discover existing class names, concept IDs, and locale codes.
2. **Use `upsert` for terms and market constraints** — it's idempotent and safe to retry.
3. **Never invent concept IDs.** They're server-generated. Use the ID returned from the create response.
4. **Class names are case-insensitive** when resolving `className`, but consistent casing is recommended.
5. **Bidirectional relationships are handled server-side.** Don't manually insert both directions — that creates duplicates.
6. **Batch up to 50 operations** per request to stay well under your plan's writes/min limit.
7. **Editing market definitions:** call `entity: "market", action: "update"` with `id: "<locale-code>"` and a **partial** `market_definitions` object. The server shallow-merges — keys you omit are left untouched. Read existing definitions via `GET /query-ontology` (`markets[].market_definitions`).

---

## 8. Semantic Web export (RDF/OWL) — Pro & Builder

For AI/RAG pipelines that ground LLMs in a knowledge graph, and for public-sector procurement that requires SKOS-compliant terminology, Black Ice exports the full ontology as **Turtle (`.ttl`)** and **JSON-LD (`.jsonld`)**.

### Vocabulary

Built on standard predicates: `skos:Concept`, `skosxl:Label`, `skosxl:literalForm`, `skos:inScheme`, `skos:definition`, `owl:Class`, `owl:ObjectProperty`, `owl:inverseOf`, `dcterms:title`.

Black Ice-specific predicates live under `https://black-ice.ai/ns#` (prefix `bi:`). See [`/black-ice-ns.ttl`](/black-ice-ns.ttl) for the full vocabulary definition.

| Predicate | Domain | Purpose |
|---|---|---|
| `bi:market` | `skosxl:Label` | Locale this label applies to |
| `bi:isSource` | `skosxl:Label` | True for the source-of-truth term |
| `bi:status` | `skosxl:Label` | `draft` / `pending_approval` / `approved` / `rejected` |
| `bi:availability` | `skosxl:Label` | `available` / `unavailable` / `restricted` |
| `bi:allowedVariant` | `skosxl:Label` | Approved variant spelling/phrasing |
| `bi:forbiddenVariant` | `skosxl:Label` | Banned variant |
| `bi:translationPolicy` | `skos:ConceptScheme` | Class-level policy |
| `bi:schemaVersion` | `owl:Ontology` | Vocabulary version (currently `1.0`) |

### Example (Turtle)

```turtle
@prefix bi: <https://black-ice.ai/ns#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#> .

<https://black-ice.ai/id/concept/feat_00000001>
    a skos:Concept, owl:Class ;
    rdfs:label "phone support" ;
    skos:definition "Support over the phone" ;
    skos:inScheme <https://black-ice.ai/id/class/feature> ;
    skosxl:prefLabel <https://black-ice.ai/id/term/feat_00000001__de-DE> .

<https://black-ice.ai/id/term/feat_00000001__de-DE>
    a skosxl:Label ;
    skosxl:literalForm "Telefonsupport"@de-DE ;
    bi:market <https://black-ice.ai/id/market/de-DE> ;
    bi:status "approved" ;
    bi:isSource "false"^^xsd:boolean ;
    bi:allowedVariant "Telefon-Support"@de-DE ;
    bi:forbiddenVariant "Phone-Support"@de-DE .
```

### Stability contract

`bi:` predicate names are **frozen** for the lifetime of `bi:schemaVersion "1.0"`. Breaking changes bump the version. Downstream consumers can safely pin to the namespace.

### How to use it

- **AI/RAG grounding:** load the JSON-LD into your vector store as structured context, or stream the Turtle into a graph DB (GraphDB, Stardog, Neo4j n10s) and ground retrieval with SPARQL.
- **Public sector / EU institutions:** the export is SKOS-compliant out of the box and resolves against the published [`/black-ice-ns.ttl`](/black-ice-ns.ttl) vocabulary stub — meets the typical procurement-checklist requirement for an open, dereferenceable namespace.
