The Bulk Event Upload page lets a partner drop a CSV or XLSX file and ingest many events at once. Like Event Editor, it lives on the CR API. This page is feature-flagged behind SHOW_SIMULATION in the reference webapp. If your integration doesn’t need bulk ingest, skip it.

What you’ll build

  • A drag-and-drop file picker (.csv and .xlsx).
  • An upload action with progress feedback.
  • A success / row-level error screen.
  • A “download template” link (static asset).

Prerequisites

  • An authenticated user.
  • X-Preferred-Partner-Id set.
  • The user has Permissions.prisma (selects prisma) or Permissions.fdlive (selects fdlive). Determines the event_type query param.

The call chain

On upload:
MethodURLHostNotes
POSTevents/bulk_upload?event_type=prisma|fdliveCR APImultipart/form-data with file field. Manually attach Authorization and X-Preferred-Partner-Id headers — the reference webapp doesn’t use its axios instance here.

Request

POST events/bulk_upload?event_type=fdlive HTTP/1.1
Host: cr.client-api.stg.future-demand.com/api
Authorization: Bearer <access_token>
X-Preferred-Partner-Id: <partner_id>
Content-Type: multipart/form-data; boundary=...

--...
Content-Disposition: form-data; name="file"; filename="events.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

<binary>
--...--

Response

The endpoint returns either of two shapes — the reference webapp branches on the response shape, not on status code: Success / partial success (row-level errors):
[400, [{ "errCode": "ROW_3_INVALID_DATE" }, { "errCode": "ROW_7_DUPLICATE_TITLE" }]]
The 400 is just a sentinel inside the array body, not the HTTP status. The HTTP response is 200. If the inner array is empty (or absent), the upload fully succeeded. Top-level error:
{ "detail": [{ "name": "ROW_3_INVALID_DATE" }, { "name": "ROW_7_DUPLICATE_TITLE" }] }

Reference implementation

async function bulkUploadEvents(
  file: File,
  eventType: "prisma" | "fdlive",
  onProgress?: (pct: number) => void,
): Promise<{ errors: string[] }> {
  const form = new FormData();
  form.append("file", file);

  const xhr = new XMLHttpRequest();
  xhr.open("POST", `${CR_BASE}/events/bulk_upload?event_type=${eventType}`);
  xhr.setRequestHeader("Authorization", `Bearer ${accessToken()}`);
  xhr.setRequestHeader("X-Preferred-Partner-Id", partnerId());

  if (onProgress) {
    xhr.upload.onprogress = e => {
      if (e.lengthComputable) onProgress((e.loaded / e.total) * 100);
    };
  }

  return new Promise((resolve, reject) => {
    xhr.onload = () => {
      try {
        const body = JSON.parse(xhr.responseText);
        // Two response shapes
        if (Array.isArray(body) && body[0] === 400) {
          resolve({ errors: (body[1] ?? []).map((e: any) => e.errCode) });
        } else if (body?.detail) {
          resolve({ errors: body.detail.map((e: any) => e.name) });
        } else {
          resolve({ errors: [] });
        }
      } catch (e) {
        reject(e);
      }
    };
    xhr.onerror = () => reject(new Error("Network error"));
    xhr.send(form);
  });
}
XMLHttpRequest is the simplest way to get onUploadProgress in the browser; fetch only got it in 2024 and support is uneven. Switch to fetch with ReadableStream if you control the runtime.

Template

Provide a downloadable template so users know the column layout:
GET /downloads/sales_template.xlsx        # static asset; ask support for the events template
The exact column set is partner-specific (Future Demand will issue a tailored template based on your data model). At minimum:
ColumnTypeNotes
titlestringRequired.
start_date_timeISO 8601Required. UTC or with offset.
citystringRequired.
venuestringRequired.
categorystringOne of the partner’s registered event categories.
capacityintegerOptional.
currencyISO 4217Optional; falls back to partner default.
price_min / price_maxnumberOptional.

Gotchas

Just like Event Editor. Sending this to the main API returns 404.
The reference webapp bypasses its axios instance (so no automatic transformRequest), and manually adds Authorization and X-Preferred-Partner-Id. Replicate, or you’ll get 401.
[400, [...]] array vs { detail: [...] } object. The HTTP status is 200 for both successful and partial-failure responses; you must inspect the body.
Without it, users will refresh the page mid-upload. Surface a percent.
${crURL}events/bulk_upload — assumes crURL ends in /. If you build your URL differently, double-check there’s exactly one slash.