curl --request POST \
--url https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions \
--header 'Content-Type: application/json' \
--data '
{
"session_number": 2,
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"encounter_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
]
}
'import requests
url = "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions"
payload = {
"session_number": 2,
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"encounter_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"]
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
session_number: 2,
title: '<string>',
synopsis: '<string>',
project_context: '<string>',
npc_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
encounter_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a']
})
};
fetch('https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'session_number' => 2,
'title' => '<string>',
'synopsis' => '<string>',
'project_context' => '<string>',
'npc_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'encounter_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions"
payload := strings.NewReader("{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
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))
}HttpResponse<String> response = Unirest.post("https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions")
.header("Content-Type", "application/json")
.body("{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"session_number": 123,
"project_id": "<string>",
"external_id": "<string>",
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"encounter_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"status": "brainstorm",
"series_id": "<string>",
"created_at": "2023-11-07T05:31:56Z"
}{
"error": "Forbidden",
"code": "forbidden",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "Your API key does not have the required scope.",
"doc_url": "https://docs.kitefrost.ai/errors/forbidden"
}{
"error": "Not found",
"code": "not_found",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "The requested resource does not exist.",
"doc_url": "https://docs.kitefrost.ai/errors/not_found"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"error": "Internal server error",
"code": "internal_error",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "An unexpected error occurred.",
"doc_url": "https://docs.kitefrost.ai/errors/internal_error"
}Create session
Create a TTRPG session with an optional title + synopsis, linking NPCs and encounters for a specific session number. Starts in brainstorm status. Use /sessions/{session_id}/generate to generate narrative content.
curl --request POST \
--url https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions \
--header 'Content-Type: application/json' \
--data '
{
"session_number": 2,
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"encounter_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
]
}
'import requests
url = "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions"
payload = {
"session_number": 2,
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"encounter_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"]
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
session_number: 2,
title: '<string>',
synopsis: '<string>',
project_context: '<string>',
npc_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
encounter_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a']
})
};
fetch('https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'session_number' => 2,
'title' => '<string>',
'synopsis' => '<string>',
'project_context' => '<string>',
'npc_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'encounter_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions"
payload := strings.NewReader("{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
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))
}HttpResponse<String> response = Unirest.post("https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions")
.header("Content-Type", "application/json")
.body("{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.kitefrost.ai/v1/projects/{project_id}/ttrpg-gm/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"session_number\": 2,\n \"title\": \"<string>\",\n \"synopsis\": \"<string>\",\n \"project_context\": \"<string>\",\n \"npc_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"encounter_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"session_number": 123,
"project_id": "<string>",
"external_id": "<string>",
"title": "<string>",
"synopsis": "<string>",
"project_context": "<string>",
"npc_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"encounter_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"status": "brainstorm",
"series_id": "<string>",
"created_at": "2023-11-07T05:31:56Z"
}{
"error": "Forbidden",
"code": "forbidden",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "Your API key does not have the required scope.",
"doc_url": "https://docs.kitefrost.ai/errors/forbidden"
}{
"error": "Not found",
"code": "not_found",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "The requested resource does not exist.",
"doc_url": "https://docs.kitefrost.ai/errors/not_found"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"error": "Internal server error",
"code": "internal_error",
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"message": "An unexpected error occurred.",
"doc_url": "https://docs.kitefrost.ai/errors/internal_error"
}Body
POST /v1/projects/{project_id}/sessions
title is required (2026-05-16, migration 094). session_number
alone is not a unique identifier - there is no UNIQUE constraint on
(project_id, session_number) and series_id is auto-generated per
session today, so two Game Nights both numbered "1" are valid state.
Without a title the dashboard list shows two indistinguishable rows.
x >= 1User-facing session name
1 - 200Short prose summary of the session
500050000NPCs to include
Encounters to include
Response
Session created
Session resource representation.
Carries the full mutable field set so the create response and the
detail GET both round-trip what the edit form needs. project_context
/ npc_ids / encounter_ids were added 2026-05-14 - before that the
response omitted them and the dashboard edit form had nothing to read.
Human-readable slug for the URL (/projects/<slug>/sessions/<external_id>). Derived from title (or session-<N> fallback) at create time and immutable. May be null on legacy rows pre-migration 102.