Skip to content

Authentication#

The Storyteller Integration API uses API key authentication to secure all requests. This page explains how to obtain and use your API key.

Getting Your API Key#

Your API key can be obtained through:

  1. Storyteller CMS - Available in the Apps section. Create a "Server" app for use with the Integrations API
  2. Contact Support - Reach out to [email protected] or via Slack

Keep Your API Key Secure

Your API key provides access to your Storyteller account. Keep it secure and never expose it in client-side code or public repositories.

Using Your API Key#

Header Authentication#

Add your API key to the x-storyteller-api-key header in all API requests:

GET /api/workflows HTTP/1.1
Host: integrations.usestoryteller.com
x-storyteller-api-key: your-api-key-here
Content-Type: application/json

Code Examples#

const fetch = require('node-fetch');

const apiKey = 'your-api-key-here';
const baseUrl = 'https://integrations.usestoryteller.com';

async function getWorkflows() {
  const response = await fetch(`${baseUrl}/api/workflows`, {
    method: 'GET',
    headers: {
      'x-storyteller-api-key': apiKey,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`Authentication failed: ${response.statusText}`);
  }

  return await response.json();
}

// Usage with error handling
getWorkflows()
  .then(workflows => console.log('Workflows:', workflows))
  .catch(error => console.error('Error:', error.message));
import requests
from requests.exceptions import RequestException

api_key = 'your-api-key-here'
base_url = 'https://integrations.usestoryteller.com'

headers = {
    'x-storyteller-api-key': api_key,
    'Content-Type': 'application/json'
}

try:
    response = requests.get(f'{base_url}/api/workflows', headers=headers)
    response.raise_for_status()  # Raises exception for bad status codes
    workflows = response.json()
    print('Workflows:', workflows)
except requests.exceptions.Unauthorized:
    print('Authentication failed: Check your API key')
except RequestException as e:
    print(f'Request failed: {e}')
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

public class StorytellerClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl = "https://integrations.usestoryteller.com";

    public StorytellerClient(string apiKey)
    {
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);
        _httpClient.DefaultRequestHeaders.Add("Content-Type", "application/json");
    }

    public async Task<dynamic> GetWorkflowsAsync()
    {
        try
        {
            var response = await _httpClient.GetAsync($"{_baseUrl}/api/workflows");

            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                throw new UnauthorizedAccessException("Authentication failed: Check your API key");
            }

            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject(json);
        }
        catch (HttpRequestException ex)
        {
            throw new Exception($"Request failed: {ex.Message}", ex);
        }
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}

// Usage
using var client = new StorytellerClient("your-api-key-here");
try
{
    var workflows = await client.GetWorkflowsAsync();
    Console.WriteLine($"Workflows: {workflows}");
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
curl -X GET "https://integrations.usestoryteller.com/api/workflows" \
  -H "x-storyteller-api-key: your-api-key-here" \
  -H "Content-Type: application/json"

Authentication Errors#

Common Error Responses#

All error responses follow the RFC 7807 Problem Details specification format.

Missing or Invalid API Key#

{
  "status": 401,
  "type": "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1",
  "title": "Unauthorized",
  "detail": "No valid API key provided",
  "instance": ""
}

Resource Not Found#

{
  "status": 404,
  "type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "detail": "The requested resource was not found",
  "instance": ""
}

Bad Request (Invalid Data)#

{
  "status": 400,
  "type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "detail": "Validation failed: Field 'mediaUrl' is required",
  "instance": "correlation-id-here"
}

Best Practices#

Environment Variables#

Store your API key in environment variables rather than hardcoding it:

// Load from environment variables
const apiKey = process.env.STORYTELLER_API_KEY;

if (!apiKey) {
  throw new Error('STORYTELLER_API_KEY environment variable is required');
}

// Example .env file setup (requires dotenv package)
// npm install dotenv
require('dotenv').config();

const client = new StorytellerClient(process.env.STORYTELLER_API_KEY);
import os
from typing import Optional

# Load from environment variables
api_key: Optional[str] = os.environ.get('STORYTELLER_API_KEY')

if not api_key:
    raise ValueError('STORYTELLER_API_KEY environment variable is required')

# Alternative: using python-dotenv
# pip install python-dotenv
# from dotenv import load_dotenv
# load_dotenv()

client = StorytellerClient(api_key)
using System;

public class Program
{
    public static void Main()
    {
        // Load from environment variables
        var apiKey = Environment.GetEnvironmentVariable("STORYTELLER_API_KEY");

        if (string.IsNullOrEmpty(apiKey))
        {
            throw new InvalidOperationException("STORYTELLER_API_KEY environment variable is required");
        }

        using var client = new StorytellerClient(apiKey);
        // Use client...
    }
}

// Alternative: Using configuration (ASP.NET Core)
// services.Configure<StorytellerOptions>(configuration.GetSection("Storyteller"));
// public class StorytellerOptions { public string ApiKey { get; set; } }
# Set environment variable
export STORYTELLER_API_KEY="your-api-key-here"

# Or add to .env file
echo "STORYTELLER_API_KEY=your-api-key-here" >> .env

# Use in cURL commands
curl -X GET "https://integrations.usestoryteller.com/api/workflows" \
  -H "x-storyteller-api-key: $STORYTELLER_API_KEY" \
  -H "Content-Type: application/json"

Request Retry Logic#

Implement retry logic for authentication failures:

async function makeAuthenticatedRequest(url, options, retries = 3) {
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'x-storyteller-api-key': process.env.STORYTELLER_API_KEY,
          'Content-Type': 'application/json',
          ...options.headers
        }
      });

      // Don't retry on authentication errors - likely a bad API key
      if (response.status === 401) {
        throw new Error('Authentication failed: Check your API key');
      }

      // Retry on server errors or rate limits
      if (response.status >= 500 || response.status === 429) {
        if (attempt < retries) {
          const backoffMs = Math.pow(2, attempt) * 1000; // Exponential backoff
          console.log(`Request failed, retrying in ${backoffMs}ms...`);
          await delay(backoffMs);
          continue;
        }
      }

      return response;
    } catch (error) {
      if (attempt < retries && error.code !== 'EAUTH') {
        const backoffMs = Math.pow(2, attempt) * 1000;
        console.log(`Network error, retrying in ${backoffMs}ms...`);
        await delay(backoffMs);
        continue;
      }
      throw error;
    }
  }
}
import time
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

