callboard / developers

Public API

REST/JSON under /v1, event-scoped, with Sessionboard-compatible conventions: POST search with a filter body, a /create suffix for creates, soft delete plus restore, and the x-access-token header.

openapi.jsonMint a key

Authentication

Mint a key in Admin → API keys. The value is shown once and stored only as a SHA-256 hash. Keys are scoped to one event, and scopes do not cascaderead:sessions grants nothing else, and no read scope ever grants a write.

export CALLBOARD_KEY='cb_…'
curl -sS 'https://demo.callboardhq.com/v1/events' -H "x-access-token: $CALLBOARD_KEY"

Scopes: read:events · read:sessions · write:sessions · read:contacts · read:metadata · write:metadata. Missing key → 401; wrong event or missing scope → 403.

MCP for agents

A separate streamable-HTTP Worker exposes the common programme workflows as eight compact tools and reaches Callboard only through this public API. Point an MCP client at the /mcp route of that Worker and send the same scoped key as x-access-token. The endpoint is deliberately not printed here: the MCP Worker is deployed separately from this one and shares no configuration with it, so this page cannot know its URL.

Tools: list_events, get_schedule, list_submissions, get_submission, search_speakers, list_tracks, capture_abstract, and get_openapi. Self-hosting and connector examples are in docs/MCP.md in the repository.

One envelope

Every collection returns the same shape. Sessionboard returns {results, pagination} for POST search and {data, pagination} with snake_case keys for its CRUD-proxy GETs; we do not reproduce that. Unassigned metadata is always null, never {}.

{
  "results": [ … ],
  "pagination": { "currentPage": 1, "pageSize": 25, "totalPages": 4, "totalResults": 97 }
}

page and pageSize (default 25, max 100) go in the query string on GET and in the body on POST search. Errors are { "error": "…", "message": "…" }.

Endpoints — examples use “Frontier AI Summit 2026”

GET/v1/eventsread:events

List events

Every event the presented key can reach. Keys are minted per event, so this returns exactly one event — the entry point that tells an integration which `eventId` to use everywhere else.

curl -sS -X GET 'https://demo.callboardhq.com/v1/events' \
  -H 'x-access-token: $CALLBOARD_KEY'
Example response
{
  "results": [
    {
      "id": "0000000ev-0000-4000-8000-000000000001",
      "name": "Frontier AI Summit 2026",
      "slug": "frontier-ai-summit-2026",
      "timezone": "America/Los_Angeles"
    }
  ],
  "pagination": {
    "currentPage": 1,
    "pageSize": 25,
    "totalPages": 1,
    "totalResults": 1
  }
}
POST/v1/event/{eventId}/sessions/searchread:sessions

Search sessions and abstracts

The workhorse. Abstracts and programme sessions are one resource discriminated by `is_abstract`, so this endpoint serves both 'list the CFP' and 'list the agenda'. `POST /v1/event/{eventId}/sessions` is an alias for wire compatibility with Sessionboard's POST-on-collection search.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/search' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"filters":{"status":["accepted","pending"],"is_abstract":true,"text":"agents"},"sort":{"order":"updatedAt","sort":"desc"},"page":1,"pageSize":25}'
Example response
{
  "results": [
    {
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "event_id": "0000000ev-0000-4000-8000-000000000001",
      "friendly_id": "ABS-3",
      "title": "Shipping agents that survive contact with users",
      "status": "accepted",
      "is_abstract": true,
      "starts_at": null,
      "ends_at": null,
      "capacity": null,
      "is_public": false,
      "composition_status": {
        "role": "standalone",
        "is_linked": false,
        "is_read_only": false,
        "source_count": 0,
        "target": null
      },
      "track": {
        "id": "…",
        "event_id": "…",
        "name": "Agents",
        "order": 0,
        "color": "#329af0"
      },
      "room": null,
      "participants": [],
      "custom_fields": [
        {
          "key": "takeaways",
          "value": "Three things you can apply Monday."
        }
      ],
      "created_at": "2026-07-25T12:00:00.000Z",
      "updated_at": "2026-08-06T12:00:00.000Z",
      "deleted_at": null,
      "admin_url": "https://callboard.example/admin/submissions/497f6eca-…"
    }
  ],
  "pagination": {
    "currentPage": 1,
    "pageSize": 25,
    "totalPages": 1,
    "totalResults": 8
  }
}
GET/v1/event/{eventId}/sessions/{sessionId}read:sessions

Get one session

Full record: participants, metadata, composition status, and CFP answers as `custom_fields`.

curl -sS -X GET 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/SESSION_ID' \
  -H 'x-access-token: $CALLBOARD_KEY'
