Two related flows: per-event manual sales ingest (from the Event detail action menu) and the sales-edit export/import. All of these use the main API (apiURL + /v3), not the CR host.

Flow 1 — Per-event “Add sales”

Triggered from the Event detail page’s action menu. Route modes are snake_case: sales_summary and document_upload. There is also a drawer-based variant reachable via update-sales=... query params.

Manual mode (/events/{eid}/update-sales/sales_summary)

POST /ingest/transaction_summary
{
  "eid":    "evt_abc123",
  "from":   "2026-06-01",
  "to":     "2026-06-12",
  "tickets": [
    { "type": "standard", "sold": 412, "revenue": 18540 }
  ]
}
The reference FE treats this primarily by HTTP status200 is success. It does not render row-level validation codes; non-200 responses surface a generic failure message.

File-upload mode (/events/{eid}/update-sales/document_upload)

POST /ingest/transaction_summary/from_excel?eid={eid}
Content-Type: multipart/form-data
form-data: file=<.xlsx>
Note this path is versioned (/v3/ingest/...), unlike the unversioned POST /media.

Reference implementation

export async function ingestManualSales(payload: {
  eid: string; from: string; to: string;
  tickets: { type: string; sold: number; revenue: number }[];
}) {
  // Main API. Treat 200 as success; surface a generic error otherwise.
  return fd.post("/ingest/transaction_summary", payload);
}

export async function ingestSalesExcel(eid: string, file: File) {
  const form = new FormData();
  form.append("file", file);
  return fd.post(
    `/ingest/transaction_summary/from_excel?eid=${encodeURIComponent(eid)}`,
    form,
    { headers: { "Content-Type": "multipart/form-data" } },
  );
}

Flow 2 — Sales-edit export / import

The sales-edit sheet is dual-purpose on the same path (main API):
GET  /daily_revenue_summary/sales_edit_export   # download current sheet (blob)
POST /daily_revenue_summary/sales_edit_export   # upload a modified sheet (multipart)
// Download
const blob = await fd.get<Blob>("/daily_revenue_summary/sales_edit_export", {
  responseType: "blob",
});

// Upload
const form = new FormData();
form.append("file", file);
await fd.post("/daily_revenue_summary/sales_edit_export", form);

Clearing previously-ingested sales

From the Event detail’s “Clear sales” menu item, with a confirm modal (main API):
DELETE /daily_revenue_summary?eid={eid}
The reference FE treats only status === 200 as success for clear sales — not 204. Match that, or you’ll show a spurious error on a successful clear.

Gotchas

All sales endpoints — transaction_summary, from_excel, sales_edit_export, and the clear DELETE — are on the main API (apiURL + /v3), not the CR host.
sales_summary and document_upload, plus a drawer variant via update-sales=... query params.
The FE keys off HTTP status (200 = success) and shows a generic failure otherwise; it does not parse row-level validation codes for transaction_summary.
Only 200 is treated as success.