def create_session_with_retries():
    """Create a requests session with automatic retry logic"""
    session = requests.Session()

    # Define retry strategy (don't retry on 401/403 - auth errors)
    retry_strategy = Retry(
        total=3,
        backoff_factor=2,  # Exponential backoff
        status_forcelist=[429, 500, 502, 503, 504],  # Retry on these status codes
        allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"]
    )

    # Mount adapter with retry strategy
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)

    return session

def make_authenticated_request(url, api_key, **kwargs):
    session = create_session_with_retries()
    headers = kwargs.get('headers', {})
    headers['x-storyteller-api-key'] = api_key
    headers['Content-Type'] = 'application/json'
    kwargs['headers'] = headers

    try:
        response = session.request('GET', url, **kwargs)
        response.raise_for_status()
        return response
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 401:
            raise ValueError('Authentication failed: Check your API key')
        raise
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class RetryHttpClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;
    private readonly int _maxRetries;

    public RetryHttpClient(string apiKey, int maxRetries = 3)
    {
        _apiKey = apiKey;
        _maxRetries = maxRetries;
        _httpClient = new HttpClient();
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await MakeRequestWithRetryAsync(() =>
        {
            var request = new HttpRequestMessage(HttpMethod.Get, url);
            request.Headers.Add("x-storyteller-api-key", _apiKey);
            request.Headers.Add("Content-Type", "application/json");
            return _httpClient.SendAsync(request);
        });
    }

    private async Task<HttpResponseMessage> MakeRequestWithRetryAsync(
        Func<Task<HttpResponseMessage>> requestFunc)
    {
        for (int attempt = 0; attempt <= _maxRetries; attempt++)
        {
            try
            {
                var response = await requestFunc();

                // Don't retry authentication errors
                if (response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    throw new UnauthorizedAccessException("Authentication failed: Check your API key");
                }

                // Retry on server errors or rate limits
                if (((int)response.StatusCode >= 500 || response.StatusCode == HttpStatusCode.TooManyRequests)
                    && attempt < _maxRetries)
                {
                    var delay = TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 1000);
                    await Task.Delay(delay);
                    continue;
                }

                return response;
            }
            catch (HttpRequestException) when (attempt < _maxRetries)
            {
                var delay = TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 1000);
                await Task.Delay(delay);
            }
        }

        throw new HttpRequestException("Request failed after maximum retry attempts");
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}
#!/bin/bash
# Simple retry logic for cURL commands

