Skip to content

Templates API#

The Templates endpoint allows you to retrieve workflow templates from your Storyteller tenant. Workflow templates define reusable configurations for content processing with customizable properties and parameters.

Endpoint Details#

GET https://integrations.usestoryteller.com/api/templates

Headers#

Header Required Description
x-storyteller-api-key Yes Your tenant Server app API key for authentication

Query Parameters#

Parameter Type Required Default Description
searchText string No - Match a substring in the title, public code or stored template description
currentPage integer No 1 Page number for pagination. Ignored when skipCount or maxResultCount is supplied.
pageSize integer No 10 Number of items per page. Must be between 1 and 1000. Ignored when skipCount or maxResultCount is supplied.
skipCount integer No 0 Legacy offset-style pagination alias. Defaults to 0 when omitted while using legacy pagination.
maxResultCount integer No 10 Legacy offset-style page-size alias. Defaults to 10 when omitted while using legacy pagination. Maximum 1000.
sort string No AlphabeticalAsc Sort order: AlphabeticalAsc, LastModifiedDesc

You can page this endpoint using either currentPage + pageSize or legacy-style skipCount + maxResultCount.

In legacy mode, omitted aliases fall back to skipCount=0 and maxResultCount=10. Because integrations responses still return currentPage, skipCount must land on a page boundary for the effective page size.

Search and ordering#

searchText matches a substring in any of title, code or description. Matching follows the database collation, including its case and accent behavior. Nonblank text is used as supplied, without trimming, tokenization or ranking. Missing fields do not match. A template matching more than one field is returned once; property remarks are not searched.

Omitting searchText, or supplying an empty or whitespace-only value, browses the authenticated tenant's integration-enabled catalogue. Search responses use the same template representation and pagination contract as browsing. totalCount and totalPages describe the filtered results; no matches returns an empty templates array and zero totals.

The default AlphabeticalAsc orders by title, and LastModifiedDesc orders by last modification time descending. Both use the internal template ID ascending to break ties. The shared API parser also accepts CreatedDesc and PublishedDesc; templates do not implement those date sorts and return deterministic internal-ID order instead. IDs used for sorting are not exposed as an alternate lookup key.

This ordering supports consistent page traversal while the catalogue is unchanged. Updates between requests can still move items between pages; pagination does not provide a snapshot.

Response#

Success Response (200 OK)#

{
  "templates": [
    {
      "code": "video-to-story",
      "title": "Video to Story Template",
      "description": "Create a story from a video and its metadata.",
      "previewImageUrl": "https://media.usestoryteller.com/templates/video-story-preview.jpg",
      "properties": [
        {
          "key": "title",
          "remarks": "The title for the story",
          "type": "string",
          "kind": "text",
          "isOptional": false
        },
        {
          "key": "storyId",
          "remarks": "ID of the story to add content to",
          "type": "string",
          "kind": "text",
          "isOptional": false
        },
        {
          "key": "category",
          "remarks": "Category to assign to the content",
          "type": "string",
          "kind": "text",
          "isOptional": true
        },
        {
          "key": "autoPublish",
          "remarks": "Whether to automatically publish the content",
          "type": "boolean",
          "kind": "text",
          "isOptional": true
        }
      ]
    },
    {
      "code": "image-slideshow",
      "title": "Image Slideshow Template",
      "description": "Create a slideshow from a set of images.",
      "previewImageUrl": "https://media.usestoryteller.com/templates/slideshow-preview.jpg",
      "properties": [
        {
          "key": "title",
          "remarks": "The title for the slideshow",
          "type": "string",
          "kind": "text",
          "isOptional": false
        },
        {
          "key": "coverImage",
          "remarks": "Image shown on the first slide",
          "type": "string",
          "kind": "image",
          "isOptional": true
        },
        {
          "key": "duration",
          "remarks": "Duration per slide in seconds",
          "type": "integer",
          "kind": "text",
          "isOptional": true
        },
        {
          "key": "transition",
          "remarks": "Transition effect between slides",
          "type": "string",
          "kind": "text",
          "isOptional": true
        }
      ]
    }
  ],
  "pageSize": 10,
  "currentPage": 1,
  "totalPages": 1,
  "totalCount": 1
}

Response Fields#

Template Object#

Field Type Description
code string Unique template identifier/code
title string Human-readable template name
description string Description of the workflow template. Always returned as a string; an unset description is returned as "".
previewImageUrl string URL to template preview image
properties array Array of configurable template properties

Property Object#

Field Type Description
key string Property key name for metadata
remarks string Human-readable description of the property
type string Value type of the metadata you supply: string, integer or boolean
kind string Kind of template element the property fills: text, image or video. For image and video, the value is normally a media URL.
isOptional boolean Whether the property is optional or required

