Skip to main content

Domain packs and entity-key crosswalks

A domain pack is an optional, platform-authored bundle of data that gets a new data domain to canonical events, labels and shortlists on day one: an event ontology, batch catalogs for common source schemas, derived label recipes, propensity shortlists, band presets and generated-corpus parameters. A pack is never platform behavior — installing one only feeds the same contract, label and mapping machinery you would otherwise fill in by hand, and nothing a pack declares reaches a model until you approve it.

What a pack saves you is setup time. It carries no domain knowledge, no model weights and no validated claims about your data: every label it proposes is an ordinary derived label you can inspect, and every event type it declares waits for your contract approval.

What is in a pack​

A pack is one canonical-JSON document {pack, version, title, provenance, sections}. Every section is a {schema, data} envelope naming the schema that owns its payload:

SectionOwning schemaBecomes
ontologythe pack's ontology definitiondraft entity types + attribute observations + one draft contract
catalogsadapter_catalog.json (batch arm)catalogs your batch sources can map through
label_recipesderived_outcome_rule.jsonderived labels, created through the normal label path
propensity_shortliststhe pack's shortlist definition (2–32 event types)installed data for evaluation configs
band_presetsbanding_policy.jsoninstalled data for banding
corpus_configthe pack's corpus-config definitionstored verbatim (inert generated-corpus parameters)

A section with an unknown name, a schema that is not registered, or any validation failure refuses the whole pack with per-section pointers (/sections/label_recipes/data/1/rule/outcome, …).

Each released version is immutable and fingerprinted: pack_fingerprint = fingerprint(canonical_json(document)). The platform pins every release's fingerprint outside the document and recomputes it on every read; a document that no longer matches its pin fails closed with failed_precondition (pack_fingerprint_mismatch, carrying expected and actual).

Browse and read​

  • GET /v1/domain-packs lists the packaged packs (latest version, its fingerprint and sections) and, for each, the release installed in your project, if any.
  • GET /v1/domain-packs/{pack_id} returns the resolved sections and pack_fingerprint of the latest version (?version=N for an older one).

Ontology entries may carry context_tags (for example, public taxonomy identifiers a source schema references). They are display-only catalog data: they never enter mapping output, features, labels, scoring or any API decision, and only these read endpoints return them.

Install a pack​

Installing requires the admin scope. Send an Idempotency-Key header like every other POST:

{ "version": 1 }

POST /v1/domain-packs/{pack_id}:install validates every section against your project's current state first, then applies everything in one transaction:

  1. registers the ontology's entity types as draft entity types and feeds its event types, attributes and sample values to contract inference as attribute observations with source pack:{pack}@v{version};
  2. creates one draft contract version — pack event types appear in your contracts as draft entries that need your approval;
  3. creates each recipe as a derived label through the normal label path (same validation as POST /v1/labels), with origin: {pack, version, pack_fingerprint, install_id} provenance (read it with GET /v1/labels/{label_id}/provenance; the label itself is unchanged);
  4. records the install (pak_…) with the pinned pack_fingerprint and writes one domain_pack.install audit entry.

Nothing becomes model-visible until you approve the draft contract: draft entity types cannot train, and a dataset build over pack types fails with no_approved_contract (or contract_not_approved when it pins the draft) exactly as it would for any unapproved data.

SituationResult
same version already installed200, the same pak_… record (idempotent)
newer version201, a new record that supersedes the previous one (both are kept)
older version than the installed one400 failed_precondition (domain_pack_downgrade)
not enough room for the pack's new entity types400 failed_precondition (entity_type_limit)
a recipe's name is taken by another label409 already_exists; nothing is installed (a label an earlier release of the same pack created with the identical definition is reused instead)
any section invalid400 invalid_argument with per-section pointers; nothing is installed

GET /v1/domain-pack-installs and GET /v1/domain-pack-installs/{install_id} list and read your install records; a record's detail includes the exact installed document, re-verified against its pinned fingerprint.

The payments-fraud pack​

payments-fraud (version 1) is a pack for payment-risk data. Install it with POST /v1/domain-packs/payments-fraud:install like any other pack; it saves setup time and carries no claims about your data.

