Skip to main content

Overview

This guide walks you through the complete workflow of managing content in Haystack, from creating your organizational structure to publishing items.

Prerequisites

Before you begin, ensure you have:
  • A Haystack account with API access
  • Your API token (see Authentication)
  • At least one collection created

Step 1: Set Up Your Organization

Create Collections

Collections are the top-level categories for your content. Create them first:
const collections = [
  { name: 'Sunday Sermons' },
  { name: 'Midweek Studies' },
  { name: 'Podcasts' }
];

for (const collection of collections) {
  const response = await fetch(`${API_URL}/collections/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(collection)
  });

  const { collection: created } = await response.json();
  console.log(`Created collection: ${created.name} (ID: ${created.id})`);
}

Create Speakers

Add the people who deliver your content:
const speakers = [
  {
    name: 'Pastor John Smith',
    bio: 'Senior Pastor since 2015, passionate about biblical teaching and discipleship.'
  },
  {
    name: 'Sarah Johnson',
    bio: 'Youth Pastor and conference speaker focusing on student ministry.'
  }
];

for (const speaker of speakers) {
  const response = await fetch(`${API_URL}/speakers/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(speaker)
  });

  const { speaker: created } = await response.json();
  console.log(`Created speaker: ${created.name} (ID: ${created.id})`);
}

Create Topics

Define the topics/tags you’ll use to categorize content:
const topics = [
  { name: 'Prayer', displayOrder: 1 },
  { name: 'Faith', displayOrder: 2 },
  { name: 'Leadership', displayOrder: 3 },
  { name: 'Relationships', displayOrder: 4 },
  { name: 'Worship', displayOrder: 5 }
];

for (const topic of topics) {
  const response = await fetch(`${API_URL}/topics/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(topic)
  });

  const { topic: created } = await response.json();
  console.log(`Created topic: ${created.name} (ID: ${created.id})`);
}

Step 2: Create a Series (Optional)

For multi-part content, create a series:
const seriesData = {
  title: 'Spiritual Disciplines',
  subTitle: 'A 4-Week Journey',
  description: 'Explore the foundational practices of prayer, fasting, worship, and scripture study that deepen your relationship with God.',
  shortDescription: 'Master the spiritual disciplines',
  collectionId: 1, // Sunday Sermons
  colorHex: '#4A90E2',
  itemSortDirection: 'ASC', // Show oldest first
  showItemOrderInSeries: true, // Show "Part 1", "Part 2", etc.
  published: true
};

const response = await fetch(`${API_URL}/series/create`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(seriesData)
});

const { series } = await response.json();
console.log(`Created series: ${series.title} (ID: ${series.id})`);

Upload Series Artwork

Add visual branding to your series:
const formData = new FormData();
formData.append('file', seriesArtworkFile); // Your image file
formData.append('artworkType', 'square'); // or 'wide'

const response = await fetch(
  `${API_URL}/series/${series.id}/artwork/upload`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    },
    body: formData
  }
);
Artwork requirements:
  • Square: 1:1 aspect ratio, minimum 1000x1000px
  • Wide: 16:9 aspect ratio, minimum 1920x1080px
  • Max file size: 15MB
  • Formats: JPG, PNG

Step 3: Create an Item

Now create your first content item:
const itemData = {
  // Basic information
  title: 'The Power of Prayer',
  subTitle: 'Part 1 of Spiritual Disciplines',
  description: 'Discover how prayer transforms not just our circumstances, but our hearts. Learn practical ways to develop a consistent prayer life and experience deeper intimacy with God.',
  shortDescription: 'Transform your prayer life',
  date: '2025-01-15',

  // Required: Collection
  collectionId: 1,

  // Optional: Series
  seriesId: series.id,

  // Optional: Speakers (array of IDs)
  speakers: [
    { id: 1 } // Pastor John Smith
  ],

  // Optional: Topics (array of IDs)
  topics: [
    { id: 1 }, // Prayer
    { id: 2 }  // Faith
  ],

  // Publishing (initially draft)
  published: false
};

const response = await fetch(`${API_URL}/items/create`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(itemData)
});

const { item } = await response.json();
console.log(`Created item: ${item.title} (ID: ${item.id})`);

Step 4: Add Item Artwork

Upload thumbnail images for the item:
// Upload square artwork (for grids)
const squareFormData = new FormData();
squareFormData.append('file', squareArtworkFile);
squareFormData.append('artworkType', 'square');

await fetch(`${API_URL}/items/${item.id}/artwork/upload`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`
  },
  body: squareFormData
});

// Upload wide artwork (for hero images)
const wideFormData = new FormData();
wideFormData.append('file', wideArtworkFile);
wideFormData.append('artworkType', 'wide');