Pagination Object#

Field Type Description
pageSize integer Number of items per page
currentPage integer Current page number
totalPages integer Total number of pages available
totalCount integer Exact total number of matching templates

Get a Template by Code#

GET https://integrations.usestoryteller.com/api/templates/{code}

Use the code returned by template discovery, URL-encoded as one path segment. This is the template's public code, not its internal database GUID. Lookup uses exact code equality under the database collation; it does not perform a substring search.

Send the same x-storyteller-api-key header as the list endpoint. Only templates available to integrations in the authenticated tenant are returned. A successful response is one template object, with no pagination or templates wrapper:

{
  "code": "example-template",
  "title": "Example Template",
  "description": "Create a story with a headline.",
  "previewImageUrl": "https://cdn.example.com/templates/example.jpg",
  "properties": [
    {
      "key": "title",
      "remarks": "Story headline",
      "type": "string",
      "kind": "text",
      "isOptional": false
    }
  ]
}

The fields and property metadata match the list representation, including description: "" when unset. Properties with the mediaUrl automation key remain excluded from both responses.

List-to-detail example#

const baseUrl = 'https://integrations.usestoryteller.com/api/templates';
const headers = { 'x-storyteller-api-key': process.env.STORYTELLER_API_KEY };
const listResponse = await fetch(`${baseUrl}?pageSize=1`, { headers });
if (!listResponse.ok) throw new Error(`List failed: ${listResponse.status}`);
const { templates } = await listResponse.json();
if (templates.length > 0) {
  const response = await fetch(`${baseUrl}/${encodeURIComponent(templates[0].code)}`, { headers });
  if (!response.ok) throw new Error(`Lookup failed: ${response.status}`);
  const template = await response.json();
  console.log(template.description, template.properties);
}

Lookup errors#

Missing, invalid or non-Server app keys return 401 Unauthorized. An unknown code, or a template that is disabled for integrations, deleted or unavailable to the authenticated tenant, returns 404 Not Found through the normal API error format:

{
  "status": 404,
  "type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "detail": "Workflow template with code example-missing not found.",
  "instance": ""
}

Code Examples#

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

