Skip to main content
The unified model is what makes the waterfall possible. A cascade can only work if its steps are interchangeable, and steps are only interchangeable if every source’s output lands in the same shape. So this is not a convenience layer. It is the contract that lets a different source answer tomorrow without your code noticing: a field means the same thing everywhere, and it keeps meaning that as sources are re-tuned and channels are added.

Four guarantees

Stable identity

An entity keeps its Unif ID across renames, handle changes and re-listings. Store the ID, not the URL.

One name per concept

revenue is revenue on every entity and every channel. There is no gmv on one and sales_amount on another.

One unit per field

Money is in the currency you requested. Rates are ratios, never percentages. Timestamps are RFC 3339 in UTC.

Declared coverage

When a field is not available for a channel and market, it is null and named in meta.missing_fields. It is never quietly zero.

The shape of every record

Each entity response has the same four layers, so code you write against one entity transfers to the others.
1

Identity

id, object, channel, market. Stable, and safe to store.
2

Attributes

Facts about the entity that do not depend on a period — name, category, price, follower count.
3

Metrics

Everything measured over a window, always under metrics. If a number moves with the period you asked for, it lives here.
4

Context

period, currency and meta. The record tells you what it is, rather than making you remember what you asked for.
The attributes-versus-metrics split is the useful line to hold in your own schema too. Attributes belong on a dimension table; metrics belong on a fact table keyed by entity and date.

What normalization does not do

Being explicit about the limits is what keeps the guarantees credible.
If a channel does not publish a metric for a market, Unif returns null and lists the field in meta.missing_fields. Modelling a plausible value would make the number look like the ones next to it, which is worse than an absent value.
Attribution windows and return handling genuinely differ between channels. Unif documents each definition in the metrics reference and reports the channel on every record. Cross-channel totals are your call to make, with that context in hand.
If a channel only reports weekly buckets for a market, a request for three arbitrary days comes back with period.adjusted: true and the window that was actually measured. Silent interpolation would be a lie with a plausible shape.
meta.refreshed_at is when Unif last refreshed the record, not when you asked. Combine it with typical_lag_hours from GET /coverage to know how current a number really is.

Adding a channel changes nothing you wrote

This is the property the model exists to deliver. When a channel goes live:
  • Your stored Unif IDs stay valid, because IDs are namespaced per entity and never recycled.
  • Your filters keep working, because filter names are model-level, not channel-level.
  • Your queries do not automatically widen. A search without channel spans the channels your workspace has enabled, so turning one on is a deliberate act.
  • New channel-specific nuance shows up in coverage, not in new field names.
The one thing you should re-read after enabling a channel is the coverage report. A filter that is fully supported on one channel may be partial on another, which changes what your result set means.