make_authenticated_request() {
    local url="$1"
    local max_retries=3
    local retry_count=0

    while [ $retry_count -le $max_retries ]; do
        response=$(curl -s -w "\n%{http_code}" -X GET "$url" \
            -H "x-storyteller-api-key: $STORYTELLER_API_KEY" \
            -H "Content-Type: application/json")

        # Extract HTTP status code from response
        http_code=$(echo "$response" | tail -n1)
        body=$(echo "$response" | sed '$d')

        case $http_code in
            200|201|202)
                echo "$body"
                return 0
                ;;
            401|403)
                echo "Authentication failed: Check your API key" >&2
                return 1
                ;;
            429|5*)
                if [ $retry_count -lt $max_retries ]; then
                    delay=$((2 ** retry_count))
                    echo "Request failed (HTTP $http_code), retrying in ${delay}s..." >&2
                    sleep $delay
                    ((retry_count++))
                else
                    echo "Request failed after $max_retries retries (HTTP $http_code)" >&2
                    return 1
                fi
                ;;
            *)
                echo "Unexpected response (HTTP $http_code): $body" >&2
                return 1
                ;;
        esac
    done
}

# Usage
make_authenticated_request "https://integrations.usestoryteller.com/api/workflows"

Testing Authentication#

You can test your authentication by making a simple request to the workflows endpoint:

const fetch = require('node-fetch');

async function testAuthentication() {
  try {
    const response = await fetch('https://integrations.usestoryteller.com/api/workflows', {
      headers: {
        'x-storyteller-api-key': process.env.STORYTELLER_API_KEY,
        'Content-Type': 'application/json'
      }
    });

    if (response.ok) {
      console.log('✅ Authentication successful!');
      const workflows = await response.json();
      console.log(`Available workflows: ${workflows.length}`);
      return true;
    } else if (response.status === 401) {
      console.log('❌ Authentication failed: Invalid API key');
      return false;
    } else {
      console.log(`❌ Request failed: ${response.status} ${response.statusText}`);
      return false;
    }
  } catch (error) {
    console.log(`❌ Network error: ${error.message}`);
    return false;
  }
}

testAuthentication();
import requests
import os

def test_authentication():
    try:
        response = requests.get(
            'https://integrations.usestoryteller.com/api/workflows',
            headers={
                'x-storyteller-api-key': os.environ.get('STORYTELLER_API_KEY'),
                'Content-Type': 'application/json'
            }
        )

        if response.ok:
            print('✅ Authentication successful!')
            workflows = response.json()
            print(f'Available workflows: {len(workflows)}')
            return True
        elif response.status_code == 401:
            print('❌ Authentication failed: Invalid API key')
            return False
        else:
            print(f'❌ Request failed: {response.status_code} {response.reason}')
            return False
    except requests.RequestException as e:
        print(f'❌ Network error: {e}')
        return False

if __name__ == '__main__':
    test_authentication()
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

public class AuthenticationTester
{
    private readonly HttpClient _httpClient;

    public AuthenticationTester()
    {
        _httpClient = new HttpClient();
    }

    public async Task<bool> TestAuthenticationAsync(string apiKey)
    {
        try
        {
            _httpClient.DefaultRequestHeaders.Clear();
            _httpClient.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);
            _httpClient.DefaultRequestHeaders.Add("Content-Type", "application/json");

            var response = await _httpClient.GetAsync("https://integrations.usestoryteller.com/api/workflows");

            if (response.IsSuccessStatusCode)
            {
                Console.WriteLine("✅ Authentication successful!");
                var content = await response.Content.ReadAsStringAsync();
                var workflows = JArray.Parse(content);
                Console.WriteLine($"Available workflows: {workflows.Count}");
                return true;
            }
            else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                Console.WriteLine("❌ Authentication failed: Invalid API key");
                return false;
            }
            else
            {
                Console.WriteLine($"❌ Request failed: {(int)response.StatusCode} {response.ReasonPhrase}");
                return false;
            }
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"❌ Network error: {ex.Message}");
            return false;
        }
    }
}

// Usage
var tester = new AuthenticationTester();
var apiKey = Environment.GetEnvironmentVariable("STORYTELLER_API_KEY");
var success = await tester.TestAuthenticationAsync(apiKey);
# Test with explicit API key
curl -X GET "https://integrations.usestoryteller.com/api/workflows" \
  -H "x-storyteller-api-key: your-api-key-here" \
  -H "Content-Type: application/json"

# Test with environment variable
curl -X GET "https://integrations.usestoryteller.com/api/workflows" \
  -H "x-storyteller-api-key: $STORYTELLER_API_KEY" \
  -H "Content-Type: application/json"

Expected Success Response:

[
  {
    "code": "add-media",
    "name": "Add Media",
    "description": "Add media assets to your Storyteller tenant"
  },
  {
    "code": "add-clip",
    "name": "Add Clip",
    "description": "Create clips from existing media"
  }
]

A successful response indicates your API key is working correctly.

Need Help?#