async function getTemplates(searchText = '', currentPage = 1, pageSize = 10, sort = '') {
  const params = new URLSearchParams({
    ...(searchText && { searchText }),
    currentPage: currentPage.toString(),
    pageSize: pageSize.toString(),
    ...(sort && { sort })
  });

  try {
    const response = await fetch(`https://integrations.usestoryteller.com/api/templates?${params}`, {
      method: 'GET',
      headers: {
        'x-storyteller-api-key': process.env.STORYTELLER_API_KEY
      }
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`HTTP ${response.status}: ${errorData.detail}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching templates:', error);
    throw error;
  }
}

// Usage examples
async function examples() {
  // Get all templates
  const allTemplates = await getTemplates();
  console.log(`Found ${allTemplates.templates.length} templates`);

  // Search for video templates
  const videoTemplates = await getTemplates('video', 1, 20);
  console.log(`Found ${videoTemplates.templates.length} video templates`);

  // Get templates sorted alphabetically
  const sortedTemplates = await getTemplates('', 1, 50, 'AlphabeticalAsc');
  console.log(`Retrieved ${sortedTemplates.templates.length} templates sorted alphabetically`);

  // Analyze template properties
  allTemplates.templates.forEach(template => {
    console.log(`Template: ${template.title} (${template.code})`);
    console.log('Required properties:',
      template.properties.filter(p => !p.isOptional).map(p => p.key)
    );
    console.log('Optional properties:',
      template.properties.filter(p => p.isOptional).map(p => p.key)
    );
  });
}
import requests
import os
from urllib.parse import urlencode

def get_templates(search_text='', current_page=1, page_size=10, sort=''):
    url = 'https://integrations.usestoryteller.com/api/templates'
    headers = {
        'x-storyteller-api-key': os.environ.get('STORYTELLER_API_KEY')
    }

    params = {
        'currentPage': current_page,
        'pageSize': page_size
    }

    if search_text:
        params['searchText'] = search_text
    if sort:
        params['sort'] = sort

    try:
        response = requests.get(url, headers=headers, params=params)
        response.raise_for_status()

        data = response.json()
        return data
    except requests.exceptions.RequestException as e:
        print(f'Error fetching templates: {e}')
        if hasattr(e.response, 'json'):
            print(f'Error details: {e.response.json()}')
        raise

# Usage examples
try:
    # Get all templates
    all_templates = get_templates()
    print(f'Found {len(all_templates["templates"])} templates')

    # Search for video templates
    video_templates = get_templates(search_text='video', page_size=20)
    print(f'Found {len(video_templates["templates"])} video templates')

    # Get templates sorted alphabetically
    sorted_templates = get_templates(page_size=50, sort='AlphabeticalAsc')
    print(f'Retrieved {len(sorted_templates["templates"])} templates sorted alphabetically')

    # Analyze template properties
    for template in all_templates['templates']:
        print(f'Template: {template["title"]} ({template["code"]})')
        required_props = [p['key'] for p in template['properties'] if not p['isOptional']]
        optional_props = [p['key'] for p in template['properties'] if p['isOptional']]
        print(f'Required properties: {required_props}')
        print(f'Optional properties: {optional_props}')

except Exception as e:
    print(f'Failed to fetch templates: {e}')
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using Newtonsoft.Json;

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

    public TemplatesClient(string apiKey)
    {
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("x-storyteller-api-key", apiKey);
    }

    public async Task<TemplatesResponse> GetTemplatesAsync(string searchText = "", int currentPage = 1, int pageSize = 10, string sort = "")
    {
        try
        {
            var queryParams = new List<string>
            {
                $"currentPage={currentPage}",
                $"pageSize={pageSize}"
            };

            if (!string.IsNullOrEmpty(searchText))
            {
                queryParams.Add($"searchText={Uri.EscapeDataString(searchText)}");
            }

            if (!string.IsNullOrEmpty(sort))
            {
                queryParams.Add($"sort={sort}");
            }

            var queryString = string.Join("&", queryParams);
            var response = await _httpClient.GetAsync($"{_baseUrl}/api/templates?{queryString}");
            response.EnsureSuccessStatusCode();

            var responseContent = await response.Content.ReadAsStringAsync();
            var templates = JsonConvert.DeserializeObject<TemplatesResponse>(responseContent);

            return templates;
        }
        catch (HttpRequestException ex)
        {
            throw new Exception($"Error fetching templates: {ex.Message}", ex);
        }
    }
}

public class TemplatesResponse
{
    public WorkflowTemplate[] Templates { get; set; }
    public int PageSize { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPages { get; set; }
}

public class WorkflowTemplate
{
    public string Code { get; set; }
    public string Title { get; set; }
    public string PreviewImageUrl { get; set; }
    public TemplateProperty[] Properties { get; set; }
}

public class TemplateProperty
{
    public string Key { get; set; }
    public string Remarks { get; set; }
    public string Type { get; set; }
    public string Kind { get; set; }
    public bool IsOptional { get; set; }
}

// Usage
var client = new TemplatesClient(Environment.GetEnvironmentVariable("STORYTELLER_API_KEY"));

try
{
    // Get all templates
    var allTemplates = await client.GetTemplatesAsync();
    Console.WriteLine($"Found {allTemplates.Templates.Length} templates");

    // Search for video templates
    var videoTemplates = await client.GetTemplatesAsync("video", 1, 20);
    Console.WriteLine($"Found {videoTemplates.Templates.Length} video templates");

    // Get templates sorted alphabetically
    var sortedTemplates = await client.GetTemplatesAsync("", 1, 50, "AlphabeticalAsc");
    Console.WriteLine($"Retrieved {sortedTemplates.Templates.Length} templates sorted alphabetically");

    // Analyze template properties
    foreach (var template in allTemplates.Templates)
    {
        Console.WriteLine($"Template: {template.Title} ({template.Code})");

        var requiredProps = template.Properties.Where(p => !p.IsOptional).Select(p => p.Key);
        var optionalProps = template.Properties.Where(p => p.IsOptional).Select(p => p.Key);

        Console.WriteLine($"Required properties: {string.Join(", ", requiredProps)}");
        Console.WriteLine($"Optional properties: {string.Join(", ", optionalProps)}");
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
# Get all templates (first page)
curl -X GET "https://integrations.usestoryteller.com/api/templates" \
  -H "x-storyteller-api-key: your-api-key-here"

# Search for video templates
curl -X GET "https://integrations.usestoryteller.com/api/templates?searchText=video&pageSize=20" \
  -H "x-storyteller-api-key: your-api-key-here"

# Get templates sorted alphabetically
curl -X GET "https://integrations.usestoryteller.com/api/templates?sort=AlphabeticalAsc&pageSize=50" \
  -H "x-storyteller-api-key: your-api-key-here"

# Get second page of templates
curl -X GET "https://integrations.usestoryteller.com/api/templates?currentPage=2&pageSize=10" \
  -H "x-storyteller-api-key: your-api-key-here"

Usage with Workflows#

Templates provide the structure for workflow execution. Use template information to build proper metadata:

// 1. Get template information
const templatesData = await getTemplates();
const videoTemplate = templatesData.templates.find(t => t.code === 'video-to-story');

// 2. Build metadata based on template properties
function buildMetadataFromTemplate(template, userValues) {
  const metadata = {};

  template.properties.forEach(property => {
    if (userValues[property.key] !== undefined) {
      // Convert value based on property type
      switch (property.type) {
        case 'boolean':
          metadata[property.key] = Boolean(userValues[property.key]);
          break;
        case 'integer':
          metadata[property.key] = Number.parseInt(String(userValues[property.key]), 10);
          break;
        default:
          metadata[property.key] = String(userValues[property.key]);
      }
    } else if (!property.isOptional) {
      throw new Error(`Required property '${property.key}' is missing`);
    }
  });

  return metadata;
}

// 3. Create metadata from template
const userValues = {
  title: 'My Product Demo',
  storyId: 'story-123',
  category: 'products',
  autoPublish: true
};

const metadata = {
  'https://example.com/video.mp4': buildMetadataFromTemplate(videoTemplate, userValues)
};

// 4. Execute workflow with template-based metadata
await executeWorkflow([videoTemplate.code], ['https://example.com/video.mp4'], metadata);

Template Validation#

function validateTemplateMetadata(template, metadata) {
  const errors = [];
  const providedKeys = Object.keys(metadata);

  // Check required properties
  template.properties.forEach(property => {
    if (!property.isOptional && !providedKeys.includes(property.key)) {
      errors.push(`Missing required property: ${property.key}`);
    }

    // Type validation
    if (metadata[property.key] !== undefined) {
      const value = metadata[property.key];
      const expectedType = property.type;

      switch (expectedType) {
        case 'boolean':
          if (typeof value !== 'boolean') {
            errors.push(`Property '${property.key}' must be a boolean`);
          }
          break;
        case 'integer':
          if (!Number.isInteger(value)) {
            errors.push(`Property '${property.key}' must be an integer`);
          }
          break;
        case 'string':
          if (typeof value !== 'string') {
            errors.push(`Property '${property.key}' must be a string`);
          }
          break;
      }
    }
  });

  return errors;
}

// Usage
const errors = validateTemplateMetadata(videoTemplate, userValues);
if (errors.length > 0) {
  console.error('Validation errors:', errors);
} else {
  console.log('Metadata is valid for template');
}

Dynamic Form Generation#

function generateFormFromTemplate(template) {
  console.log(`Template: ${template.title}`);
  console.log('Required fields:');

  template.properties.forEach(property => {
    const required = property.isOptional ? ' (optional)' : ' (required)';
    console.log(`- ${property.key} (${property.type})${required}: ${property.remarks}`);
  });
}

// Generate form structure for all templates
async function generateForms() {
  const templatesData = await getTemplates();

  templatesData.templates.forEach(template => {
    generateFormFromTemplate(template);
    console.log('---');
  });
}

Pagination Example#

async function getAllTemplates() {
  let allTemplates = [];
  let currentPage = 1;
  let totalPages = 1;

  do {
    const response = await getTemplates('', currentPage, 50);
    allTemplates.push(...response.templates);
    totalPages = response.totalPages;
    currentPage++;
  } while (currentPage <= totalPages);

  return allTemplates;
}

// Usage
const allTemplates = await getAllTemplates();
console.log(`Retrieved ${allTemplates.length} total templates`);

Error Handling#

Common Error Responses#

{
  "status": 401,
  "type": "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1",
  "title": "Unauthorized",
  "detail": "No valid API key provided",
  "instance": ""
}
{
  "status": 400,
  "type": "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "detail": "Invalid page size. Must be between 1 and 100",
  "instance": ""
}

Best Practices#

  1. Cache Templates - Templates don't change frequently, cache them for better performance
  2. Validate Metadata - Always validate user input against template property requirements
  3. Handle Types Properly - Convert string inputs to proper types based on property definitions
  4. Required vs Optional - Clearly distinguish between required and optional properties in your UI
  5. Use Template Codes - Reference templates by their stable code rather than title
  6. Preview Images - Use preview images to help users select appropriate templates
  7. Property Documentation - Show property remarks to help users understand what each field does

Template Property Types#

Type Description Example Values
string Text value "My Title", "product-category"
integer Whole number 42, 0
boolean True/false value true, false

Template Property Kinds#

Kind The property fills Value to supply
text A text element The text
image An image element Normally an image URL
video A video element Normally a video URL

Complete Implementation Example#

For a complete working example that demonstrates how to build a form interface for handling template metadata and properties, see the Storyteller Sample Integration repository.

This Next.js application shows:

  • Template Properties Handling: Dynamically generating form fields based on template properties
  • Metadata Collection: Building metadata objects from user input
  • Form Validation: Handling required vs optional template properties
  • Workflow Execution: Submitting templates with metadata to the Integration API

Key files to examine:

  • src/api/requests.ts - API integration methods for templates and workflows
  • src/components/PublishToStoryteller/ - Complete form implementation