Migration guide

Migrate from SessionBoard

Move your event's program — tracks, speakers, sessions, schedule — to your own OpenSession instance in about five minutes. One CLI, both public APIs, safe to re-run.

Why migrate

  • Own your data. Everything lives in your Postgres database, exportable at any time — no vendor lock-in, no per-seat pricing.
  • Same API shape. OpenSession's REST API mirrors SessionBoard's conventions — x-access-token auth, snake_case payloads, POST …/create — so existing scripts and integrations port with a URL change.
  • The whole program workflow. CFP forms with conditional logic, evaluation rounds, one-click accept/decline emails, a speaker portal, and a drag-and-drop agenda with live conflict detection.
  • Open source, MIT licensed. Self-host on a free tier; read, fork, and extend every line.

What moves over

EntityDeduped byNotes
Tracks, tags, formats, levels, roomsnameCreated if missing; track colors snap to the nearest palette preset, room capacity carries over.
Custom field valuesinternal_nameSessionBoard's array shape becomes an object keyed by internal_name. Definitions are reported, not created — add them in Settings → Fields to surface the values.
ContactsemailProfile, company, links, pronouns, bio, custom fields. Contacts without an email are reported and skipped.
Sessionsclient_session_id, then titleTitle, description (HTML kept), status, abstract flag, schedule, capacity, CEU credits, metadata by name, tags, custom fields. The SessionBoard id is stored as client_session_id.
Participantscontact emailSpeakers, moderators and chairpersons (legacy and Sessions 2.0 shapes) attach with their role and primary flag via a follow-up update.

Not migrated: evaluations, sent emails, speaker-portal state, uploaded files, agenda drafts, and subsessions — see the FAQ below.

The 5-minute migration

  1. 1

    Grab your SessionBoard API token

    In SessionBoard: Organization Settings → API Tokens. Read scopes are enough — the migration never writes to the source. Note your event id (an integer), then sanity check the token:

    curl -H "x-access-token: $SB_TOKEN" \
      https://public-api.sessionboard.com/v1/events
  2. 2

    Mint an OpenSession token

    In your OpenSession event: Settings → API Tokens. Include the read and write scopes for metadata, contacts, and sessions. Then find your destination event's UUID:

    curl -H "x-access-token: $OS_TOKEN" \
      https://your-opensession-host/api/v1/events
  3. 3

    Dry-run the migration

    From a checkout of the OpenSession repo (after pnpm install). The dry run prints the full plan — per-entity counts and samples — without writing anything:

    pnpm exec tsx scripts/migrate-from-sessionboard.ts \
      --source-url https://public-api.sessionboard.com \
      --source-token $SB_TOKEN --source-event 12345 \
      --dest-url https://your-opensession-host \
      --dest-token $OS_TOKEN --dest-event $EVENT_UUID \
      --dry-run
  4. 4

    Run it for real, then verify

    Drop --dry-run and run the same command. You'll get a created/skipped/failed table plus per-row failure detail. Re-running is safe — a second pass skips everything. Then spot-check:

    curl -H "x-access-token: $OS_TOKEN" \
      "https://your-opensession-host/api/v1/event/$EVENT_UUID/sessions?pageSize=5"

    Or open your event in the app: Submissions, Speakers, and Agenda should all be populated.

Do it with an AI agent

The migration is a CLI plus two documented REST APIs — exactly the kind of task coding agents are good at. Paste one of these into Claude Code (or any agent with shell access), fill in the placeholders, and keep your tokens in environment variables.

Plan and run the migration (dry-run first)
Clone https://github.com/ky-zo/opensession and run a SessionBoard migration for me.

Source: SessionBoard event <EVENT_ID>, token in $SB_TOKEN.
Destination: https://<my-opensession-host>, event <EVENT_UUID>, token in $OS_TOKEN.

1. Run scripts/migrate-from-sessionboard.ts with --dry-run and show me the plan.
2. Stop and wait for my approval.
3. After I approve, run it for real, then run it once more to confirm the
   second pass skips everything (it is idempotent).
4. Summarize created/skipped/failed per entity and any warnings I should act on.

Never paste my tokens into the chat and never commit them anywhere.

OpenSession also exposes an MCP endpoint at /api/mcp, so agents can explore your destination event with structured tools instead of raw curl. Configure it with your API token:

MCP server config (mcp.json)
{
  "mcpServers": {
    "opensession": {
      "url": "https://your-opensession-host/api/mcp",
      "headers": { "x-access-token": "<your OpenSession API token>" }
    }
  }
}
Pre-flight check over MCP
Using the opensession MCP server, list my events, then look at event
<EVENT_UUID>: which tracks, tags, formats, levels, rooms, contacts and
sessions already exist? Summarize what a SessionBoard import would still
need to add, and flag anything that looks like a duplicate.
Post-migration verification
My SessionBoard event <EVENT_ID> was just migrated to OpenSession event
<EVENT_UUID>. Compare the two over their REST APIs (both use an
x-access-token header; SessionBoard is at public-api.sessionboard.com/v1,
OpenSession at https://<my-opensession-host>/api/v1): count tracks, tags,
formats, levels, rooms, contacts and sessions on each side, diff the
session titles, and flag any session whose track, room, schedule or
speaker list does not match the source.

Field mapping reference

OpenSession's API is SessionBoard-shaped by design, so most fields map 1:1 under the same name. The full serialization lives in packages/api/src/rest/serialize.ts.

Sessions

SessionBoardOpenSessionNotes
idclient_session_idkept for traceability; a new UUID is assigned
friendly_idfriendly_idre-allocated as SESS-n in order of import
title / descriptiontitle / descriptionHTML descriptions preserved
statusstatussame enum (accepted, accept_queue, pending, …)
is_abstract / is_publicis_abstract / is_public
starts_at / ends_atstarts_at / ends_at
capacity / ceu_creditscapacity / ceu_credits
track / format / level / roomtrack_id / format_id / level_id / room_idmatched by name
tags[]tag_ids[]matched by name
custom_fields (array)custom_fields (object)keyed by internal_name
participants / speakers / moderators / chairpersonsparticipants[]role + is_primary kept
language, subsessions, composition, filesnot migrated

Contacts

SessionBoardOpenSessionNotes
first_name / last_name / emailfirst_name / last_name / emailemail is required and deduped
company_name / titlecompany_name / title
aboutabout
phone_mobile (or phone_home)phone_mobile
photo_url / website_url / linkedin_url / twitter_url / facebook_urlsame names
pronounspronouns
custom_fields (array)custom_fields (object)keyed by internal_name
speaker-bureau extras (speaker_score, availability, industry, …)not migrated

FAQ