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+pageSizeor legacy-styleskipCount+maxResultCount.In legacy mode, omitted aliases fall back to
skipCount=0andmaxResultCount=10. Because integrations responses still returncurrentPage,skipCountmust 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#
- Cache Templates - Templates don't change frequently, cache them for better performance
- Validate Metadata - Always validate user input against template property requirements
- Handle Types Properly - Convert string inputs to proper types based on property definitions
- Required vs Optional - Clearly distinguish between required and optional properties in your UI
- Use Template Codes - Reference templates by their stable code rather than title
- Preview Images - Use preview images to help users select appropriate templates
- 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 workflowssrc/components/PublishToStoryteller/- Complete form implementation
Related Documentation#
- Executing Workflows - Use templates in workflow execution
- Validation API - Validate workflow payloads before execution
- Getting Workflows - Get available workflows that use templates
- Authentication - API key setup and usage
- Troubleshooting - Common issues and solutions