Two related flows live under “uploads”: per-event manual sales ingest (from the Event detail action menu) and the generic Uploads page for sales-data spreadsheets.

Flow 1 — Per-event “Add sales”

Triggered from the Event detail page’s action menu. The user picks either manual or file upload mode.

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

POST /ingest/transaction_summary
{
  "eid":    "evt_abc123",
  "from":   "2026-06-01",
  "to":     "2026-06-12",
  "tickets": [
    { "type": "standard", "sold": 412, "revenue": 18540 },
    { "type": "premium",  "sold":  87, "revenue":  7830 }
  ]
}
Returns 200 on success with the persisted record. Validation errors come back as 400 with the standard error envelope.

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

POST /ingest/transaction_summary/from_excel?eid={eid}
Content-Type: multipart/form-data
form-data: file=<.xlsx>
Template download: /downloads/sales_template.xlsx (static asset). Ask support for partner-specific column variants.

Reference implementation

export async function ingestManualSales(payload: {
  eid: string;
  from: string;     // YYYY-MM-DD
  to: string;       // YYYY-MM-DD
  tickets: { type: string; sold: number; revenue: number }[];
}) {
  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" } },
  );
}
The reference webapp builds the XLSX upload with raw axios (not the shared instance) but still injects Authorization and X-Preferred-Partner-Id. You can use your normal client — just make sure Content-Type is set to multipart/form-data (or let the browser set it automatically with the boundary).

Flow 2 — Generic Uploads page (/uploads)

The Uploads page lets users drop multiple files, set a category per file, and dispatch each to the correct ingest endpoint. For the sales-data category, the upload routes to:
POST /daily_revenue_summary/sales_edit_export
Content-Type: multipart/form-data
form-data: file=<.xlsx>
Note: this same path serves an export on GET — the same URL, different verb, returns a partner’s current sales-edit sheet as XLSX:
const blob = await fd.get<Blob>("/daily_revenue_summary/sales_edit_export", {
  responseType: "blob",
});

Response on upload

200 on success. Validation errors come back with:
{
  "validation_errors": [
    { "row": 12, "code": "MISSING_REVENUE" },
    { "row": 18, "code": "DUPLICATE_DATE" }
  ]
}
Render row numbers so the user can find and fix each problem in the spreadsheet.

Clearing previously-ingested sales

From the Event detail’s “Clear sales” menu item, with a confirm modal:
DELETE /daily_revenue_summary?eid={eid}
Returns 204. This deletes all sales-related rows the partner has ingested for the event (manual + file + adjustment).

Reference implementation — file routing

type FileCategory = "sales-data" | "marketing-creative" | "event-list" | ...;

async function uploadFile(file: File, category: FileCategory) {
  switch (category) {
    case "sales-data":
      return uploadSalesData(file);
    case "event-list":
      return bulkUploadEvents(file, "fdlive");  // see Bulk Upload guide
    // ...
  }
}

async function uploadSalesData(file: File) {
  const form = new FormData();
  form.append("file", file);
  const resp = await fd.post("/daily_revenue_summary/sales_edit_export", form, {
    onUploadProgress: e => {
      if (e.total) setProgress(Math.round((e.loaded / e.total) * 100));
    },
  });
  return resp.data;
}

Gotchas

On chunked-transfer responses some browsers don’t surface total. Guard the percentage calc — the reference webapp doesn’t and shows NaN%.
Same URL: GET = download, POST = upload. Don’t accidentally call GET when you meant POST.
For both transaction_summary and sales_edit_export the inner validation_errors array can be non-empty even on a 200. Always inspect the body.
Confirm with the user, and consider exporting first via the GET counterpart.