await fetch(`${API_URL}/items/${item.id}/artwork/upload`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`
  },
  body: wideFormData
});

console.log('Artwork uploaded successfully');

Step 5: Add Scriptures (Optional)

Link Bible passages to your item:
const scriptures = [
  {
    itemId: item.id,
    book: 'matthew',
    chapter: 6,
    verseStart: 9,
    verseEnd: 13
  },
  {
    itemId: item.id,
    book: '1_thessalonians',
    chapter: 5,
    verseStart: 17,
    verseEnd: 17
  }
];

for (const scripture of scriptures) {
  await fetch(`${API_URL}/scriptures/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(scripture)
  });
}

console.log('Scriptures added');

Step 6: Publish the Item

Once everything is ready, publish the item:
const response = await fetch(`${API_URL}/items/${item.id}/publish`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`
  }
});

const { item: publishedItem } = await response.json();
console.log(`Published: ${publishedItem.title}`);
Items must have at least one media asset attached before they can be published. See the Working with Media guide to learn how to upload media.

Updating Content

Update Item Metadata

Modify an existing item:
const updates = {
  title: 'The Transforming Power of Prayer', // Updated title
  description: 'Updated description...',
  topics: [
    { id: 1 }, // Prayer
    { id: 5 }  // Worship (added)
  ]
};

const response = await fetch(`${API_URL}/items/${item.id}`, {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(updates)
});

const { item: updatedItem } = await response.json();

Unpublish an Item

Take an item offline:
await fetch(`${API_URL}/items/${item.id}/unpublish`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_TOKEN}`
  }
});

Bulk Operations

Creating Multiple Items

When importing a library, create items in batches:
const items = [
  { title: 'Item 1', date: '2025-01-01', collectionId: 1 },
  { title: 'Item 2', date: '2025-01-08', collectionId: 1 },
  { title: 'Item 3', date: '2025-01-15', collectionId: 1 }
];

// Create items sequentially
for (const itemData of items) {
  const response = await fetch(`${API_URL}/items/create`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(itemData)
  });

  const { item } = await response.json();
  console.log(`Created: ${item.title} (ID: ${item.id})`);

  // Small delay to avoid rate limits
  await new Promise(resolve => setTimeout(resolve, 100));
}
For large bulk imports (>100 items), consider batching your requests and implementing retry logic for failed imports.

Querying Content

List Items

Retrieve items with filtering and pagination:
// Get published items from a specific series
const response = await fetch(
  `${API_URL}/items?seriesId=5&published=true&sortBy=date&sortDirection=DESC&page=1&pageSize=20`,
  {
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    }
  }
);

const data = await response.json();
console.log(`Found ${data.total} items`);
console.log(`Showing page ${data.page} of ${Math.ceil(data.total / data.pageSize)}`);

data.items.forEach(item => {
  console.log(`- ${item.title} (${item.date})`);
});
Fetch an item with all related data:
const response = await fetch(
  `${API_URL}/items/42?_expand=collection&_expand=series&_expand=speakers&_expand=mediaAssets`,
  {
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`
    }
  }
);

const { item } = await response.json();

// Now item includes nested objects
console.log(item.collection.name); // "Sunday Sermons"
console.log(item.series.title);    // "Spiritual Disciplines"
console.log(item.speakers[0].name); // "Pastor John Smith"

Common Patterns

Checking if Item Exists

async function itemExists(title, date, collectionId) {
  const response = await fetch(
    `${API_URL}/items?title=${encodeURIComponent(title)}&date=${date}&collectionId=${collectionId}`,
    {
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    }
  );

  const data = await response.json();
  return data.items.length > 0;
}

Finding Items by Speaker

async function getItemsBySpeaker(speakerId) {
  const response = await fetch(
    `${API_URL}/items?speakerId=${speakerId}&_expand=speakers`,
    {
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    }
  );

  const data = await response.json();
  return data.items;
}

Getting Latest Items

async function getLatestItems(limit = 10) {
  const response = await fetch(
    `${API_URL}/items?published=true&sortBy=date&sortDirection=DESC&pageSize=${limit}`,
    {
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`
      }
    }
  );

  const data = await response.json();
  return data.items;
}

Best Practices

Design your collections before importing content. Changing an item’s collection later requires updating the item, which can affect analytics.
Maintain consistent naming conventions for speakers and topics to avoid duplicates.
  • ✅ “John Smith” (everywhere)
  • ❌ “John Smith”, “Pastor John”, “Rev. Smith” (creates duplicates)
Create items as drafts, add all metadata and media, then publish when complete.
Rich metadata (descriptions, topics, scriptures) improves search quality and user experience.

Error Handling

Always handle errors gracefully:
async function createItemSafely(itemData) {
  try {
    const response = await fetch(`${API_URL}/items/create`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(itemData)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error.message);
    }

    const { item } = await response.json();
    return item;
  } catch (error) {
    console.error('Failed to create item:', error.message);
    throw error;
  }
}

Next Steps