Example response
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "event_id": "0000000ev-0000-4000-8000-000000000001",
  "friendly_id": "ABS-3",
  "title": "Shipping agents that survive contact with users",
  "status": "accepted",
  "is_abstract": true,
  "starts_at": null,
  "ends_at": null,
  "capacity": null,
  "is_public": false,
  "composition_status": {
    "role": "standalone",
    "is_linked": false,
    "is_read_only": false,
    "source_count": 0,
    "target": null
  },
  "track": {
    "id": "…",
    "event_id": "…",
    "name": "Agents",
    "order": 0,
    "color": "#329af0"
  },
  "room": null,
  "participants": [],
  "custom_fields": [
    {
      "key": "takeaways",
      "value": "Three things you can apply Monday."
    }
  ],
  "created_at": "2026-07-25T12:00:00.000Z",
  "updated_at": "2026-08-06T12:00:00.000Z",
  "deleted_at": null,
  "admin_url": "https://callboard.example/admin/submissions/497f6eca-…"
}
POST/v1/event/{eventId}/sessions/createwrite:sessions

Create a session or abstract

`title` is the only required field. `is_abstract: true` creates a CFP submission and is immutable afterwards — an abstract never becomes a session, it gets composed into one. `is_public` is update-only.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/create' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"title":"Evals that predict production failures","is_abstract":true,"status":"pending","description":"<p>HTML allowed.</p>"}'
Example response
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "event_id": "0000000ev-0000-4000-8000-000000000001",
  "friendly_id": "ABS-3",
  "title": "Shipping agents that survive contact with users",
  "status": "accepted",
  "is_abstract": true,
  "starts_at": null,
  "ends_at": null,
  "capacity": null,
  "is_public": false,
  "composition_status": {
    "role": "standalone",
    "is_linked": false,
    "is_read_only": false,
    "source_count": 0,
    "target": null
  },
  "track": {
    "id": "…",
    "event_id": "…",
    "name": "Agents",
    "order": 0,
    "color": "#329af0"
  },
  "room": null,
  "participants": [],
  "custom_fields": [
    {
      "key": "takeaways",
      "value": "Three things you can apply Monday."
    }
  ],
  "created_at": "2026-07-25T12:00:00.000Z",
  "updated_at": "2026-08-06T12:00:00.000Z",
  "deleted_at": null,
  "admin_url": "https://callboard.example/admin/submissions/497f6eca-…"
}
PUT/v1/event/{eventId}/sessions/{sessionId}write:sessions

Update a session

Partial update. Send the `updated_at` you last read to get optimistic concurrency: a `409` means somebody else wrote first. Omit it to force the write. Flipping `is_public` true requires the speaker's decision letter to have been sent, or `publish_override: true`.

curl -sS -X PUT 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/SESSION_ID' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"status":"accepted","updated_at":"2026-08-06T12:00:00.000Z"}'
Example response
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "event_id": "0000000ev-0000-4000-8000-000000000001",
  "friendly_id": "ABS-3",
  "title": "Shipping agents that survive contact with users",
  "status": "accepted",
  "is_abstract": true,
  "starts_at": null,
  "ends_at": null,
  "capacity": null,
  "is_public": false,
  "composition_status": {
    "role": "standalone",
    "is_linked": false,
    "is_read_only": false,
    "source_count": 0,
    "target": null
  },
  "track": {
    "id": "…",
    "event_id": "…",
    "name": "Agents",
    "order": 0,
    "color": "#329af0"
  },
  "room": null,
  "participants": [],
  "custom_fields": [
    {
      "key": "takeaways",
      "value": "Three things you can apply Monday."
    }
  ],
  "created_at": "2026-07-25T12:00:00.000Z",
  "updated_at": "2026-08-06T12:00:00.000Z",
  "deleted_at": null,
  "admin_url": "https://callboard.example/admin/submissions/497f6eca-…"
}
DELETE/v1/event/{eventId}/sessions/{sessionId}write:sessions

Soft-delete a session

The row stays and `deleted_at` is set. Deleted rows drop out of search unless you pass `filters.include_deleted`.

curl -sS -X DELETE 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/SESSION_ID' \
  -H 'x-access-token: $CALLBOARD_KEY'
Example response
{
  "id": "497f6eca-…",
  "deleted_at": "2026-08-08T18:04:00.000Z"
}
POST/v1/event/{eventId}/sessions/{sessionId}/restorewrite:sessions

Restore a soft-deleted session

Clears `deleted_at`. 404s if the session was never deleted.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/SESSION_ID/restore' \
  -H 'x-access-token: $CALLBOARD_KEY'
