The Bulk Event Upload page lets a partner drop an Excel workbook and ingest many events at once. Like Event Editor, it lives on the CR API. The route is gated by Permissions.simulation in the reference webapp.

What you’ll build

  • A file picker (.xls / .xlsx).
  • Client-side column validation + workbook normalisation.
  • An upload action (indeterminate loading state).
  • A success toast + redirect, or a row-level error screen.

File formats

XLS / XLSX only. The reference FE uses the xlsx library and does not accept CSV.

Prerequisites

  • An authenticated user with Permissions.simulation.
  • X-Preferred-Partner-Id set.
  • The partner’s product / access tier — drives event_type (see below).

Deriving event_type

The reference FE does not use Permissions.prisma / Permissions.fdlive. It derives event_type dynamically:
  • prisma when hasPartialServiceAccessTier() is true (partner product PRISMA or FD_FULL_SERVICE) or the page was reached from Wave (location.state?.from === 'wave')
  • otherwise fdlive

The call chain

On upload:
MethodURLHostNotes
POSTevents/bulk_upload?event_type=prisma|fdliveCR APImultipart/form-data, field name file. Manually attach Authorization and X-Preferred-Partner-Id (the FE uses a direct axios call, not the shared instance).

Pre-upload validation and normalisation

Before sending, the reference FE:
  1. Validates required columns — the required set differs between prisma and fdlive.
  2. Normalises the workbook (delocalizeSheetAndHeaders, formatSheetFromArrays) — so the uploaded file is not always sent verbatim; headers and localised values are transformed first.
The partner-specific template and validation headers are the source of truth, not a generic column table.

Template download

GET /downloads/event-bulk-upload/[prisma/]bulk_events-{EU|US}-{lang}.xlsx
(Region EU / US and language are chosen per partner. This is not /downloads/sales_template.xlsx.)

Response handling

The HTTP status is 200 for both full success and partial failure — you must inspect the body. Two shapes: Row-level errors — [400, errs] with HTTP 200:
[400, [ { "ROW_3_INVALID_DATE": "..." }, { "ROW_7_DUPLICATE_TITLE": "..." } ]]
The reference FE extracts error keys with Object.keys(errorObj) — i.e. it expects each error object shaped like { "ROW_3_INVALID_DATE": ... }, not { "errCode": "ROW_3_INVALID_DATE" }. Top-level error — in the .catch branch:
{ "detail": [ { "name": "ROW_3_INVALID_DATE" } ] }
On success the FE shows a toast and redirects to / after ~2s.

Reference implementation

async function bulkUploadEvents(
  file: File,                          // .xls / .xlsx, already normalised
  eventType: "prisma" | "fdlive",
): Promise<{ errors: string[] }> {
  const form = new FormData();
  form.append("file", file);

  const resp = await axios.post(
    `${CR_BASE}/events/bulk_upload?event_type=${eventType}`,
    form,
    {
      headers: {
        Authorization:           `Bearer ${accessToken()}`,
        "X-Preferred-Partner-Id": partnerId(),
        "Content-Type":           "multipart/form-data",
      },
      // onUploadProgress exists, but the reference FE shows an indeterminate
      // spinner rather than a real percentage.
    },
  ).catch((e) => {
    const detail = e?.response?.data?.detail;
    if (Array.isArray(detail)) {
      return { __caught: { errors: detail.map((d: any) => d.name) } };
    }
    throw e;
  });

  if ((resp as any).__caught) return (resp as any).__caught;

  const body = (resp as any).data;
  if (Array.isArray(body) && body[0] === 400) {
    const errs = (body[1] ?? []).flatMap((o: any) => Object.keys(o));
    return { errors: errs };
  }
  return { errors: [] };
}

Gotchas

The reference FE rejects CSV. Accept .xls / .xlsx.
Sending this to the main API returns 404.
The FE uses a direct axios call (no transformRequest), so it adds Authorization + X-Preferred-Partner-Id by hand. Replicate or 401.
The [400, errs] shape carries { "ROW_x_...": ... } objects — read keys, not an errCode field.
The FE transforms headers/values and validates required columns (which differ for prisma vs fdlive) before sending. Don’t send the user’s file verbatim.
Not prisma/fdlive permission enums.