> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pnbr.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Bridge an external API

> Ingest an OpenAPI spec and record version-stamped matches between its types and your ontology — with coverage and drift detection.

<Note>
  **Beta.** IR mapping is live and in active development. The surface below is
  real and tested; expect it to grow (more source formats, deeper composition).
  Reach it over REST under `/v1/bridges`, or through the `pb.bridge` SDK surface
  shown at the end.
</Note>

Every integration starts with the same unglamorous question: *which of their
types is which of ours?* IR mapping makes the answer a saved, version-stamped
artifact instead of tribal knowledge. You ingest an OpenAPI spec, then record a
**match** for each of their types — what it lines up with in your ontology, and
why. The mapping tells you its own coverage, and when their spec changes, it
tells you exactly which matches broke.

It is the inverse of the [Workbench](/shapes/the-workbench): instead of
authoring a model from scratch, you start from the spec the other side already
publishes.

## The vocabulary

| Noun             | What it is                                                                                                                                                                                            |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Symbol table** | Their types, resolved and version-stamped — every schema in the spec, keyed by path (`#/components/schemas/Customer`).                                                                                |
| **Type mapping** | The one thing you record: how one of their types lines up with one of yours, carrying a relation and your reasoning.                                                                                  |
| **Relation**     | Four values: `identity` (same thing), `subset` (theirs is a narrower case of ours), `related` (connected, not the same), `disjoint` (deliberately not mapped — a recorded decision, not an omission). |
| **Coverage**     | Three-state completeness: mapped, disjoint, unmapped. Unmapped is work remaining; disjoint is work done.                                                                                              |
| **Drift**        | A flag, per match, when either side changes: a type vanished from their spec, or a version hash moved on yours.                                                                                       |

## The loop

A mapping is a persistent session. You open one, ingest a spec into it, record
matches against it, then analyze it. Each step is a REST call under
`/v1/bridges`. Tool errors return in the body (`isError: true`), never as a
bare HTTP failure.

<Steps>
  <Step title="Open a mapping session">
    ```bash theme={null}
    curl -X POST https://pnbr.io/v1/bridges \
      -H "Authorization: Bearer $PENUMBRA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "project_id": "<your-project>", "name": "Commerce API → CRM" }'
    ```

    The response carries a `mapping_id` — the session everything else operates
    on. Mappings persist; you can come back to one.
  </Step>

  <Step title="Ingest the spec">
    Pass a parsed OpenAPI 3.x document. The ingest walks `components.schemas`,
    resolves `$ref` / `allOf` / `oneOf` / `anyOf`, and produces the symbol
    table.

    ```bash theme={null}
    curl -X POST https://pnbr.io/v1/bridges/<mapping-id>/sources \
      -H "Authorization: Bearer $PENUMBRA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "source_ir_id": "commerce-api@v1",
        "openapi": { ...the spec, as JSON... }
      }'
    ```
  </Step>

  <Step title="Record the matches">
    One call per match. The relation and your reasoning are what gets saved:

    ```bash theme={null}
    curl -X POST https://pnbr.io/v1/bridges/<mapping-id>/types \
      -H "Authorization: Bearer $PENUMBRA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "from_path": "#/components/schemas/Customer",
        "to_type_name": "Person",
        "to_shape": "crm",
        "relation": "identity",
        "reasoning": "Same thing — their Customer is our Person in a buying role."
      }'
    ```

    `POST .../properties` then maps field-level pairs under a type mapping
    (with roles like `titular`); `POST .../types/remove` and
    `POST .../properties/remove` reverse decisions.
  </Step>

  <Step title="Analyze">
    ```bash theme={null}
    curl -X POST https://pnbr.io/v1/bridges/<mapping-id>/analyze \
      -H "Authorization: Bearer $PENUMBRA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{}'
    ```

    Returns coverage, property coverage, drift, and a single `healthy` roll-up
    you can gate on.
  </Step>
</Steps>

## A worked mapping

A commerce API mapped onto a CRM ontology — five types, five different
answers:

