Make draft courses from a meeting
US Region: https://public-api.sessionboard.com · EU Region: https://public-api-eu.sessionboard.com
Turns the event's sessions into draft courses, gathered into one course for the meeting ("includeWithoutRecording, sessions without one become drafts that wait for a recording. Up to 60 sessions per call: remaining says how many are left, so call again until it is 0. Safe to repeat: a session that is already a course is never drafted twice, a waiting draft whose session now has a recording gets it (attached), and a draft nobody edited takes the session's new title and description (refreshed). With dryRun it only counts. Nothing is published and no one is notified; publishing is Publish a meeting on demand. Learners keep the credit they already claimed in the room. The token's user needs the Lessons create permission. Requires write:learning; refused for a key restricted to particular events. Also at POST /v1/learning/workspace/on-demand/{eventId}.
Request example
curl --request POST \ --url 'https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}' \ --header 'x-access-token: <api-token>' \ --header 'Content-Type: application/json' \ --data '{ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": true, "dryRun": false}'const response = await fetch('https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}', { method: 'POST', headers: { "x-access-token": "<api-token>", "Content-Type": "application/json" }, body: JSON.stringify({ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": true, "dryRun": false }),});
const data = await response.json();console.log(data);import requests
response = requests.request( "POST", "https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}", headers={ "x-access-token": "<api-token>", "Content-Type": "application/json" }, json={ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": True, "dryRun": False },)
print(response.json())<?php
$curl = curl_init();
curl_setopt_array($curl, [ CURLOPT_URL => "https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}", CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "POST", CURLOPT_POSTFIELDS => '{ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": true, "dryRun": false}', CURLOPT_HTTPHEADER => [ "x-access-token: <api-token>", "Content-Type: application/json", ],]);
$response = curl_exec($curl);curl_close($curl);
echo $response;package main
import ( "fmt" "io" "net/http" "strings")
func main() { req, _ := http.NewRequest("POST", "https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}", strings.NewReader(`{ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": true, "dryRun": false}`)) req.Header.Add("x-access-token", "<api-token>") req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))}HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://public-api.sessionboard.com/v1/organization/{orgId}/learning/workspace/on-demand/{eventId}")) .header("x-access-token", "<api-token>") .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString("{\n \"sessionIds\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"includeWithoutRecording\": true,\n \"dryRun\": false\n}")) .build();
HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());Try it
Response headers
Authorizations
x-access-tokenstringheaderOrganization API token. Generate from Organization Settings → API Tokens.
AuthorizationBearer <token>headerorOAuth 2.1 access token. Obtained via the OAuth PKCE flow at
/oauth/token.
Send one of these. See Authentication.
Path parameters
orgIdstringpathrequiredYour organization ID. Any other organization returns
404.eventIdintegerpathrequiredThe Sessionboard event. Another organization's event answers
401.- Minimum
1
Request body
sessionIdsstring<uuid>[]Only these sessions. Omit for every session of the event.
- Max items
500
includeWithoutRecordingbooleanAlso draft sessions with no recording yet, as courses waiting for one.
dryRunbooleanCount what would be made; write nothing.
- Default
false
{ "sessionIds": [ "3c90c3cc-0d44-4b50-8888-8dd25736052a" ], "includeWithoutRecording": true, "dryRun": false}Responses
201application/json
Drafted (or, with dryRun, counted). A dry run answers LearningOnDemandDryRun; otherwise LearningOnDemandMakeResult. When every session is already a course, created is 0 and message says so.
successbooleanpayloadLearningOnDemandDryRun | LearningOnDemandMakeResultdry_run: true (LearningOnDemandDryRun)
dry_runbooleaneventLearningEventRefShow child attributes
idintegernamestring
would_createintegerwould_wait_for_recordingintegerbatchintegerSessions drafted per call (60).
Option 2 (LearningOnDemandMakeResult)
eventLearningEventRefShow child attributes
idintegernamestring
createdintegerwaiting_for_recordingintegerOf the courses made, how many wait for a recording. Absent when
createdis0.attachedintegerWaiting drafts that got their recording.
refreshedintegerUntouched drafts that took the session's new title and description.
remainingintegerSessions left; call again until
0.series_idstring<uuid> | nullThe meeting's course ("
on demand"). nullwhen nothing was made.coursesobject[]The draft courses made. Absent when
createdis0.Show child attributes
idstring<uuid>titlestringstatusstring
messagestring
400application/json
The body failed validation, eventId is not a whole number, or No courses could be made from those sessions.
Gateway errors carry error (a code) and message. Errors from Learning itself carry status, success: false, message, name, and sometimes code (and feature or paused_at).
errorstring | objectThe gateway's error code (
SCOPE_REQUIRED,ORG_WIDE_TOKEN_REQUIRED,NOT_FOUND, …).messagestringstatusintegersuccessbooleannamestringcodestringFEATURE_NOT_ENABLED,AGENT_WRITES_PAUSED, …featurestringpaused_atstring<date-time>
401
Missing, invalid or expired token, or the event does not belong to your organization (Event does not belong to your organization).
403application/json
As for every Learning workspace operation:
SCOPE_REQUIRED— the token lacksread:learning(reads) orwrite:learning(writes). Legacy tokens with no scopes cannot write.ORG_WIDE_TOKEN_REQUIRED— the token is restricted to particular events: "The Learning workspace covers every event and learner in the organization, so this API key (restricted to particular events) cannot use it. Use a key without an event restriction."EVENT_ARCHIVED— the event is archived.FEATURE_NOT_ENABLED(feature: lessons_credits) — Learning is not on for the organization.FORBIDDEN— the token's user is not an active member of the organization, or lacks the Lessons permission the operation needs.
Gateway errors carry error (a code) and message. Errors from Learning itself carry status, success: false, message, name, and sometimes code (and feature or paused_at).
errorstring | objectThe gateway's error code (
SCOPE_REQUIRED,ORG_WIDE_TOKEN_REQUIRED,NOT_FOUND, …).messagestringstatusintegersuccessbooleannamestringcodestringFEATURE_NOT_ENABLED,AGENT_WRITES_PAUSED, …featurestringpaused_atstring<date-time>
404application/json
orgId is not your organization (error: NOT_FOUND, message: Organization not found).
Gateway errors carry error (a code) and message. Errors from Learning itself carry status, success: false, message, name, and sometimes code (and feature or paused_at).
errorstring | objectThe gateway's error code (
SCOPE_REQUIRED,ORG_WIDE_TOKEN_REQUIRED,NOT_FOUND, …).messagestringstatusintegersuccessbooleannamestringcodestringFEATURE_NOT_ENABLED,AGENT_WRITES_PAUSED, …featurestringpaused_atstring<date-time>
423application/json
A person has paused connected assistants for the organization or the event (code: AGENT_WRITES_PAUSED). Ask them to resume it in Sessionboard before trying again.
Gateway errors carry error (a code) and message. Errors from Learning itself carry status, success: false, message, name, and sometimes code (and feature or paused_at).
errorstring | objectThe gateway's error code (
SCOPE_REQUIRED,ORG_WIDE_TOKEN_REQUIRED,NOT_FOUND, …).messagestringstatusintegersuccessbooleannamestringcodestringFEATURE_NOT_ENABLED,AGENT_WRITES_PAUSED, …featurestringpaused_atstring<date-time>
429application/json
- The token's request limit (100 requests per 15 minutes per bucket by default — see Rate Limiting). Wait
Retry-Afterseconds. QUOTA_EXCEEDED(writes) — the token's daily write quota (10,000 a day). Resets at midnight UTC.
Gateway errors carry error (a code) and message. Errors from Learning itself carry status, success: false, message, name, and sometimes code (and feature or paused_at).
errorstring | objectThe gateway's error code (
SCOPE_REQUIRED,ORG_WIDE_TOKEN_REQUIRED,NOT_FOUND, …).messagestringstatusintegersuccessbooleannamestringcodestringFEATURE_NOT_ENABLED,AGENT_WRITES_PAUSED, …featurestringpaused_atstring<date-time>
Retry-AfterintegerheaderSeconds to wait before retrying.
Response examples
{ "success": true, "payload": { "dry_run": true, "event": { "id": 123, "name": "<string>" }, "would_create": 123, "would_wait_for_recording": 123, "batch": 123 }}{ "error": "<string>", "message": "<string>", "status": 123, "success": true, "name": "<string>", "code": "<string>", "feature": "<string>", "paused_at": "2026-01-15T09:30:00Z"}{ "error": "<string>", "message": "<string>", "status": 123, "success": true, "name": "<string>", "code": "<string>", "feature": "<string>", "paused_at": "2026-01-15T09:30:00Z"}{ "error": "NOT_FOUND", "message": "Organization not found"}{ "status": 423, "success": false, "message": "A person has paused connected assistants in Sessionboard. Ask them to resume it from their workspace before trying again.", "name": "AgentWritesPausedError", "code": "AGENT_WRITES_PAUSED", "paused_at": "2026-11-06T15:04:05.000Z"}{ "error": "<string>", "message": "<string>", "status": 123, "success": true, "name": "<string>", "code": "<string>", "feature": "<string>", "paused_at": "2026-01-15T09:30:00Z"}