Skip to main content
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.
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: instead of authoring a model from scratch, you start from the spec the other side already publishes.

The vocabulary

NounWhat it is
Symbol tableTheir types, resolved and version-stamped — every schema in the spec, keyed by path (#/components/schemas/Customer).
Type mappingThe one thing you record: how one of their types lines up with one of yours, carrying a relation and your reasoning.
RelationFour 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).
CoverageThree-state completeness: mapped, disjoint, unmapped. Unmapped is work remaining; disjoint is work done.
DriftA 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.
1

Open a mapping session

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.
2

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.
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... }
  }'
3

Record the matches

One call per match. The relation and your reasoning are what gets saved:
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.
4

Analyze

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.

A worked mapping

A commerce API mapped onto a CRM ontology — five types, five different answers:
Their typeRelationYour typeWhy
Customeridentitycrm.PersonSame thing.
Addressidentitycrm.PostalAddressSame thing.
Orderidentitycrm.SalesOrderSame thing.
LineItemrelatedcrm.OrderLineConnected, but the granularities differ.
PaymentdisjointDeliberately 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 is CI for graph writes.

From the SDK

pb.bridge wraps the same routes. The fluent session reads like the judgments you are making:
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 for every method.

The tools

Each REST resource has a friendly tool name. GET /v1/bridges/tools returns this table at runtime.
ToolRouteSafetyWhat it does
openMappingPOST /v1/bridgeswriteOpen (or create) a persistent mapping session.
ingestSourcePOST /v1/bridges/:id/sourceswriteIngest an OpenAPI 3.x document into the symbol table.
mapTypePOST /v1/bridges/:id/typeswriteMap one of their types to one of yours.
mapPropertyPOST /v1/bridges/:id/propertieswriteMap a property pair under a type mapping.
unmapType / unmapPropertyPOST .../types/remove · .../properties/removewriteReverse a decision.
reviewProposalsPOST /v1/bridges/:id/proposals/reviewreadReview model-proposed mappings: each comes back accepted or rejected, with reasons.
inspectGET /v1/bridges/:idreadRead the session: paths, mappings, state.
analyzePOST /v1/bridges/:id/analyzereadCoverage, 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.