Introduction
Most data pipeline failures are schema failures wearing a costume.
The parser is fine. The infrastructure is fine. The problem is that somebody decided, months earlier, that a product has a price — and every downstream model, dashboard, and decision inherited that decision without ever examining it.
Schema is not plumbing. On a platform as structurally rich as Flipkart, the schema is the product, and getting it right at the outset is the difference between a dataset that answers questions and one that quietly generates confident wrong answers.
This is a field-by-field reference for a Flipkart product data API: the design principles, the full structure, the JSON, and the decisions that are easy to get wrong and expensive to reverse.
Five Design Principles
1. Model the platform, not the page. The page is a rendering. The platform has price tiers, seller arrays, variant matrices, offer structures, and location resolution. Model those. If your schema mirrors the HTML, it will break every time the HTML changes — and it will encode the page's presentational choices as if they were facts.
2. Nest what nests. Sellers, variants, and offers are one-to-many relationships. Flattening them into a wide row destroys information that cannot be reconstructed. Nest them, and flatten later for analysts who want a spreadsheet.
3. Compute what is derivable; store what is not. Effective price is derivable from price plus offers, so compute it — but store the inputs, because your weights will change and you will want to recompute history. Never store only the derived value.
4. Timestamp everything. Half the useful metrics in marketplace analytics are durations, not states. "Out of stock" is worth little. "Out of stock for 11 days" is an escalation. Without captured_at on every record, you have states and no durations.
5. Never overwrite a price tier. Listed price, Plus price, deal price, and effective price are separate fields. The moment you collapse them into price, you have thrown away the layer your competitors are actually competing on — and you cannot get it back.
The Schema, Group by Group
Identity
| Field |
Type |
Notes |
| product_id |
string |
Platform identifier |
| title |
string |
|
| brand |
string |
|
| category_path |
array |
Full breadcrumb, not a single string — you will want to group by level |
| url |
string |
|
| captured_at |
ISO 8601 |
Mandatory. With timezone offset. |
| schema_version |
string |
See versioning, below |
category_path as an array rather than a string is a small decision that pays for itself the first time someone asks for a category-level rollup. "Mobiles > Smartphones > Android" is a display string. ["Mobiles", "Smartphones", "Android"] is data.
Pricing — all tiers, never collapsed
| Field |
Type |
Notes |
| mrp |
number |
Reference only. Discounts against MRP are the least informative figure available. |
| listed_price |
number |
What a non-member sees |
| plus_exclusive_price |
number | null |
Null when no Plus tier is offered — null, not equal to listed price |
| deal_price |
number | null |
Sale-event price |
| effective_price_best_case |
number |
Computed: optimal legal offer combination |
| effective_price_realised |
number | null |
Computed with the client's realisation weights |
| currency |
string |
|
The plus_exclusive_price: null versus plus_exclusive_price == listed_price distinction matters more than it looks. "No Plus tier offered" and "Plus tier equals standard tier" are different facts about the seller's strategy, and collapsing them destroys a signal.
Offers — structured, never a display string
"bank_offers": [
{
"bank": "Bank A",
"card_type": "credit",
"offer_type": "instant_discount",
"pct": 10,
"flat_value": null,
"max_discount": 2000,
"min_txn": 10000,
"computed_discount": 2000,
"cap_binding": true
}
]
This is the single most important structural decision in the whole schema.
Most pipelines store the offer as "10% instant discount on Bank A credit cards". That is a string. You cannot compute against it, you cannot detect that the cap is binding, and you cannot compare it to a competitor's offer without a human reading both.
Store it as fields, compute computed_discount cap-aware, and flag cap_binding. A 10 percent discount capped at 2,000 delivers 10 percent on a 20,000-rupee item and 4 percent on a 50,000-rupee item. If you do not flag the cap, your effective price is wrong, and it is most wrong on your most valuable SKUs.
Alongside:
| Field |
Type |
| best_bank_discount |
number (cap-aware) |
| offers_are_stackable |
boolean | null |
| no_cost_emi_available |
boolean |
| emi_tenures_months |
array |
| exchange_offer_max |
number | null |
| supercoins_earnable |
number |
| supercoin_earn_rate |
number (coins per unit currency) |
| category_baseline_earn_rate |
number |
supercoin_earn_rate normalised to coins-per-rupee is the comparable metric. Raw coin counts compare price levels, not generosity, and every analysis built on them is measuring the wrong thing.
Sellers — an array, always
"all_sellers": [
{
"seller_name": "AuthorisedPartner A",
"seller_id": "SELLERA123",
"seller_rating": 4.6,
"price": 24999,
"plus_price": 23749,
"is_f_assured": true,
"is_default_seller": true,
"return_policy_days": 7
},
{
"seller_name": "Unknown Seller 3",
"seller_id": "SELLERX987",
"seller_rating": 3.8,
"price": 23149,
"plus_price": null,
"is_f_assured": false,
"is_default_seller": false,
"return_policy_days": 0
}
]
is_f_assured lives here, inside the seller object. It is a seller-SKU attribute, not a product attribute. The same product can be F-Assured from one seller and not another, and a product-level boolean collapses that distribution into whichever value the default seller happened to have. That field is not imprecise — it is arbitrary.
is_default_seller is the Buy Box equivalent, and it is the field brand-protection queues are built on.
seller_id matters more than it looks. Seller names vary, get edited, and get re-registered. A stable identifier is what lets you connect the same actor across multiple listings — which is the hard part of unauthorised-seller detection and the part that string-matching on names cannot do.
Variants — an array, with structured attributes
"variants": [
{
"variant_id": "V001",
"variant_label": "8 GB / 256 GB / Midnight Blue",
"attributes": {"ram": "8GB", "storage": "256GB", "colour": "Midnight Blue"},
"variant_price": 28999,
"variant_plus_price": 27999,
"in_stock": false,
"stock_signal": "out_of_stock",
"is_default_variant": false,
"variant_seller_id": "SELLERA123"
}
]
attributes as a structured object rather than a label string is what makes the matrix groupable, filterable, and matchable against a competitor's matrix. "8 GB / 256 GB" is a label. {ram: "8GB", storage: "256GB"} is data.
variant_seller_id catches a fact that surprises most teams: different sellers can hold different variants of the same product. The seller grid and the variant grid interact, and neither is legible without the other.
Location
| Field |
Type |
| pincode |
string |
| city, state, region_tier |
string |
| deliverable |
boolean |
| delivery_eta_days |
number | null |
deliverable: false is a finding, not a null. Report it as coverage: this SKU is deliverable to 78 percent of our panel, and trend that number.
Sale event
| Field |
Type |
| bbd_active |
boolean |
| deal_type |
enum — flash, lightning, sustained, early_access |
| deal_start, deal_end |
ISO 8601 |
deal_type is what distinguishes a competitor running a two-hour burst from a competitor resetting their price for the event. Same price. Entirely different competitive meaning.
Quick commerce
| Field |
Type |
| flipkart_quick_eligible |
boolean |
| quick_eta_minutes |
number | null |
| dark_store_available |
boolean | null |
| availability_reason |
enum — not_assorted, out_of_stock, no_quick_coverage |
| quick_price |
number | null |
| category_depth |
number |
availability_reason is the field that separates three completely different problems owned by three different teams. Collapsing them into a single "unavailable" state is the most common analytical error in q-commerce reporting.
category_depth is the denominator for share of shelf. Without it, share of shelf is a number divided by nothing.
The Full Record
{
"schema_version": "2.1",
"product_id": "MOBH8G7ZQJ4XYZAB",
"title": "Smartphone Model X Pro",
"brand": "BrandX",
"category_path": ["Mobiles", "Smartphones", "Android"],
"captured_at": "2026-07-14T09:12:04+05:30",
"pincode": "560001",
"city": "Bengaluru",
"region_tier": "metro",
"deliverable": true,
"delivery_eta_days": 2,
"mrp": 31999,
"listed_price": 24999,
"plus_exclusive_price": 23749,
"deal_price": null,
"effective_price_best_case": 22459,
"effective_price_realised": 23158,
"bank_offers": [
{"bank": "Bank A", "card_type": "credit", "offer_type": "instant_discount",
"pct": 10, "max_discount": 1500, "min_txn": 10000,
"computed_discount": 1500, "cap_binding": true}
],
"best_bank_discount": 1500,
"offers_are_stackable": false,
"no_cost_emi_available": true,
"emi_tenures_months": [3, 6, 9],
"exchange_offer_max": 15000,
"supercoins_earnable": 240,
"supercoin_earn_rate": 0.0096,
"category_baseline_earn_rate": 0.0096,
"bbd_active": false,
"flipkart_quick_eligible": false,
"all_sellers": [
{"seller_id": "SELLERA123", "seller_name": "AuthorisedPartner A",
"seller_rating": 4.6, "price": 24999, "is_f_assured": true, "is_default_seller": true},
{"seller_id": "SELLERX987", "seller_name": "Unknown Seller 3",
"seller_rating": 3.8, "price": 23149, "is_f_assured": false, "is_default_seller": false}
],
"variants": [
{"variant_id": "V001", "variant_label": "6 GB / 128 GB",
"attributes": {"ram": "6GB", "storage": "128GB"},
"variant_price": 24999, "in_stock": true, "is_default_variant": true},
{"variant_id": "V002", "variant_label": "8 GB / 256 GB",
"attributes": {"ram": "8GB", "storage": "256GB"},
"variant_price": 28999, "in_stock": false, "stock_signal": "out_of_stock"}
]
}
Flattening for Analysts — and What You Lose
Analysts want a wide table. Give them one, but generate it from the nested record rather than instead of it, and be explicit about the loss.
| Flattening choice |
What it costs you |
| One row per product |
Everything. The seller array and variant matrix vanish. |
| One row per product-variant |
Seller detail collapses to the default seller |
| One row per product-seller |
Variant detail collapses to the default variant |
| One row per product-variant-seller-pincode |
Nothing — but the row count multiplies fast |
The last is correct and expensive. Most teams land on product-variant with a separate seller table, joined on product_id. That is a reasonable compromise, and it is a compromise — worth knowing you have made it.
Schema Versioning, Rate Limits, and Idempotency
Version the schema. schema_version on every record. When a field's meaning changes — and it will — you need to know which records were written under which contract. This is dull and it is the difference between a dataset you can trust in eighteen months and one you cannot.
Idempotency. (product_id, pincode, seller_id, variant_id, captured_at) is a natural key. Deduplicate on it. Re-delivered records happen; double-counted stockout durations should not.
Pagination and rate limits. Cursor-based, not offset-based — the underlying set changes between pages, and offset pagination on a moving set silently skips and duplicates records.
Nulls. Never coerce a null to a zero. plus_exclusive_price: 0 is a catastrophic bug that looks like a bargain, and it will propagate into an effective-price calculation and out into a pricing decision before anyone catches it.
Frequently Asked Questions
Can we get a subset of the schema?
Yes. Most engagements start with a field group or two and expand.
How do you version schema changes?
schema_version on every record, with changes communicated ahead of deployment.
Do you deliver flattened CSV?
Yes — at the grain you specify, with the trade-offs above made explicit.
Can you push directly to our warehouse?
S3, GCS, Snowflake, and BigQuery.
What if we only want price?
Then a generic scraper will do, and we will tell you so. The schema above exists because decisions need it — not for its own sake.
The Schema Is the Decision
Every field above exists because a real decision depends on it. The seller array exists because a legal team needs timestamped evidence. cap_binding exists because a pricing team was about to cut a price it did not need to cut. availability_reason exists because "unavailable" was sending a trade team to argue with the wrong party.
Get the schema right and the analysis becomes easy. Get it wrong, and no amount of downstream sophistication will recover what you never captured.
Product Data Scrape delivers a Flipkart product data API on the schema above — nested seller arrays, full variant matrices, structured offers with cap-aware computation, all price tiers preserved, pincode resolution, Quick availability, sale-event flags, and versioned, timestamped records.
Ask us for the full schema reference and a sample payload on your own category.
Product Data Scrape — turning marketplace complexity into decision-ready data.