PartContent
entity typesaccount, card, customer, device, merchant
event typespayment, refund, chargeback (primary entity card); login, account_change (primary entity account); payout (primary entity merchant)
attributesamount_minor (integer, the amount in the currency's minor units, such as cents), currency (string, the ISO 4217 alphabetic code) and transaction_id (string) on the money events; external_event_id (string, your own event identifier) on every event
draft contractthe ontology above, as draft entries that need your approval
derived labelsaccount_chargeback_90d, card_chargeback_90d, customer_chargeback_90d, device_chargeback_90d and merchant_chargeback_90d: true when the entity's ledger records a chargeback event within 90 days after an event
shortlists and bandscard_activity, account_activity; a fraud_review band preset

Each event names one primary entity; the other participants (the customer, the device, the merchant) are sent as secondary entities of the same event, so each of them gets its own history. The labels are derived only from the chargeback events you send: Tessera does not infer fraud from behavior. transaction_id and external_event_id are identifiers; leave them out of model inputs when you approve the contract.

In the console, Packs under Data lists the packaged packs with their fingerprints and active installs. Open one to read a release as it was packaged, then choose Install. A confirmation shows the release and its fingerprint, and the result links to the draft contract version, which a person must approve before a model can see anything from the pack.

payments-fraud version 2​

Version 2 records every event against the customer as the primary entity, with the card, account, device, merchant, IP address, location and merchant category carried as secondary entities, and widens the event types to the ones payment systems send. Version 1 is unchanged and stays available; an install request names the version it installs ("version": 1 or 2).

PartContent
entity typesaccount, card, customer, device, ip, location, merchant, merchant_category
event typespayment, payment_declined, refund, chargeback, payout, payout_requested, transfer_inbound, transfer_outbound, login, login_failed, account_change, account_opened, beneficiary_added, device_enrolled (primary entity customer)
derived labelsthe same five *_chargeback_90d labels. Label histories are read per primary entity, so on customer-primary data only customer_chargeback_90d sees chargebacks; the account, card, device and merchant labels apply to sources whose events have those entities as primary. Secondary-entity label histories (planned) are not built.
shortlists and bandscustomer_activity; a fraud_review band preset

Map a batch source through a pack catalog​

An installed pack's catalogs are batch catalogs you can select by name instead of pasting the catalog into your mapping:

{
"pack_catalog": { "pack": "fleet-telematics", "catalog": "vendor-a.telemetry" },
"overrides": { "entries": { "DTC": { "enabled": false } } }
}

PUT /v1/sources/{src_id}/mapping resolves the reference against your project's active install of the pack (its stored document re-verified against pack_fingerprint) and stores the ordinary batch-catalog form {catalog, overrides?, assume_timezone?} as an immutable mapping version — your overrides win over the pack catalog, and an optional assume_timezone (see local timestamps with no offset) is carried onto the stored form. The version pins the install's pack_fingerprint — read it with GET /v1/sources/{src_id}/mapping/provenance (add ?version=N for an older version; pack_fingerprint is null for versions that did not come from a pack) — and every sync records the version's fingerprint, so each landed event traces back to the exact pack release. The mapping version resource itself is unchanged. Add "version": N to the reference to assert which release you expect.

SituationResult
the pack is not installed400 failed_precondition (pack_not_installed)
version is not the active install400 failed_precondition (pack_version_not_installed)
the release has no catalog of that name400 invalid_argument at /pack_catalog/catalog
an override breaks the merged catalog400 invalid_argument at /overrides/…
assume_timezone is not an IANA zone of the runtime tz database400 invalid_argument at /assume_timezone

POST /v1/sources/{src_id}/mapping:preview accepts the same reference as a candidate mapping and previews exactly what a sync would land.

Entity-key crosswalks​

Two vendors often key the same real-world entity differently — one sends a VIN, another its own unit id. A crosswalk is an optional, per-source table you supply that maps one source's raw ids to your canonical ids:

{
"version": 1,
"entries": [
{ "entity_type": "vehicle", "raw_id": "UNIT-0042", "canonical_id": "1FTFW1E50NFA00042" }
]
}

Store it with PUT /v1/sources/{src_id}/crosswalk (admin scope) and read the active version with GET — the response includes applied and passed_through counters for the active version.

How it applies:

  • Forward only. A crosswalk rewrites entity.id and each matching secondary_entities[].id (by entity type) of events mapped after the version is stored — on batch syncs and on adapter deliveries (webhooks, and Amplitude and Mixpanel deliveries alike). Events already in your ledger keep their original keys permanently: there is no merging, rewriting or read-time aliasing of history.
  • Unmapped ids pass through untouched; ids are matched exactly as supplied.
  • No # in canonical ids. A raw_id may contain # (rewriting such ids is a good use of a crosswalk), but a new version whose canonical_id contains # is rejected with invalid_argument on /entries/{i}/canonical_id. Entity ids may not contain # (see entity ids); an event whose id still contains # after the crosswalk is refused like any other.
  • Earlier numeric spellings. On a batch column mapping, an id read from a number is now its digits (how identifiers become entity ids). An entry written for the spelling such an id had before (12.0, or a NUMERIC 12.000000000) still applies to it after a miss on 12: a fixed alias of that one id, counted as applied and as crosswalk_legacy_match in the sync's details. An entry for 12 itself wins over one for 12.0, so if both exist with different targets, rows read from numbers now take the 12 entry's target. Your operator can count the entries in an earlier spelling, and such conflicting pairs, per source.
  • Previewed honestly. mapping:preview applies the active version exactly like the live path, without advancing its counters.
  • Pinned. Each sync records the crosswalk version it applied — read it with GET /v1/syncs/{sync_id}/crosswalk (crosswalk_version and crosswalk_fingerprint are null when none applied; the sync resource itself is unchanged). A stored version that fails its fingerprint check is never applied (the sync fails with crosswalk_integrity_failed, an adapter source answers 404 until fixed).

Bounds, all refused with 400 invalid_argument and a pointer:

  • at most 100,000 entries per version;
  • each (entity_type, raw_id) at most once;
  • single hop: no canonical_id may also appear as a raw_id in the same version.

Versions are immutable and only move forward: PUT a higher version to change the table (re-sending the active version unchanged is a no-op). Other stream sources — which never map through a catalog or a column mapping — refuse a crosswalk with failed_precondition (crosswalk_unsupported_source).

Tessera never infers, suggests or merges identities: a crosswalk only does what its entries say, and sources without one keep one raw id per entity.

Troubleshooting​

SymptomCauseFix
install answers already_existsa recipe's label name is already used in this environmentrename or archive your label, or keep it and skip the pack's recipe
install answers pack_fingerprint_mismatchthe packaged release does not match its pinned fingerprintreport it; the release is refused until fixed
pack event types missing from modelsthe draft contract is not approved yetreview and approve it under contracts
events from two vendors still splitthe crosswalk was stored after those events were mappedexpected — crosswalks apply forward only
a sync reports crosswalk_legacy_matchan entry is written for an id's earlier spelling (12.0) and applied to the id now read as 12expected; add an entry for 12 in the next version to retire the old spelling
a sync fails with crosswalk_integrity_failedthe stored crosswalk version no longer matches its fingerprintPUT a new version

Map raw JSON through a preset​

A raw-source preset is a pinned, platform-authored mapping from one named JSON record shape to canonical events. Bind a source to one with PUT /v1/sources/{src_id}/raw-preset (admin scope) and read the binding with GET:

{ "preset": "mcc-canonical-event", "version": 1 }

The source's raw receipts (POST /v1/sources/{src_id}/raw-events) are then mapped by that preset instead of being profiled. The binding pins the release fingerprint, which is verified before every use.

  • Either a preset or an inferred binding. A source with an accepted inferred binding answers failed_precondition (source_has_binding), and a preset-bound source has no inferred proposal to approve (source_has_raw_preset). Use a new source to switch.
  • The contract still decides. A mapped event needs its event type included in your approved contract, like any other event; install a pack and approve its draft contract first.
  • Nothing is guessed. A record the preset cannot map (another record shape, no entities, an invalid amount or time) is quarantined with its retained raw record under the ordinary receipt rules, is re-evaluated later (for example after you approve its event type), and is cleared when the retention window ends. The onboarding exception message names the typed reason, never a value from the record.
  • Collision-safe event ids. The preset derives each canonical event_id from the source's own event id (a namespaced SHA-256, 64 characters) and keeps the original as the external_event_id attribute, so look-ups by your own id work and a re-sent event is stored once.

Reading the results for a preset source:

  • Quarantine messages. A quarantined record's receipt entry reads "Record is quarantined because it could not be normalized with the approved binding"; for a preset source "binding" means the bound preset. The onboarding exception names the reason, for example "A raw record remains quarantined: no_entities" (an empty or absent entity list), unsupported_source_shape, invalid_entity, invalid_event_type, invalid_attribute_value or a timestamp reason.
  • Semantic work is not_tracked. Preset-mapped events are not enriched, so the processing counts report their semantic work as not_tracked, which is expected and not a fault.
  • One way to interpret a source. A source with a preset, an inferred binding or a column mapping refuses the other two: PUT /v1/sources/{src_id}/mapping answers failed_precondition (source_has_raw_preset or source_has_binding).
  • Erasure. A record of an erased entity that arrives later is accepted and erased at once, like every other intake: nothing of it is stored or published, its receipt entry is redacted and the batch's retained records are dropped.
  • Console. The source page shows which preset a source uses and does not change it. Setting a preset is an API call.

Map your event types onto the pack​

A preset can carry an event_type.map: exact source event type to canonical event type. Version 2 of mcc-canonical-event maps the 14 event types of that source onto the version 2 pack (for example card.authorization.approved to payment and chargeback.filed to chargeback), so the pack's labels see them. A listed type is replaced and the original is kept in context.source_event_type; any other type is normalized as before and, if your contract does not include it, waits at the contract gate.

Moving a source from version 1 to version 2 never re-keys events: the event id derives from the preset name and your own event id, so an event you send again is stored once.

  • The first stored copy wins. An event keeps the type it was stored with. Events ingested under version 1 keep their version 1 types, and sending them again after you move to version 2 does not retype them. Bind version 2 before the first ingest, or use a new source or project to retype history. The pack labels apply to events ingested after the move.
  • A request without a version. PUT /v1/sources/{src_id}/raw-preset with {"preset": …} and no version binds the latest release of that preset. A source already bound to the same preset keeps its current version, so repeating the request never changes it; name the version to move it. A source bound to a different preset is rebound to the latest release of the one you name.
  • Matching. Map keys are matched against the source event type exactly as the source sends it. Your document is stored in Unicode NFC form, so a source that sends decomposed text does not match an NFC key. An oversized map is one bound_exceeded error (at most 64 entries, 32 in your own presets).

Write your own preset​

If your JSON does not match a packaged preset, author one in the same format. A preset is data, never code: a closed document that names where in your record each canonical field lives.

{
"format": "tessera.raw-source-preset.v1",
"preset": "orders-json",
"version": 1,
"title": "Orders JSON",
"provenance": { "authored_by": "customer" },
"source": { "shape": { "pointer": "/kind", "equals": "order.v1" } },
"event_id": {
"pointer": "/order_ref",
"recipe": "sha256_namespaced_v1",
"retain_as": "external_event_id"
},
"event_type": { "pointer": "/event", "normalize": "dotted_snake_v1" },
"timestamp": { "pointer": "/placed", "format": "rfc3339" },
"entities": {
"pointer": "/parties",
"type_pointer": "/type",
"id_pointer": "/id",
"role_pointer": "/role",
"primary_role": "buyer",
"primary_fallback": "first_entry"
},
"attributes": {
"amount_minor": { "pointer": "/total/cents", "convert": "integer_string" },
"currency": { "pointer": "/total/ccy", "convert": "string", "pattern": "^[A-Z]{3}$" }
}
}

Check it before you save it. POST /v1/sources/{src_id}/raw-preset:validate (admin scope) takes {"document": …, "sample_record": …} and answers with typed errors, each a JSON pointer into your document (at most 512 characters), a reason code and a message (at most 1024), and, for a valid document, the canonical event your sample record would become. It also reports what the save would refuse on this source, such as a source that already has an inferred binding or is a manual source. A message never repeats a value from your sample record. A document that cannot be read at all (a number that is not finite, an unpaired surrogate, or nesting deeper than 8 levels) is one error at the root. It stores nothing. Store the preset with PUT /v1/sources/{src_id}/raw-preset and {"document": …}; read it back with GET /v1/sources/{src_id}/raw-preset/document.

What the format allows:

  • Closed and bounded. Unknown members are refused. A document is at most 32 KiB with at most 128 attributes, and every path is a literal JSON Pointer at most 8 levels deep. 128 attributes with long names fit: for example 128 keys of 64 characters read from /attributes/<key>, each with a value shape, take about 26 KB. The only conversions are copying a value, copying a string, and reading a whole number written as a string.
  • Event-type map. An optional event_type.map replaces exact source event types with canonical ones (at most 32 entries); any other type is normalized as usual.
  • Value shapes, not regular expressions. An attribute pattern is a short anchored shape such as ^[A-Z]{3}$ or ^ORD-[0-9]{4,8}$: character classes, literals and bounded counts. Groups, alternation and open-ended repeats are refused, so no value can make a check slow. . matches any character except a line feed and d matches only 0-9, as in a regular expression.
  • Versions. Each source keeps its presets as immutable, fingerprinted versions. A new version must have a higher version than the stored one, the preset name never changes for a source, and a source keeps at most 100 versions. A version is a whole number from 1 to 2147483647. A packaged preset's name is not available as the first version of a name.
  • Stable event ids. Your presets derive each canonical event_id from custom: plus the preset name and your own event id, not from the version. Editing a preset therefore never re-keys events: an event you send again after an edit is stored once. The original id stays in the attribute named by retain_as.
  • Same rules as packaged presets. Everything under "Map raw JSON through a preset" applies: one way to interpret a source, the approved contract decides which event types are accepted, unmappable records are quarantined with their retained record, and records of erased entities are erased at intake.