Unified data model

The ten tables every connector maps onto: fields, types, status enumerations, identity rules, PII handling and the raw column policy, with carrier and ERP mappings.

Every platform page in this section ends with a mapping table onto the ten tables below. This page is the definition those tables point at. It is version 1 of the model, dated 2026-09-22, and it is deliberately narrow: it holds what 221 platforms can actually supply, not everything a single rich platform could.

Read this with Authentication patterns for how credentials are stored, and with Listing and inventory updates for the write direction.

Design rules

  1. Channel data is not authoritative, it is observed. Every table carries the channel it came from and the account it came from. Nothing is merged across channels except through an explicit identity rule below.
  2. Every row keeps its source. The raw column holds the original response fragment. See the raw column policy.
  3. Absent is not zero. A platform that does not report a field leaves it null. Zero means the platform said zero. This distinction matters most for stock.
  4. Money is stored in minor units as an integer, with a currency code. No floats. Several platforms return money as a string, and two return it inconsistently typed within one response.

orders

The header of a customer order on a sales channel.

Order status is the enumeration created, confirmed, packed, shipped, delivered, cancelled, returned. Platform statuses are mapped onto these, never stored raw in this column. The mapping is lossy on purpose. Where a platform has a status with no equivalent, the platform page says so and the detail stays in raw.

Warning

Spelling differs between platforms and sometimes within one platform. Talabat sends CANCELED inbound and expects CANCELLED outbound. Normalise on write, never compare against a platform's own string.

order_items

Line level status is not decorative. On Amazon, Flipkart and most Indian marketplaces a single order can have one line shipped and another cancelled, and the header status alone will mislead any revenue calculation.

products

Our own catalogue, independent of any channel.

listings

The join between a product and a channel. One row per SKU per channel account.

stock here is the channel's belief, which is not our truth. Our truth is in inventory. The difference between the two is the reconciliation signal described in Listing and inventory updates.

inventory

Note

Several platforms have no location dimension at all. Snapdeal holds one inventory pool per seller. Microsoft Dynamics Business Central's v2.0 API exposes no per location stock. Optiply's published model carries no warehouse identifier. For these, write a single synthetic location per channel account and record why on the platform page rather than inventing a warehouse code.

shipments

Warning

shipping_cost is not comparable across carriers. FedEx, UPS, DHL and Aramex all report the rated estimate at ship time, not the invoiced amount, and none of the four has a COD remittance API. The invoiced figure arrives later in a settlement file or not at all. Never reconcile margin against this column alone.

Two platforms cannot fill events at all: Odoo exposes only a tracking reference with an add on, and ERPNext has a free text field. Neither has carrier scan events, so delivered_at and ndr_reason stay null when the ERP is the only source.

returns

AJIO returns do not synchronise by any API and must be read from the panel. Amazon's newer Fulfillment Outbound version has no create return operation, so returns still require the legacy version.

settlements

fees[].type is deliberately not an enumeration. Every marketplace names its deductions differently and the names change; normalising them early destroys the ability to reconcile against the platform's own statement.

noon and Talabat have no settlement API despite otherwise complete platforms. For those, settlements are a report download.

customers

Everything in this table is PII. See the PII section.

locations

Identity rules

These are the only places rows are joined across sources.

  • An order is unique on channel + channel_account_id + channel_order_id. Never on the channel order id alone: order numbers collide across sellers, and on several platforms they restart per financial year.
  • A listing is unique on channel + channel_account_id + channel_listing_id. A SKU may have several listings on one channel, including duplicates the seller did not intend.
  • A shipment is unique on carrier + awb. Air waybills are reused by some Indian carriers after a long enough interval, so include shipped_at in any historical query.
  • A settlement line joins to an order through channel_order_id, not through our order_id, because settlements frequently arrive for orders that were never ingested (cancelled before first sync, or older than the connector).
  • A product joins to a listing through sku, which we own. If a channel's seller SKU does not match ours, the mapping lives in listings, never by rewriting the channel's data.

PII fields and masking

PII fields are customers.name, customers.email, customers.phone, customers.addresses and orders.ship_to_address.

Rules:

  • Store them encrypted at rest, in a schema with separate grants from the rest of the model. Analytics should be able to read orders without reading customers.
  • Many channels already mask. Amazon requires a restricted data token for buyer information and returns anonymised addresses otherwise. Several Indian marketplaces relay phone numbers. Record what you received, never attempt to unmask.
  • A vendor purchase order model has no customer at all. ship_to_address on those rows is a platform warehouse, which is not PII. Do not encrypt it into uselessness: it is needed for logistics planning.
  • Apply the platform's retention limit. Amazon requires buyer data be deleted within 30 days of the order reaching a terminal state unless a legal basis applies.

The raw column policy

Every table has raw jsonb. It holds the platform's own response for that entity, trimmed to the object in question, not the whole page of results.

  • Keep it for 90 days at full fidelity, then drop all but identifiers and money fields. It exists to debug a mapping, not to be a second database.
  • Never query it in production paths. If a field is needed often enough to query, promote it to a column and record that on the platform page.
  • Redact credentials before storing. Eleven platforms repeat the account password in every request body, and a naive raw capture of a request will store it.

How carriers and ERPs map

The model is shaped around sales channels. The other two families fit as follows.

Carriers populate shipments and nothing else. They have no listings, no stock and no catalogue, which is why every carrier page in this section states "not applicable, this is a carrier" under the write heading and documents shipment create and cancel as the write path instead. A carrier row joins to an order through shipments.order_id, which we set at creation time, because carriers do not know our order identifier unless we send it as a reference.

ERP, accounting and POS systems sit on the other side. They consume orders and produce settlements. For them "write" means pushing sales orders, invoices and stock adjustments into the system, so the write column on those pages describes an inbound direction. They generally supply inventory well, products adequately and shipments badly or not at all.

BI and analytics platforms are peers rather than sources. MapleMonk consumes the same connectors we do. Reading from them means reading their warehouse, which is a different integration shape and is noted on each page.