Docs API reference
Import speakers from a CSV
A complete Node script that reads a CSV and upserts every speaker in one call.
Your speaker portal, CRM or registration tool can export its speakers as a CSV. This guide turns that file into Wunderframe presenters with one short Node script — no dependencies — that you can run again whenever the list changes. Running it twice never creates duplicates.
How it works
- Every speaker gets a ref made from the portal’s own id, like
speakerportal/1234. POST /presenters/importupserts up to 500 presenters by ref per call: new refs are created, known refs are updated.- A row that fails is reported back and never stops the others.
- A
photo_urlis fetched by Wunderframe and stored in your media library as the presenter’s photo.
Set up
Create an API key
In Settings → Integrations, create a key with the
presentersscope and copy it. See Authentication.Export your speakers
Save them as
speakers.csvwith a header row. The script below expects these columns; rename them intoPresenterif your export differs.Install Node.js
Node.js 18 or newer has
fetchbuilt in, which is all the script needs.
id,name,title,company,email,bio,photo
1234,Ada Lovelace,Head of Research,Analytical Engines Ltd,ada@example.com,"Writes the first programs.",https://example.com/photos/ada.jpg
1235,Grace Hopper,Rear Admiral,US Navy,grace@example.com,"Invented the compiler, found the first bug.",https://example.com/photos/grace.jpg
1236,Katherine Johnson,Mathematician,NASA,,,The script
// import-speakers.mjs — upsert every speaker in a CSV into Wunderframe.
// Usage: WUNDERFRAME_API_KEY=wfi.… node import-speakers.mjs speakers.csv
import { readFile } from 'node:fs/promises';
const BASE = process.env.WUNDERFRAME_URL ?? 'https://www.wunderframe.com';
const KEY = process.env.WUNDERFRAME_API_KEY;
const SOURCE = 'speakerportal'; // prefix for refs: speakerportal/1234
const BATCH = 500; // the most one import call accepts
if (!KEY) throw new Error('Set WUNDERFRAME_API_KEY to a key with the "presenters" scope.');
/** A small CSV reader: commas, quoted fields, "" inside quotes, CRLF. */
function parseCsv(text) {
const rows = [];
let row = [], field = '', quoted = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (quoted) {
if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') quoted = false;
else field += c;
} else if (c === '"') quoted = true;
else if (c === ',') { row.push(field); field = ''; }
else if (c === '\n' || c === '\r') {
if (c === '\r' && text[i + 1] === '\n') i++;
row.push(field); rows.push(row); row = []; field = '';
} else field += c;
}
if (field || row.length) { row.push(field); rows.push(row); }
const [header, ...body] = rows.filter((r) => r.some((v) => v.trim()));
return body.map((r) => Object.fromEntries(header.map((h, i) => [h.trim(), (r[i] ?? '').trim()])));
}
/** Map one CSV row to a presenter. Empty cells are left out, so they never blank a field. */
function toPresenter(row) {
const presenter = {
ref: `${SOURCE}/${row.id}`,
name: row.name,
title: row.title,
organization: row.company,
email: row.email,
bio: row.bio,
photo_url: row.photo
};
return Object.fromEntries(Object.entries(presenter).filter(([, v]) => v));
}
const rows = parseCsv(await readFile(process.argv[2] ?? 'speakers.csv', 'utf8'));
const items = rows.filter((r) => r.id && r.name).map(toPresenter);
let created = 0, updated = 0, failed = 0;
for (let start = 0; start < items.length; start += BATCH) {
const res = await fetch(`${BASE}/api/v1/presenters/import`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ items: items.slice(start, start + BATCH) })
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
created += body.created;
updated += body.updated;
for (const f of body.failed) {
failed++;
console.warn(`Row ${start + f.index + 2} (${f.ref ?? 'no ref'}): ${f.error.message}`);
}
}
console.log(`Done: ${created} created, ${updated} updated, ${failed} failed.`);Run it
export WUNDERFRAME_API_KEY="wfi.…"
node import-speakers.mjs speakers.csvThe script prints the rows that failed with their line number in the CSV, then a summary:
Row 3 (speakerportal/1235): The image URL answered 404.
Done: 2 created, 0 updated, 1 failed.Good to know
- Empty cells are skipped, so a speaker without a bio in the CSV keeps the bio someone typed in the app. To clear a field on purpose, send it as an empty string.
- Photos must be public https images (JPEG, PNG, WebP or GIF, up to 10 MB). See Upload presenter photos.
- Removing a speaker from the CSV does not delete the presenter. Delete them with
DELETE /presenters/{id}— a presenter in a session that has not finished cannot be deleted. - Changes only: to see what changed on the Wunderframe side since your last run, list presenters with
updated_since.