Skip to main content

API Documentation

REST API reference for integrating with Cipher & Scrollbook (Multi-Campaign tier and above)

Last updated: January 15, 2025

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

text
https://api.scrollbook.app/v1

Authentication

All API requests require authentication using a JWT token.

Getting Your API Token:

  1. Log in to scrollbook.app
  2. Go to Settings → API Access
  3. Click "Generate API Token"
  4. Copy and securely store your token

Using the Token:

Include in the Authorization header:

bash
Authorization: Bearer YOUR_API_TOKEN

Example Request:

bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  https://api.scrollbook.app/v1/campaigns

Rate Limits

Rate limits vary by tier:

TierRequests per MinuteRequests per Hour
Multi-Campaign601,000
Unlimited Guild1205,000
EnterpriseCustomCustom

Rate Limit Headers:

text
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000

Rate Limit Exceeded:

json
{
  "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:

text
GET /campaigns

Query Parameters:

  • guild_id (optional): Filter by Discord guild ID
  • status (optional): Filter by status (active, archived, completed)
  • limit (optional): Number of results (default: 50, max: 100)
  • offset (optional): Pagination offset

Request:

bash
GET /campaigns?guild_id=123456789&status=active&limit=10

Response:

json
{
  "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:

text
GET /campaigns/{campaign_id}

Request:

bash
GET /campaigns/campaign_abc123

Response:

json
{
  "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:

text
POST /campaigns

Request Body:

json
{
  "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:

json
{
  "id": "campaign_def456",
  "name": "Dragon Heist",
  "created_at": "2025-01-15T10:00:00Z",
  "message": "Campaign created successfully"
}

Update Campaign

Update campaign details.

Endpoint:

text
PATCH /campaigns/{campaign_id}

Request Body:

json
{
  "description": "Updated description",
  "tone": "serious",
  "settings": {
    "homebrew_allowed": true
  }
}

Response:

json
{
  "id": "campaign_abc123",
  "message": "Campaign updated successfully",
  "updated_at": "2025-01-15T11:00:00Z"
}

Delete Campaign

Permanently delete a campaign.

Endpoint:

text
DELETE /campaigns/{campaign_id}

Response:

json
{
  "message": "Campaign deleted successfully"
}

Characters

List Characters

Get all characters you have access to.

Endpoint:

text
GET /characters

Query Parameters:

  • campaign_id (optional): Filter by campaign
  • user_id (optional): Filter by owner
  • limit (optional): Number of results
  • offset (optional): Pagination offset

Request:

bash
GET /characters?campaign_id=campaign_abc123&limit=10

Response:

json
{
  "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:

text
GET /characters/{character_id}

Request:

bash
GET /characters/char_123abc

Response:

json
{
  "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:

text
POST /characters

Request Body:

json
{
  "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:

json
{
  "id": "char_456def",
  "name": "Aria Moonwhisper",
  "created_at": "2025-01-15T12:00:00Z",
  "message": "Character created successfully"
}

Update Character

Update character attributes.

Endpoint:

text
PATCH /characters/{character_id}

Request Body:

json
{
  "hit_points_current": 35,
  "experience_points": 7000,
  "level": 6
}

Response:

json
{
  "id": "char_123abc",
  "message": "Character updated successfully",
  "updated_at": "2025-01-15T13:00:00Z"
}

Delete Character

Permanently delete a character.

Endpoint:

text
DELETE /characters/{character_id}

Response:

json
{
  "message": "Character deleted successfully"
}

Sessions

List Sessions

Get session history.

Endpoint:

text
GET /sessions

Query Parameters:

  • campaign_id (required): Campaign ID
  • status (optional): Filter by status (active, paused, completed)
  • limit (optional): Number of results
  • offset (optional): Pagination offset

Request:

bash
GET /sessions?campaign_id=campaign_abc123&status=completed&limit=5

Response:

json
{
  "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:

text
GET /sessions/{session_id}

Request:

bash
GET /sessions/session_789ghi

Response:

json
{
  "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:

text
POST /sessions

Request Body:

json
{
  "campaign_id": "campaign_abc123",
  "title": "The Tomb of Horrors",
  "participant_character_ids": [
    "char_123abc",
    "char_456def"
  ]
}

Response:

json
{
  "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:

text
POST /sessions/{session_id}/end

Request Body:

json
{
  "summary": "Optional brief summary",
  "xp_awarded": 1200
}

Response:

json
{
  "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:

text
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 name
  • limit (optional): Number of results
  • offset (optional): Pagination offset

Request:

bash
GET /content/spells?level=3&school=evocation&limit=10

Response:

json
{
  "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:

text
GET /content/spells/{spell_id}

List Monsters

Get SRD monster list.

Endpoint:

text
GET /content/monsters

Query Parameters:

  • cr (optional): Filter by challenge rating
  • type (optional): Filter by type (dragon, undead, etc.)
  • size (optional): Filter by size (tiny, small, medium, etc.)
  • search (optional): Search by name
  • limit (optional): Number of results
  • offset (optional): Pagination offset

Request:

bash
GET /content/monsters?type=dragon&cr=10&limit=5

Response:

json
{
  "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:

text
GET /content/monsters/{monster_id}

Webhooks

Create Webhook

Register a webhook to receive events.

Endpoint:

text
POST /webhooks

Request Body:

json
{
  "url": "https://your-server.com/webhook",
  "events": [
    "session.started",
    "session.ended",
    "character.created",
    "character.level_up"
  ],
  "secret": "your_webhook_secret"
}

Response:

json
{
  "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.created
  • campaign.updated
  • campaign.deleted

Character Events:

  • character.created
  • character.updated
  • character.deleted
  • character.level_up

Session Events:

  • session.started
  • session.paused
  • session.resumed
  • session.ended

Example Webhook Payload:

json
{
  "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:

json
{
  "error": "error_code",
  "message": "Human-readable error message",
  "details": {
    "field": "Additional context if applicable"
  }
}

Common Error Codes:

CodeStatusDescription
unauthorized401Invalid or missing API token
forbidden403Insufficient permissions
not_found404Resource not found
validation_error422Invalid request data
rate_limit_exceeded429Too many requests
internal_error500Server error

SDKs & Libraries

Official SDKs

Python:

bash
pip install cipher-api
python
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:

bash
npm install @cipher/api
javascript
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:

python
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:

javascript
// 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:

bash
#!/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:

javascript
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

javascript
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 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.

API Documentation | Scrollbook Documentation | Scrollbook