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:
| Section | Owning schema | Becomes |
|---|---|---|
ontology | the pack's ontology definition | draft entity types + attribute observations + one draft contract |
catalogs | adapter_catalog.json (batch arm) | catalogs your batch sources can map through |
label_recipes | derived_outcome_rule.json | derived labels, created through the normal label path |
propensity_shortlists | the pack's shortlist definition (2–32 event types) | installed data for evaluation configs |
band_presets | banding_policy.json | installed data for banding |
corpus_config | the pack's corpus-config definition | stored 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-packslists 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 andpack_fingerprintof the latest version (?version=Nfor 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:
- 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}; - creates one draft contract version — pack event types appear in your contracts as draft entries that need your approval;
- creates each recipe as a derived label through the normal label path
(same validation as
POST /v1/labels), withorigin: {pack, version, pack_fingerprint, install_id}provenance (read it withGET /v1/labels/{label_id}/provenance; the label itself is unchanged); - records the install (
pak_…) with the pinnedpack_fingerprintand writes onedomain_pack.installaudit 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.
| Situation | Result |
|---|---|
| same version already installed | 200, the same pak_… record (idempotent) |
| newer version | 201, a new record that supersedes the previous one (both are kept) |
| older version than the installed one | 400 failed_precondition (domain_pack_downgrade) |
| not enough room for the pack's new entity types | 400 failed_precondition (entity_type_limit) |
| a recipe's name is taken by another label | 409 already_exists; nothing is installed (a label an earlier release of the same pack created with the identical definition is reused instead) |
| any section invalid | 400 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.
| Part | Content |
|---|---|
| entity types | account, card, customer, device, merchant |
| event types | payment, refund, chargeback (primary entity card); login, account_change (primary entity account); payout (primary entity merchant) |
| attributes | amount_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 contract | the ontology above, as draft entries that need your approval |
| derived labels | account_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 bands | card_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).
| Part | Content |
|---|---|
| entity types | account, card, customer, device, ip, location, merchant, merchant_category |
| event types | payment, 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 labels | the 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 bands | customer_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.
| Situation | Result |
|---|---|
| the pack is not installed | 400 failed_precondition (pack_not_installed) |
version is not the active install | 400 failed_precondition (pack_version_not_installed) |
| the release has no catalog of that name | 400 invalid_argument at /pack_catalog/catalog |
| an override breaks the merged catalog | 400 invalid_argument at /overrides/… |
assume_timezone is not an IANA zone of the runtime tz database | 400 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.idand each matchingsecondary_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. Araw_idmay contain#(rewriting such ids is a good use of a crosswalk), but a new version whosecanonical_idcontains#is rejected withinvalid_argumenton/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 aNUMERIC12.000000000) still applies to it after a miss on12: a fixed alias of that one id, counted as applied and ascrosswalk_legacy_matchin the sync'sdetails. An entry for12itself wins over one for12.0, so if both exist with different targets, rows read from numbers now take the12entry's target. Your operator can count the entries in an earlier spelling, and such conflicting pairs, per source. - Previewed honestly.
mapping:previewapplies 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_versionandcrosswalk_fingerprintarenullwhen none applied; the sync resource itself is unchanged). A stored version that fails its fingerprint check is never applied (the sync fails withcrosswalk_integrity_failed, an adapter source answers404until 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_idmay also appear as araw_idin 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
| Symptom | Cause | Fix |
|---|---|---|
install answers already_exists | a recipe's label name is already used in this environment | rename or archive your label, or keep it and skip the pack's recipe |
install answers pack_fingerprint_mismatch | the packaged release does not match its pinned fingerprint | report it; the release is refused until fixed |
| pack event types missing from models | the draft contract is not approved yet | review and approve it under contracts |
| events from two vendors still split | the crosswalk was stored after those events were mapped | expected — crosswalks apply forward only |
a sync reports crosswalk_legacy_match | an entry is written for an id's earlier spelling (12.0) and applied to the id now read as 12 | expected; add an entry for 12 in the next version to retire the old spelling |
a sync fails with crosswalk_integrity_failed | the stored crosswalk version no longer matches its fingerprint | PUT 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_idfrom the source's own event id (a namespaced SHA-256, 64 characters) and keeps the original as theexternal_event_idattribute, 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_valueor a timestamp reason. - Semantic work is
not_tracked. Preset-mapped events are not enriched, so the processing counts report their semantic work asnot_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}/mappinganswersfailed_precondition(source_has_raw_presetorsource_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-presetwith{"preset": …}and noversionbinds 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_exceedederror (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.mapreplaces 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
patternis 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 anddmatches only0-9, as in a regular expression. - Versions. Each source keeps its presets as immutable, fingerprinted
versions. A new version must have a higher
versionthan the stored one, thepresetname 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_idfromcustom: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 byretain_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.