Content Sections
The rich content-section blocks used by sessions and sponsors.
Sessions and sponsors can carry a content collection (contentData) of ordered rich blocks, the same building blocks the console editor offers. Send it as an ordered array of { type, data } sections; the API assigns the order and stores it.
"contentData": [
{ "type": "title", "data": { "content": "About Us" } },
{ "type": "subSession", "data": { "content": "Case 1 & Discussion", "startTime": "09:22", "endTime": "09:37" } },
{ "type": "paragraph", "data": { "content": "[{\"insert\":\"We build things.\\n\"}]" } },
{ "type": "image", "data": { "content": "https://example.com/logo.png" } },
{ "type": "imageText", "data": { "content": "[{\"insert\":\"Beside the picture.\\n\"}]", "imageUrl": "https://example.com/photo.jpg", "imageWidth": 40, "imagePosition": "left" } },
{ "type": "webEmbed", "data": { "content": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "embedHeight": 400, "embedTitle": "Welcome video" } },
{ "type": "button", "data": { "content": { "buttonText": "Visit", "buttonUrl": "https://example.com" } } },
{ "type": "sponsor", "data": { "content": "<sponsorId>" } },
{ "type": "speaker", "data": { "speakerType": "Speakers", "speakerIds": ["<speakerId>"] } }
]
Section types
type |
data shape |
Notes |
|---|---|---|
title |
{ "content": "<heading text>" } |
Plain-text heading |
subSession |
{ "content": "<heading text>", "startTime": "...", "endTime": "..." } |
A timed block inside a session. Times are free text. See below. |
paragraph |
{ "content": "<Delta JSON string>" } |
Rich text - see Rich Text Format. Plain text is accepted too. |
image |
{ "content": "<image url>" } |
|
imageText |
{ "content": "<Delta JSON string>", "imageUrl": "<image url>", "imageWidth": 30|40|50|60|70, "imagePosition": "left"|"right" } |
Rich text beside an image. Both halves optional, but not both empty. See below. |
webEmbed |
{ "content": "<https url>", "embedHeight": 100-2000, "embedTitle": "..." } |
A third-party page in a frame. Share links are converted for you. See below. |
button |
{ "content": { "buttonText": "...", "buttonUrl": "..." } } |
|
sponsor |
{ "content": "<sponsorId>" } |
Reference to a sponsor in this event |
speaker |
{ "speakerType": "<label>", "speakerIds": ["<id>", ...] } |
References to speakers in this event |
menuShortcuts |
{} |
A grid of buttons linking to the event’s own pages. Carries no data. App only. See below. |
abstract |
{ "content": "<abstractId>", "label": "..." } |
A button opening one abstract’s page. App only. See below. |
App Shortcuts sections
menuShortcuts is the one section type with no data of its own. Its data is
always {}; anything you send in it is discarded rather than stored, so it cannot
come back on a read looking like a setting that does something.
The buttons are derived, not authored. The mobile app builds them from that event’s own menu, leaving out any page switched off in Event Settings and any page already in the bottom bar - so the block adapts as the event is configured, with nothing to keep in step.
It is intended for an event’s welcome content, and the console offers it only there. Nothing rejects it elsewhere, but a session or sponsor page is not where an attendee looks for app navigation.
It renders in the mobile app only. The published website widgets ignore it, having no app menu to derive buttons from - a page built from this content will simply not show the block.
Abstract sections
An abstract section renders a button that opens one abstract’s page in the mobile
app: put one on a session’s app content to link the session to the paper being
presented in it. content is the abstract’s id, taken from
/getAbstracts; it must exist on the event or the write is
rejected. label is the optional button text and defaults to “View Abstract”.
Reads also return an abstractTitle, filled in from the abstract record so you can
see what a section points at. It is not writable: whatever you send there is replaced
with the live title.
Like App Shortcuts, it renders in the mobile app only: the button opens an app screen, so the published website widgets and the web preview leave the block out. If the abstract it points at is later deleted or unpublished, the app hides the button rather than showing a dead one.
Sub-session sections
A subSession section marks a timed block inside a session - a case discussion inside a workshop, a talk inside a plenary. It carries its own time and heading, and every surface draws the two as one block with extra space above it, so a run of sub-sessions reads as a sequence of groups rather than as one flat stream of headings.
This is what a time-and-a-title used to be built from by hand: a paragraph holding the time, a blank line, then a title. Nothing in a flat section list said those two belonged together, so no renderer could group them. Use subSession instead - the paragraph-plus-title arrangement still works and is not deprecated, it just cannot be grouped.
startTime and endTime are free text, not timestamps. The session itself holds the real schedule; these are the labels an organizer copies out of a printed programme, so "09:22", "09:00 CET" and "Morning" are all accepted as sent. Anything shown to attendees is exactly what you send, joined with " - " when both are present.
Omit endTime for a single point in time rather than a range. An endTime sent without a startTime is stored as the start and reported in warnings: a lone time is displayed as a start on every surface, so keeping it under the other key would render as something other than what you sent.
content is the heading, plain text like a title. At least one of the three fields must be non-empty; a section with all three blank is rejected, since it would be invisible everywhere.
Image + text sections
An imageText section puts rich text beside an image. content is a Delta JSON string exactly like a paragraph, and imageUrl is an image url exactly like an image.
imageWidth is the image’s share of the block, in percent, and must be one of 30, 40, 50, 60, 70. imagePosition is "left" or "right" and says which side of the text the image sits on. Both default to 40 and "left" if you omit them. A value outside those steps is corrected rather than rejected: it is stored as the default and reported in warnings, because a width outside the range starves one of the two columns on every surface that renders it.
Either half may be empty - text with no image, or an image with no text - and the surviving half then takes the full width. A section with neither is rejected, since it would be invisible everywhere and is far more likely a mistake than an intention.
The block is rendered side by side at every screen size, including on phones. The proportion you set is the proportion attendees see; no surface re-stacks it on its own.
Web embed sections
A webEmbed section puts a third-party page inside the content. content must be an https URL; http is rejected, because a plain-text frame is blocked as mixed content in the browser and refused outright by the app.
Share links are converted for you. Sites hand out a link meant for viewing, not for embedding, and those refuse to be framed - so the block would come up blank with nothing to explain why. A YouTube watch?v=, youtu.be, /shorts/ or /live/ link becomes its /embed/ form, a vimeo.com/<id> page becomes its player URL (keeping the privacy hash an unlisted video needs), and a Google Maps URL gains output=embed. The stored value is the converted one, and the conversion is reported in warnings so you can see it happened. A URL already in embeddable form, or from any other provider, is stored exactly as you sent it.
embedHeight is the height in pixels, between 100 and 2000, defaulting to 400. It does not apply to video: a YouTube or Vimeo player is sized 16:9 on every surface, because giving a player a height rather than a ratio letterboxes it with black on a narrow screen. A value outside that range is clamped and reported in warnings rather than rejected. embedTitle is optional: it is read out by screen readers and used as the link text on the website.
The height means different things on different surfaces, and this is not something the API can paper over. On the website and in the console preview it is the height, because a cross-origin frame cannot be measured from the page around it. In the app it is a minimum: the app loads the page in a real browser view, asks it how tall it is, and grows the block to fit, so the embed scrolls with the page instead of scrolling inside itself.
Not every URL can be embedded everywhere. A site can refuse to be framed (X-Frame-Options, CSP frame-ancestors), which affects the website widget and the console preview but not the app, whose webview is a real browser rather than a frame. So a block can work on a phone and be blank on the website. The website always shows an “open in a new tab” link under the frame, because a page that refuses framing renders its own error inside the frame and no surrounding page can detect that to react to it.
References are validated
For sponsor and speaker sections, every id must reference a record in the same event that is not hidden or deleted. If any reference is missing, hidden, or deleted, the whole write is rejected with 400 and a details list naming the offending section and id - nothing is saved. An unpublished speaker or sponsor (Invited / unListed:true) can still be referenced - only deleted or hidden records are rejected.
For speaker sections you send speakerIds (an array); the API looks up each speaker and stores the speakerList map ({ id: name }) with the names filled in for you, so the stored names always match the speaker records. A section read from the API carries speakerList as well, and sending that back instead of speakerIds also works - the ids are what is read, and the names are re-resolved either way, so you never have to keep them in step. If you send both, speakerIds wins.
speakerType is the label shown above the group (e.g. “Chair”, “Keynote Speaker”). It is free text, but case is not meaningful: a label matching one of the usual ones is stored in its normal spelling, so a SPEAKER arriving from a spreadsheet export does not end up shouting at attendees. Anything else is stored exactly as you sent it and reported in warnings, so a typo is still visible. GET /apiSchema lists the recognised labels under formats.contentSections.speakerTypes.
To feature speakers on a session, add speaker sections to its contentData - there is no separate speakers field to set.
What you get back
On read (getSessions / getSponsors), contentData comes back as the same ordered array you send - so a section you read can be posted straight back without reshaping it. Speaker sections carry both speakerList (id to name, resolved for you) and speakerIds (the array the write side takes):
"contentData": [
{ "type": "title", "data": { "content": "About Us" } },
{
"type": "speaker",
"data": {
"speakerType": "Speakers",
"speakerList": { "<id>": "Jane Doe" },
"speakerIds": ["<id>"]
}
}
]
Sending a new contentData replaces the whole collection, so removing a section from the array removes it from the record.