API Documentation
REST API reference for integrating with Cipher & Scrollbook (Multi-Campaign tier and above)
The Cipher API allows you to programmatically interact with campaigns, characters, sessions, and content. API access is available for Multi-Campaign tier and above.
Getting Started
Base URL
https://api.scrollbook.app/v1
Authentication
All API requests require authentication using a JWT token.
Getting Your API Token:
- Log in to scrollbook.app
- Go to Settings → API Access
- Click "Generate API Token"
- Copy and securely store your token
Using the Token:
Include in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
Example Request:
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://api.scrollbook.app/v1/campaigns
Rate Limits
Rate limits vary by tier:
| Tier | Requests per Minute | Requests per Hour |
|---|---|---|
| Multi-Campaign | 60 | 1,000 |
| Unlimited Guild | 120 | 5,000 |
| Enterprise | Custom | Custom |
Rate Limit Headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
Rate Limit Exceeded:
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 30 seconds.",
"retry_after": 30
}
Campaigns
List Campaigns
Get all campaigns you have access to.
Endpoint:
GET /campaigns
Query Parameters:
guild_id(optional): Filter by Discord guild IDstatus(optional): Filter by status (active,archived,completed)limit(optional): Number of results (default: 50, max: 100)offset(optional): Pagination offset
Request:
GET /campaigns?guild_id=123456789&status=active&limit=10
Response:
{
"data": [
{
"id": "campaign_abc123",
"name": "The Lost Mines of Phandelver",
"description": "A classic adventure in the Forgotten Realms",
"guild_id": "123456789",
"dm_user_id": "user_xyz789",
"era": "forgotten_realms",
"tone": "serious",
"starting_level": 1,
"current_session": 12,
"status": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2025-01-10T15:45:00Z",
"settings": {
"xp_system": "milestone",
"encumbrance": "variant",
"homebrew_allowed": true
}
}
],
"pagination": {
"total": 3,
"limit": 10,
"offset": 0,
"has_more": false
}
}
Get Campaign
Get details of a specific campaign.
Endpoint:
GET /campaigns/{campaign_id}
Request:
GET /campaigns/campaign_abc123
Response:
{
"id": "campaign_abc123",
"name": "The Lost Mines of Phandelver",
"description": "A classic adventure in the Forgotten Realms",
"guild_id": "123456789",
"dm_user_id": "user_xyz789",
"era": "forgotten_realms",
"tone": "serious",
"starting_level": 1,
"current_session": 12,
"status": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2025-01-10T15:45:00Z",
"settings": {
"xp_system": "milestone",
"encumbrance": "variant",
"homebrew_allowed": true,
"content_rating": "pg13"
},
"statistics": {
"total_sessions": 12,
"total_players": 5,
"ai_hours_used": 24.5,
"total_xp_awarded": 18000
}
}
Create Campaign
Create a new campaign.
Endpoint:
POST /campaigns
Request Body:
{
"name": "Dragon Heist",
"description": "Waterdeep adventure",
"guild_id": "123456789",
"era": "forgotten_realms",
"tone": "lighthearted",
"starting_level": 1,
"settings": {
"xp_system": "traditional",
"encumbrance": "none",
"homebrew_allowed": false,
"max_players": 6
}
}
Response:
{
"id": "campaign_def456",
"name": "Dragon Heist",
"created_at": "2025-01-15T10:00:00Z",
"message": "Campaign created successfully"
}
Update Campaign
Update campaign details.
Endpoint:
PATCH /campaigns/{campaign_id}
Request Body:
{
"description": "Updated description",
"tone": "serious",
"settings": {
"homebrew_allowed": true
}
}
Response:
{
"id": "campaign_abc123",
"message": "Campaign updated successfully",
"updated_at": "2025-01-15T11:00:00Z"
}
Delete Campaign
Permanently delete a campaign.
Endpoint:
DELETE /campaigns/{campaign_id}
Response:
{
"message": "Campaign deleted successfully"
}
Characters
List Characters
Get all characters you have access to.
Endpoint:
GET /characters
Query Parameters:
campaign_id(optional): Filter by campaignuser_id(optional): Filter by ownerlimit(optional): Number of resultsoffset(optional): Pagination offset
Request:
GET /characters?campaign_id=campaign_abc123&limit=10
Response:
{
"data": [
{
"id": "char_123abc",
"name": "Thorin Ironforge",
"campaign_id": "campaign_abc123",
"user_id": "user_xyz789",
"class_name": "Fighter",
"subclass_name": "Champion",
"species_name": "Dwarf",
"level": 5,
"experience_points": 6500,
"hit_points_current": 38,
"hit_points_max": 43,
"armor_class": 18,
"created_at": "2024-01-20T14:00:00Z",
"updated_at": "2025-01-10T16:30:00Z"
}
],
"pagination": {
"total": 5,
"limit": 10,
"offset": 0,
"has_more": false
}
}
Get Character
Get full character sheet data.
Endpoint:
GET /characters/{character_id}
Request:
GET /characters/char_123abc
Response:
{
"id": "char_123abc",
"name": "Thorin Ironforge",
"campaign_id": "campaign_abc123",
"user_id": "user_xyz789",
"class_name": "Fighter",
"subclass_name": "Champion",
"species_name": "Dwarf",
"background_name": "Soldier",
"level": 5,
"experience_points": 6500,
"hit_points_current": 38,
"hit_points_max": 43,
"hit_points_temp": 0,
"armor_class": 18,
"proficiency_bonus": 3,
"speed": 25,
"initiative": 1,
"ability_scores": {
"strength": 16,
"dexterity": 12,
"constitution": 14,
"intelligence": 10,
"wisdom": 11,
"charisma": 8
},
"saving_throws": {
"strength": 6,
"dexterity": 1,
"constitution": 5,
"intelligence": 0,
"wisdom": 0,
"charisma": -1
},
"skills": {
"athletics": 6,
"intimidation": 2,
"perception": 3,
"survival": 0
},
"inventory": [
{
"id": "item_001",
"name": "Greataxe",
"type": "weapon",
"quantity": 1,
"equipped": true,
"weight": 7
}
],
"features": [
{
"name": "Second Wind",
"source": "Fighter",
"description": "Regain 1d10 + 5 HP as a bonus action",
"uses_current": 0,
"uses_max": 1
}
],
"created_at": "2024-01-20T14:00:00Z",
"updated_at": "2025-01-10T16:30:00Z"
}
Create Character
Create a new character.
Endpoint:
POST /characters
Request Body:
{
"name": "Aria Moonwhisper",
"campaign_id": "campaign_abc123",
"class_name": "Wizard",
"species_name": "Elf",
"background_name": "Sage",
"level": 1,
"ability_scores": {
"strength": 8,
"dexterity": 14,
"constitution": 12,
"intelligence": 16,
"wisdom": 10,
"charisma": 13
}
}
Response:
{
"id": "char_456def",
"name": "Aria Moonwhisper",
"created_at": "2025-01-15T12:00:00Z",
"message": "Character created successfully"
}
Update Character
Update character attributes.
Endpoint:
PATCH /characters/{character_id}
Request Body:
{
"hit_points_current": 35,
"experience_points": 7000,
"level": 6
}
Response:
{
"id": "char_123abc",
"message": "Character updated successfully",
"updated_at": "2025-01-15T13:00:00Z"
}
Delete Character
Permanently delete a character.
Endpoint:
DELETE /characters/{character_id}
Response:
{
"message": "Character deleted successfully"
}
Sessions
List Sessions
Get session history.
Endpoint:
GET /sessions
Query Parameters:
campaign_id(required): Campaign IDstatus(optional): Filter by status (active,paused,completed)limit(optional): Number of resultsoffset(optional): Pagination offset
Request:
GET /sessions?campaign_id=campaign_abc123&status=completed&limit=5
Response:
{
"data": [
{
"id": "session_789ghi",
"campaign_id": "campaign_abc123",
"session_number": 12,
"title": "Into the Dragon's Lair",
"status": "completed",
"started_at": "2025-01-10T18:00:00Z",
"ended_at": "2025-01-10T22:00:00Z",
"duration_minutes": 240,
"ai_hours_used": 2.3,
"participants": [
{
"user_id": "user_xyz789",
"character_id": "char_123abc",
"character_name": "Thorin Ironforge",
"attended": true
}
],
"summary": "The party descended into the dragon's lair...",
"xp_awarded": 1500,
"created_at": "2025-01-10T18:00:00Z"
}
],
"pagination": {
"total": 12,
"limit": 5,
"offset": 0,
"has_more": true
}
}
Get Session
Get full session details including transcript.
Endpoint:
GET /sessions/{session_id}
Request:
GET /sessions/session_789ghi
Response:
{
"id": "session_789ghi",
"campaign_id": "campaign_abc123",
"session_number": 12,
"title": "Into the Dragon's Lair",
"status": "completed",
"started_at": "2025-01-10T18:00:00Z",
"ended_at": "2025-01-10T22:00:00Z",
"duration_minutes": 240,
"ai_hours_used": 2.3,
"participants": [...],
"summary": "The party descended into the dragon's lair...",
"highlights": [
"Thorin's critical hit on the dragon",
"Aria's clever use of Misty Step",
"Discovery of the ancient artifact"
],
"npcs_encountered": [
{
"name": "Smaug the Terrible",
"role": "Ancient Red Dragon"
}
],
"loot_acquired": [
{
"name": "Dragonscale Armor",
"rarity": "very_rare"
}
],
"quests_updated": [
{
"quest_id": "quest_001",
"quest_name": "Slay the Dragon",
"status": "completed"
}
],
"xp_awarded": 1500,
"transcript": "Full session transcript...",
"created_at": "2025-01-10T18:00:00Z"
}
Start Session
Start a new session (DM only).
Endpoint:
POST /sessions
Request Body:
{
"campaign_id": "campaign_abc123",
"title": "The Tomb of Horrors",
"participant_character_ids": [
"char_123abc",
"char_456def"
]
}
Response:
{
"id": "session_012jkl",
"session_number": 13,
"status": "active",
"started_at": "2025-01-15T18:00:00Z",
"message": "Session started successfully"
}
End Session
End an active session (DM only).
Endpoint:
POST /sessions/{session_id}/end
Request Body:
{
"summary": "Optional brief summary",
"xp_awarded": 1200
}
Response:
{
"id": "session_012jkl",
"status": "completed",
"ended_at": "2025-01-15T22:00:00Z",
"duration_minutes": 240,
"ai_hours_used": 2.1,
"message": "Session ended successfully"
}
Content (SRD)
List Spells
Get SRD spell list.
Endpoint:
GET /content/spells
Query Parameters:
level(optional): Filter by spell level (0-9)school(optional): Filter by school (evocation,transmutation, etc.)class(optional): Filter by class (wizard,cleric, etc.)search(optional): Search by namelimit(optional): Number of resultsoffset(optional): Pagination offset
Request:
GET /content/spells?level=3&school=evocation&limit=10
Response:
{
"data": [
{
"id": "spell_fireball",
"name": "Fireball",
"level": 3,
"school": "Evocation",
"casting_time": "1 action",
"range": "150 feet",
"components": ["V", "S", "M"],
"material": "a tiny ball of bat guano and sulfur",
"duration": "Instantaneous",
"concentration": false,
"ritual": false,
"description": "A bright streak flashes from your pointing finger...",
"higher_levels": "When you cast this spell using a spell slot of 4th level or higher...",
"classes": ["Sorcerer", "Wizard"]
}
],
"pagination": {
"total": 45,
"limit": 10,
"offset": 0,
"has_more": true
}
}
Get Spell
Get detailed spell information.
Endpoint:
GET /content/spells/{spell_id}
List Monsters
Get SRD monster list.
Endpoint:
GET /content/monsters
Query Parameters:
cr(optional): Filter by challenge ratingtype(optional): Filter by type (dragon,undead, etc.)size(optional): Filter by size (tiny,small,medium, etc.)search(optional): Search by namelimit(optional): Number of resultsoffset(optional): Pagination offset
Request:
GET /content/monsters?type=dragon&cr=10&limit=5
Response:
{
"data": [
{
"id": "monster_adult_red_dragon",
"name": "Adult Red Dragon",
"size": "Huge",
"type": "Dragon",
"alignment": "Chaotic Evil",
"challenge_rating": 17,
"armor_class": 19,
"hit_points": 256,
"speed": {
"walk": 40,
"fly": 80
},
"ability_scores": {
"strength": 27,
"dexterity": 10,
"constitution": 25,
"intelligence": 16,
"wisdom": 13,
"charisma": 21
}
}
],
"pagination": {
"total": 3,
"limit": 5,
"offset": 0,
"has_more": false
}
}
Get Monster
Get detailed monster stat block.
Endpoint:
GET /content/monsters/{monster_id}
Webhooks
Create Webhook
Register a webhook to receive events.
Endpoint:
POST /webhooks
Request Body:
{
"url": "https://your-server.com/webhook",
"events": [
"session.started",
"session.ended",
"character.created",
"character.level_up"
],
"secret": "your_webhook_secret"
}
Response:
{
"id": "webhook_123",
"url": "https://your-server.com/webhook",
"events": ["session.started", "session.ended"],
"created_at": "2025-01-15T10:00:00Z",
"message": "Webhook created successfully"
}
Webhook Events
Available webhook events:
Campaign Events:
campaign.createdcampaign.updatedcampaign.deleted
Character Events:
character.createdcharacter.updatedcharacter.deletedcharacter.level_up
Session Events:
session.startedsession.pausedsession.resumedsession.ended
Example Webhook Payload:
{
"event": "session.ended",
"timestamp": "2025-01-15T22:00:00Z",
"data": {
"session_id": "session_012jkl",
"campaign_id": "campaign_abc123",
"session_number": 13,
"duration_minutes": 240,
"ai_hours_used": 2.1,
"xp_awarded": 1200
}
}
Error Responses
All error responses follow this format:
{
"error": "error_code",
"message": "Human-readable error message",
"details": {
"field": "Additional context if applicable"
}
}
Common Error Codes:
| Code | Status | Description |
|---|---|---|
unauthorized | 401 | Invalid or missing API token |
forbidden | 403 | Insufficient permissions |
not_found | 404 | Resource not found |
validation_error | 422 | Invalid request data |
rate_limit_exceeded | 429 | Too many requests |
internal_error | 500 | Server error |
SDKs & Libraries
Official SDKs
Python:
pip install cipher-api
from cipher_api import CipherClient
client = CipherClient(api_token="YOUR_API_TOKEN")
# List campaigns
campaigns = client.campaigns.list(status="active")
# Get character
character = client.characters.get("char_123abc")
# Start session
session = client.sessions.create(
campaign_id="campaign_abc123",
title="The Tomb of Horrors"
)
JavaScript/Node.js:
npm install @cipher/api
import { CipherAPI } from '@cipher/api';
const cipher = new CipherAPI('YOUR_API_TOKEN');
// List campaigns
const campaigns = await cipher.campaigns.list({ status: 'active' });
// Get character
const character = await cipher.characters.get('char_123abc');
// Start session
const session = await cipher.sessions.create({
campaignId: 'campaign_abc123',
title: 'The Tomb of Horrors'
});
Community SDKs
- Ruby:
cipher-ruby(community-maintained) - PHP:
cipher-php(community-maintained) - Go:
cipher-go(community-maintained)
Use Cases
Integration Examples
1. Discord Bot Integration
Create a custom Discord bot that fetches character data:
import discord
from cipher_api import CipherClient
cipher = CipherClient(api_token=API_TOKEN)
@bot.command()
async def stats(ctx, character_name):
# Find character
characters = cipher.characters.list(
search=character_name
)
if characters:
char = characters[0]
await ctx.send(f"{char.name} - Level {char.level} {char.class_name}")
2. Analytics Dashboard
Build custom analytics on top of Cipher data:
// Fetch session data
const sessions = await cipher.sessions.list({
campaignId: 'campaign_abc123',
limit: 100
});
// Calculate metrics
const totalHours = sessions.reduce(
(sum, s) => sum + s.ai_hours_used, 0
);
const avgDuration = sessions.reduce(
(sum, s) => sum + s.duration_minutes, 0
) / sessions.length;
3. Automated Backups
Schedule regular data exports:
#!/bin/bash
# backup-campaigns.sh
curl -H "Authorization: Bearer $API_TOKEN" \
https://api.scrollbook.app/v1/campaigns > campaigns.json
curl -H "Authorization: Bearer $API_TOKEN" \
https://api.scrollbook.app/v1/characters > characters.json
4. Webhooks for Notifications
Send Discord notifications on events:
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'session.ended') {
// Send Discord message
sendDiscordMessage(
`Session ${data.session_number} ended! XP awarded: ${data.xp_awarded}`
);
}
res.sendStatus(200);
});
Best Practices
Security
- Never expose your API token in client-side code
- Regenerate tokens if compromised
- Use environment variables to store tokens
- Implement webhook signature verification
Performance
- Cache responses when appropriate
- Use pagination for large datasets
- Batch requests when possible
- Implement exponential backoff for rate limits
Error Handling
try {
const character = await cipher.characters.get(characterId);
} catch (error) {
if (error.status === 404) {
console.log('Character not found');
} else if (error.status === 429) {
// Rate limit - wait and retry
await sleep(error.retryAfter * 1000);
// Retry request
} else {
console.error('API error:', error.message);
}
}
Support
- API Issues: Contact Support
- SDK Issues: Open issue on GitHub
- Feature Requests: Community Forum
- Status: status.scrollbook.app
API Version: 1.0 Last Updated: January 15, 2025 Changelog: View API changelog
Was this helpful?
Help us improve our documentation. Let us know if something is unclear or missing.