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:
- Storyteller CMS - Available in the Apps section. Create a "Server" app for use with the Integrations API
- 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?#
- Invalid API Key? Contact [email protected] or reach out via Slack
- Authentication Issues? Check our Troubleshooting guide