Build a single-page Orders Manager application powered by the DataDoe REST API. Use when the user asks to create, scaffold, or build an orders manager, orders dashboard, orders table, or order management app using DataDoe.
Low-level agent (Cursor, Claude Code, Codex, Aider, etc.): follow the Default Tech Stack below.
High-level vibe coding assistant (Lovable, Base44, Bolt, v0, etc.): ignore the tech stack section entirely; use whatever the platform provides. Follow only the App Features, DataDoe API Integration, and Implementation Rules sections.
App Features
1. Seller/Vendor Picker
On load, fetch connected sellers and vendors from the API.
Show a dropdown to select one. Orders cannot be loaded without a selection.
Only sellers with sellerCentralConnection !== null are valid (Order Line Items source is SELLER_CENTRAL).
2. Orders Table
Display orders in a table with expandable nested rows for line items.
Parent row: amazon_order_id, order_date, order_status, fulfillment_channel, total item count, total price.
Show a spinner/skeleton during export creation and polling.
Show user-friendly error messages for API failures (network errors, 429 rate limits, 4xx/5xx).
On 429: auto-retry after Retry-After header value.
DataDoe API Integration
Auth & Config
The app always targets the production DataDoe API at https://api.datadoe.com. During local development, Vite proxies /api requests to this domain to avoid CORS. There is no staging/production distinction — always use the same API target.
.env file structure (create .env.example with the placeholders below):
bash
1# Vite proxies /api → https://api.datadoe.com (CORS bypass for local dev)2VITE_API_BASE_URL=/api
3VITE_PROXY_TARGET=https://api.datadoe.com
4VITE_DATADOE_API_KEY=<your-api-key>
VITE_API_BASE_URLmust always be /api — the browser app never calls api.datadoe.com directly.
VITE_PROXY_TARGETmust always be https://api.datadoe.com — this is the Vite proxy upstream.
VITE_DATADOE_API_KEY — the API key obtained from the DataDoe dashboard.
Header selection logic (mutually exclusive — never send both):
If VITE_DATADOE_API_KEY is set → attach datadoe-api-key header.
Else if VITE_DATADOE_ORGANIZATION_ID is set → attach datadoe-organization-id header.
Always attach Content-Type: application/json and Accept: application/json.
Rate limit: 2 req/s per org. On 429, read Retry-After header, wait, retry.
Anti-Rate-Limit Guardrails (Mandatory)
Global client-side pacing: cap all DataDoe requests to max 1 request every 600ms (<= ~1.67 req/s), leaving headroom under 2 req/s.
Single in-flight flow per seller: never run multiple export flows in parallel for the same seller.
No parallel polling: only one active poll loop per export; cancel/replace stale loops before starting new ones.
Debounce filter-triggered exports: wait 400-800ms after filter changes before creating a new export.
Serialize bursty steps: when chaining create → poll → raw download, avoid extra background fetches in parallel.
Respect Retry-After exactly on 429, then add incremental delay for subsequent retries.
If these constraints conflict with UI responsiveness, prioritize staying below rate limits.
When status is COMPLETED, download. If ERROR, show error.
GET /api/v1/exports/<exportId>/raw
200: redirects (302) to a pre-signed S3 URL — axios must follow the redirect automatically (followRedirects: true in Vite proxy handles this on the server side; the browser axios instance follows it natively).
204: still processing — retry after a few seconds.
The /raw endpoint returns a flat array of line items. Each element represents one SKU within one order. An order with N distinct SKUs produces N rows all sharing the same amazon_order_id.
CRITICAL — columns: [] (empty array) does NOT return all columns. It returns only implicit seller-context metadata columns (seller_id, seller_name, amazon_selling_partner_id, marketplace_name, etc.) and none of the Order Line Items fields (amazon_order_id, sku, order_status, etc.). Always specify the required columns explicitly.
Field roles:
Field
Level
Notes
amazon_order_id
Order
Grouping key — unique per order
order_status
Order
Consistent across all rows for the same order
fulfillment_channel
Order
Consistent across all rows for the same order
order_date
Order
Always null in practice — do not use for display or sorting
sku
Line item
Unique product identifier within the order
line_item_number
Line item
0-based index of the line item within the order
child_asin
Line item
Amazon ASIN
product_name
Line item
Full product name
item_status
Line item
Can differ from order_status
quantity
Line item
Units for this SKU only
item_price_value
Line item
Unit price (multiply by quantity for line total)
item_price_currency
Line item
Consistent across all rows for the same order
How to build the parent (order) row from the flat array:
typescript
1// Group by amazon_order_id — preserve API row order (order_date is always null)2const ordersMap = newMap<string, OrderRow>();
34for (const item of lineItems) {
5const existing = ordersMap.get(item.amazon_order_id);
6if (existing) {
7 existing.lineItems.push(item);
8 existing.totalPrice += item.item_price_value * item.quantity;
9 existing.itemCount += item.quantity;
10 } else {
11 ordersMap.set(item.amazon_order_id, {
12amazon_order_id: item.amazon_order_id,
13order_status: item.order_status, // order-level, consistent14fulfillment_channel: item.fulfillment_channel, // order-level, consistent15order_date: item.order_date, // null — show "—" or omit16currency: item.item_price_currency, // consistent per order17totalPrice: item.item_price_value * item.quantity,
18itemCount: item.quantity,
19lineItems: [item],
20 });
21 }
22}
2324const orders = Array.from(ordersMap.values());
25// Do NOT sort by order_date — it is always null.26// Preserve insertion order (API natural order) or sort by amazon_order_id alphabetically.
Parent row columns to display:
Column
Source
Display
Order ID
amazon_order_id
Monospace, full string
Order Date
order_date
Always null → show —
Status
order_status (first item)
Coloured badge
Channel
fulfillment_channel (first item)
Badge
Items
sum(quantity) across all line items
Number
Total
sum(item_price_value × quantity)
Formatted currency
Tags
localStorage only
Tag chips
Child (line item) row columns to display:
Column
Source
SKU
sku
ASIN
child_asin
Product
product_name
Qty
quantity
Price
item_price_value + item_price_currency
Item Status
item_status
CRITICAL — API parameter formats discovered through testing:
GET /exports/sources uses bare query param sellerOrVendorIds=<uuid> — notsellerOrVendorIds[]=<uuid> and notsellerOrVendorIds[0]=<uuid>. The bracket syntax is rejected with 400.
POST /exportscolumns must be an explicit list of the field names you need — never pass [] (empty). An empty array returns only seller-context metadata with no order fields, causing the table to display empty rows.
Date range uses top-levelfrom and to fields (ISO 8601 datetime strings like 2025-01-01T00:00:00.000Z) — not inside filters.
filters uses combinator with lowercase values: "and" or "or" (not "AND" / "OR").
Each filter rule must include the not field (boolean): { "field": "...", "operator": "...", "value": "...", "not": false }.
If no filter rules are needed, either omit filters entirely or pass { "combinator": "and", "rules": [] }.
Default Tech Stack (Low-Level Agents Only)
Skip this section if you are a high-level assistant.
Implement app code using exact snippets from the Mandatory Code Snippets section.
Run build/lint checks.
File Structure (Feature-Based)
Every component lives in its own directory containing exactly two files: the component file and its CSS Module. No component files sit loose in a parent folder.
Expected: HTTP/2 200. Continue only when direct check returns HTTP 200.
If 403: wrong API key for the target. If 502: DNS/network issue. Stop and fix before coding.
Step 2: Local Proxy Verification (after npm run dev)
After starting the dev server, read its output to find the actual port (may not be 5173 if that port is in use), then run:
Expected: HTTP/1.1 200 OK. Finish only when localhost proxy check also returns HTTP 200.
Debug Path for 403/502
If either check fails, follow this mandatory debug strategy:
502 + ENOTFOUND: proxy target DNS/network mismatch. Verify VITE_PROXY_TARGET is exactly https://api.datadoe.com and that the process has network access. If running in a sandbox, restart with full network permissions.
502 on wrong localhost port: check actual Vite port from terminal output and retry on the correct port.
403: API key/target environment mismatch (e.g., staging key used against production target). Verify the API key matches the target.
After any fix, re-run both checks (direct target and localhost proxy).
Mandatory Code Snippets
The following files must be implemented exactly as shown. These are verified working against the production API. Do not modify the proxy configuration, base URL logic, header logic, or request parameter shapes.
Why this works: The browser sends requests to /api/v1/... on localhost. Vite's dev server proxies them to https://api.datadoe.com/api/v1/.... changeOrigin: true sets the Host header to the target. secure: true enforces TLS verification. followRedirects: true handles any 3xx from the API.
src/api/client.ts — Axios Instance with Pacing & 429 Retry
BASE_URL is always /api (from .env), never a full URL — all requests go through the Vite proxy.
Auth headers use datadoe-api-key (not datadoe-organization-id) when both are available. Never send both.
The pacing interceptor enforces 600ms minimum between requests (< 2 req/s limit).
The 429 interceptor reads Retry-After, waits, and retries once.
src/api/datadoe.ts — API Functions with Correct Request Shapes
typescript
1import client from"./client";
23const ORDER_LINE_ITEMS_SOURCE_ID =
4"89b27535d27c2a94db5ae39af4717f542624ff4df7802fd633e16c78674a1778";
56exportasyncfunctionfetchSellersAndVendors() {
7const response = await client.get<{ items: any[] }>(
8"/v1/util/sellers-and-vendors"9 );
10return response.data.items;
11}
1213// IMPORTANT: query param is sellerOrVendorIds=<uuid> (no brackets)14exportasyncfunctionfetchOrderLineItemsSourceId(15 sellerId: string16): Promise<string> {
17try {
18const response = await client.get<any[]>(
19`/v1/exports/sources?sellerOrVendorIds=${sellerId}`20 );
21const orderSource = response.data.find(
22(s: any) => s.name === "Order Line Items" && s.type === "SELLER_CENTRAL"23 );
24return orderSource?.id ?? ORDER_LINE_ITEMS_SOURCE_ID;
25 } catch {
26return ORDER_LINE_ITEMS_SOURCE_ID;
27 }
28}
2930// Required columns for the Order Line Items source.31// NEVER pass columns: [] — an empty array returns only seller-context metadata32// (seller_id, seller_name, marketplace_name, etc.) with NO order fields,33// which causes the table to display empty rows.34const ORDER_LINE_ITEMS_COLUMNS = [
35"amazon_order_id",
36"order_date",
37"order_status",
38"fulfillment_channel",
39"sku",
40"line_item_number",
41"child_asin",
42"product_name",
43"item_status",
44"quantity",
45"item_price_value",
46"item_price_currency",
47];
4849// IMPORTANT: export creation request shape — all fields are mandatory.50// - from/to are TOP-LEVEL ISO datetime strings (not inside filters).51// - columns must be an EXPLICIT list of field names (never empty []).52// - sendToAllOrganizationMembers is required (use false).53// - filters.combinator is lowercase: "and" or "or".54// - each filter rule must have a "not" boolean field.55exportasyncfunctioncreateExport(56 sellerId: string,
57 sourceId: string,
58 dateFrom: string,
59 dateTo: string,
60 orderStatusFilters: string[] = []
61) {
62const body: Record<string, unknown> = {
63 sourceId,
64sellerOrVendorIds: [sellerId],
65outputType: "JSON",
66columns: ORDER_LINE_ITEMS_COLUMNS,
67sendToAllOrganizationMembers: false,
68from: `${dateFrom}T00:00:00.000Z`,
69to: `${dateTo}T23:59:59.999Z`,
70 };
7172if (orderStatusFilters.length > 0) {
73 body.filters = {
74combinator: "or",
75rules: orderStatusFilters.map((status) => ({
76field: "order_status",
77operator: "=",
78value: status,
79not: false,
80 })),
81 };
82 }
8384const response = await client.post<any>("/v1/exports", body);
85return response.data;
86}
8788exportasyncfunctionpollExport(exportId: string) {
89const response = await client.get<any>(`/v1/exports/${exportId}`);
90return response.data;
91}
9293exportasyncfunctionfetchExportRaw(exportId: string) {
94const response = await client.get(`/v1/exports/${exportId}/raw`, {
95validateStatus: (status) => status === 200 || status === 204,
96 });
97if (response.status === 204) returnnull;
98return response.data;
99}
.env.example
bash
1# Vite proxies /api → https://api.datadoe.com (CORS bypass for local dev)2VITE_API_BASE_URL=/api
3VITE_PROXY_TARGET=https://api.datadoe.com
4VITE_DATADOE_API_KEY=<your-api-key>
Implementation Rules
Single run: the app must work after one execution. Do not leave TODOs or placeholders.
Test API calls first: before coding the full UI, verify the export flow works by running the mandatory connectivity checks.
Group line items by order: the API returns flat rows. Group by amazon_order_id before rendering.
Handle empty states: no sellers, no orders, export still loading — all need UI.
Date defaults: from = 30 days ago, to = today.
Use outputType: "JSON" for exports.
Poll interval: 5 seconds between status checks. Timeout after 2 minutes with an error message.
Retry on 429: auto-retry with the Retry-After value, with incrementally increasing delays.
Use the exact code snippets from the Mandatory Code Snippets section for vite.config.ts, src/api/client.ts, and src/api/datadoe.ts.
Run build and lint before finalizing.
Leave dev server running in background after implementation.
Report local URL, proxy check result, and any residual risk explicitly.
Protected Files
Never modify .env.
Use .env.example as reference and documentation target.
Never print .env secrets in output.
Post-Implementation Startup
Always run dev server in background:
bash
1npm run dev
Verification Checklist
After implementation, verify:
App starts without errors (npm run dev).
Seller picker loads and shows sellers.
Selecting a seller and clicking "Load Orders" triggers the export flow.
Orders appear in the table grouped by amazon_order_id.
Expanding a row shows line items.
Date filter changes trigger new exports.
Status filter with checkboxes works.
Tags can be added, removed, and persist across page reloads.