Example response
{
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "event_id": "0000000ev-0000-4000-8000-000000000001",
  "friendly_id": "ABS-3",
  "title": "Shipping agents that survive contact with users",
  "status": "accepted",
  "is_abstract": true,
  "starts_at": null,
  "ends_at": null,
  "capacity": null,
  "is_public": false,
  "composition_status": {
    "role": "standalone",
    "is_linked": false,
    "is_read_only": false,
    "source_count": 0,
    "target": null
  },
  "track": {
    "id": "…",
    "event_id": "…",
    "name": "Agents",
    "order": 0,
    "color": "#329af0"
  },
  "room": null,
  "participants": [],
  "custom_fields": [
    {
      "key": "takeaways",
      "value": "Three things you can apply Monday."
    }
  ],
  "created_at": "2026-07-25T12:00:00.000Z",
  "updated_at": "2026-08-06T12:00:00.000Z",
  "deleted_at": null,
  "admin_url": "https://callboard.example/admin/submissions/497f6eca-…"
}
POST/v1/event/{eventId}/sessions/bulkwrite:sessions

Bulk create / update / delete

Up to 100 operations, applied in order with PARTIAL SUCCESS: each item reports its own status, and one bad row never sinks the batch. The response carries a `batch_id` and a `stats` block.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/sessions/bulk' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"operations":[{"action":"create","data":{"title":"Lightning: tool-calling traps"}},{"action":"update","id":"497f6eca-…","data":{"status":"accepted"}},{"action":"delete","id":"8f14e45f-…"}]}'
Example response
{
  "batch_id": "3f1c…",
  "results": [
    {
      "index": 0,
      "action": "create",
      "status": "success",
      "id": "…"
    },
    {
      "index": 1,
      "action": "update",
      "status": "error",
      "error": {
        "code": "not_found",
        "message": "Session not found."
      }
    }
  ],
  "stats": {
    "total": 2,
    "succeeded": 1,
    "failed": 1
  }
}
POST/v1/event/{eventId}/speakers/searchread:contacts

Search speakers

Speakers are a projection over contacts — anyone who is a participant on a live session of this event. `POST /v1/event/{eventId}/speakers` is the compatibility alias.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/speakers/search' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"filters":{"text":"okafor"},"page":1,"pageSize":25}'
Example response
{
  "results": [
    {
      "id": "…",
      "email": "rina@example.com",
      "full_name": "Rina Okafor",
      "company_name": "Company 1",
      "about": "…",
      "photo_url": null
    }
  ],
  "pagination": {
    "currentPage": 1,
    "pageSize": 25,
    "totalPages": 1,
    "totalResults": 8
  }
}
GET/v1/event/{eventId}/speakers/{contactId}read:contacts

Get one speaker

The contact record plus `session_ids` — the sessions this person is on, which is what every speaker-portal view needs.

curl -sS -X GET 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/speakers/CONTACT_ID' \
  -H 'x-access-token: $CALLBOARD_KEY'
GET/v1/event/{eventId}/{family}read:metadata

List a metadata family

`family` is one of `tracks`, `rooms`, `tags`, `formats`, `levels`. One generic handler serves all five; `POST` on the same path searches with `filters.text`.

curl -sS -X GET 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/tracks' \
  -H 'x-access-token: $CALLBOARD_KEY'
Example response
{
  "results": [
    {
      "id": "…",
      "event_id": "…",
      "name": "Agents",
      "order": 0,
      "color": "#329af0",
      "created_at": "…",
      "updated_at": "…"
    }
  ],
  "pagination": {
    "currentPage": 1,
    "pageSize": 25,
    "totalPages": 1,
    "totalResults": 3
  }
}
POST/v1/event/{eventId}/{family}/createwrite:metadata

Create a metadata row

`name` is required. `color` (tracks), `capacity` (rooms) and `default_minutes` (formats) are accepted only on the family that has the column.

curl -sS -X POST 'https://demo.callboardhq.com/v1/event/000000ev-0000-4000-8000-000000000001/tracks/create' \
  -H 'x-access-token: $CALLBOARD_KEY' \
  -H 'content-type: application/json' \
  -d '{"name":"Workshop Room 3","capacity":60}'

Notes worth reading before you integrate

  • Abstracts are sessions. One resource, discriminated by is_abstract, which is immutable after create. An abstract never becomes a session — it gets composed into one, and composition_status tells you which side you are looking at.
  • Metadata families share one handler: tracksroomstagsformatslevels— same paths, same envelope, same paging.
  • No read cache. Sessionboard serves reads up to three minutes stale after a write; a read here always reflects the last write, so read-after-write tests behave.
  • Sorting is not restricted to timestamps. Their API sorts only by createdAt/updatedAt; this one also accepts startsAt and title.
  • Optimistic concurrency is opt-in. Send the updated_at you read on a PUT and a concurrent write gives you 409; omit it to force the write.