| Their type | Relation   | Your type           | Why                                       |
| ---------- | ---------- | ------------------- | ----------------------------------------- |
| `Customer` | `identity` | `crm.Person`        | Same thing.                               |
| `Address`  | `identity` | `crm.PostalAddress` | Same thing.                               |
| `Order`    | `identity` | `crm.SalesOrder`    | Same thing.                               |
| `LineItem` | `related`  | `crm.OrderLine`     | Connected, but the granularities differ.  |
| `Payment`  | `disjoint` | —                   | Deliberately out of scope, on the record. |

Analyze then reports `{ mapped: 4, disjoint: 1, unmapped: 3 }` — the three
unmapped types (`CardPayment`, `BankPayment`, `OrderStatus`) are the visible
remaining work, not silent gaps.

## Drift: when their spec changes

Re-ingest version 2 of the spec and analyze again. If `Address` disappeared from
their schema, its match flags `left_dangling`; surviving matches flag
`left_version_changed` so you know to re-confirm them. Your integration's
assumptions just became something a pipeline can check — drift CI for your data
model, the way [`dq.audit`](/advanced/verify-before-you-act) is CI for graph
writes.

## From the SDK

`pb.bridge` wraps the same routes. The fluent session reads like the judgments you
are making:

```ts theme={null}
import { createPenumbra } from "@penumbra-systems/platform";

const pb = createPenumbra({ apiKey: process.env.PENUMBRA_API_KEY! });

const mapping = await pb.bridge.open({ projectId, name: "Commerce API → CRM" });
await mapping.ingestOpenAPI(spec);

await mapping
  .type("#/components/schemas/Customer")
  .mapsTo("crm.Person", { relation: "identity" });

await mapping
  .property("#/components/schemas/Customer", "email")
  .mapsTo("crm.Person.email", { role: "titular" });

// Deliberately out of scope, on the record.
await mapping
  .type("#/components/schemas/Payment")
  .markDisjoint({ reasoning: "Billing lives in the finance system, not the CRM." });

const analysis = await mapping.analyze();
```

`pb.bridge.map(...)` runs the whole thing in one call when you already have the
mappings in hand. See the [`pb.bridge` reference](/sdk/bridge) for every method.

## The tools

Each REST resource has a friendly tool name. `GET /v1/bridges/tools` returns this
table at runtime.

| Tool                          | Route                                             | Safety | What it does                                                                        |
| ----------------------------- | ------------------------------------------------- | ------ | ----------------------------------------------------------------------------------- |
| `openMapping`                 | `POST /v1/bridges`                                | write  | Open (or create) a persistent mapping session.                                      |
| `ingestSource`                | `POST /v1/bridges/:id/sources`                    | write  | Ingest an OpenAPI 3.x document into the symbol table.                               |
| `mapType`                     | `POST /v1/bridges/:id/types`                      | write  | Map one of their types to one of yours.                                             |
| `mapProperty`                 | `POST /v1/bridges/:id/properties`                 | write  | Map a property pair under a type mapping.                                           |
| `unmapType` / `unmapProperty` | `POST .../types/remove` · `.../properties/remove` | write  | Reverse a decision.                                                                 |
| `reviewProposals`             | `POST /v1/bridges/:id/proposals/review`           | read   | Review model-proposed mappings: each comes back accepted or rejected, with reasons. |
| `inspect`                     | `GET /v1/bridges/:id`                             | read   | Read the session: paths, mappings, state.                                           |
| `analyze`                     | `POST /v1/bridges/:id/analyze`                    | read   | Coverage, property coverage, drift, `healthy`.                                      |

`reviewProposals` is the agent-era piece: let a model propose the mappings,
and have each one checked and returned accepted or rejected, with reasons.
Nothing enters the mapping unreviewed.

## v1 boundaries

Honest edges of what ships today: OpenAPI 3.x is the only source format
(GraphQL, protobuf, and SQL schemas are on the roadmap), composition across
mapping chains is single-hop, and contradiction detection is not yet wired.
