Skip to main content

Overview

While ButterCMS doesn’t provide dedicated bulk endpoints, you can perform efficient bulk operations by orchestrating multiple Write API calls. This guide covers patterns and best practices for bulk content operations.
Write API access requires a Write API Token. Contact support@buttercms.com to request access.

Key Considerations

  • Request Pacing: Add small delays between requests to keep bulk operations stable and reduce monthly usage spikes.
  • Error Handling: Implement retry logic for transient failures.
  • Progress Tracking: Track successful and failed operations for large imports.
  • Async Nature: Write operations return 202 Accepted—content typically appears within seconds but processing time may vary.

Bulk Create Pattern

Sequential Creation with Request Pacing

async function bulkCreate(collectionKey, items, delayMs = 200) {
  const results = {
    success: [],
    failed: []
  };

  for (const item of items) {
    try {
      const response = await axios.post(
        `https://api.buttercms.com/v2/content/${collectionKey}/`,
        {
          status: 'draft',
          fields: item
        },
        {
          headers: {
            'Authorization': `Token ${WRITE_TOKEN}`,
            'Content-Type': 'application/json'
          }
        }
      );

      results.success.push({
        item,
        id: response.data.data.id
      });
    } catch (error) {
      results.failed.push({
        item,
        error: error.response?.data || error.message
      });
    }

    // Rate limiting delay
    await new Promise(r => setTimeout(r, delayMs));
  }

  return results;
}

// Usage
const items = [
  { name: 'Item 1', category: 'A' },
  { name: 'Item 2', category: 'B' },
  { name: 'Item 3', category: 'A' }
];

const results = await bulkCreate('products', items);
console.log(`Created: ${results.success.length}, Failed: ${results.failed.length}`);

Parallel Creation with Concurrency Control

For faster bulk operations, use controlled concurrency:
const pLimit = require('p-limit');

async function bulkCreateParallel(collectionKey, items, concurrency = 5) {
  const limit = pLimit(concurrency);
  const results = { success: [], failed: [] };

  const promises = items.map(item =>
    limit(async () => {
      try {
        const response = await axios.post(
          `https://api.buttercms.com/v2/content/${collectionKey}/`,
          { status: 'draft', fields: item },
          { headers: { 'Authorization': `Token ${WRITE_TOKEN}` } }
        );
        results.success.push({ item, id: response.data.data.id });
      } catch (error) {
        results.failed.push({ item, error: error.response?.data });
      }
    })
  );

  await Promise.all(promises);
  return results;
}

Bulk Update Pattern

Update Multiple Items by ID

async function bulkUpdate(collectionKey, updates, delayMs = 200) {
  const results = { success: [], failed: [] };

  for (const { id, fields } of updates) {
    try {
      await axios.patch(
        `https://api.buttercms.com/v2/content/${collectionKey}/${id}/`,
        { fields },
        {
          headers: {
            'Authorization': `Token ${WRITE_TOKEN}`,
            'Content-Type': 'application/json'
          }
        }
      );
      results.success.push({ id });
    } catch (error) {
      results.failed.push({ id, error: error.response?.data });
    }

    await new Promise(r => setTimeout(r, delayMs));
  }

  return results;
}

// Usage
const updates = [
  { id: 123, fields: { status: 'active' } },
  { id: 124, fields: { status: 'active' } },
  { id: 125, fields: { status: 'inactive' } }
];

await bulkUpdate('products', updates);

Bulk Status Update

async function bulkPublish(collectionKey, itemIds) {
  const results = { success: [], failed: [] };

  for (const id of itemIds) {
    try {
      await axios.patch(
        `https://api.buttercms.com/v2/content/${collectionKey}/${id}/`,
        { status: 'published' },
        { headers: { 'Authorization': `Token ${WRITE_TOKEN}` } }
      );
      results.success.push(id);
    } catch (error) {
      results.failed.push({ id, error: error.response?.data });
    }

    await new Promise(r => setTimeout(r, 200));
  }

  return results;
}

Bulk Delete Pattern

async function bulkDelete(collectionKey, itemIds, delayMs = 200) {
  const results = { success: [], failed: [] };

  for (const id of itemIds) {
    try {
      await axios.delete(
        `https://api.buttercms.com/v2/content/${collectionKey}/${id}/`,
        { headers: { 'Authorization': `Token ${WRITE_TOKEN}` } }
      );
      results.success.push(id);
    } catch (error) {
      results.failed.push({ id, error: error.response?.data });
    }

    await new Promise(r => setTimeout(r, delayMs));
  }

  return results;
}

Content Migration Example

WordPress to ButterCMS Migration

const axios = require('axios');

const WRITE_TOKEN = 'your-write-token';
const API_BASE = 'https://api.buttercms.com/v2';

async function migrateWordPressPosts(wpPosts) {
  console.log(`Starting migration of ${wpPosts.length} posts...`);

  const results = {
    success: [],
    failed: [],
    skipped: []
  };

  for (let i = 0; i < wpPosts.length; i++) {
    const post = wpPosts[i];
    console.log(`[${i + 1}/${wpPosts.length}] Processing: ${post.title}`);

    // Skip if slug already exists
    try {
      await axios.get(
        `${API_BASE}/posts/${post.slug}/?auth_token=${READ_TOKEN}`
      );
      results.skipped.push({ slug: post.slug, reason: 'Already exists' });
      continue;
    } catch (e) {
      // Post doesn't exist, proceed with creation
    }

    try {
      const butterPost = {
        title: post.title,
        slug: post.slug,
        body: post.content,
        summary: post.excerpt || '',
        seo_title: post.seo_title || post.title,
        meta_description: post.meta_description || post.excerpt || '',
        featured_image: post.featured_image || '',
        status: post.status === 'publish' ? 'published' : 'draft',
        category_slugs: post.categories?.map(c => slugify(c.name)) || [],
        tag_slugs: post.tags?.map(t => slugify(t.name)) || []
      };

      await axios.post(
        `${API_BASE}/posts/`,
        butterPost,
        { headers: { 'Authorization': `Token ${WRITE_TOKEN}` } }
      );

      results.success.push({ slug: post.slug });
    } catch (error) {
      results.failed.push({
        slug: post.slug,
        error: error.response?.data || error.message
      });
    }

    // Rate limiting
    await new Promise(r => setTimeout(r, 300));
  }

  console.log('\nMigration complete:');
  console.log(`  Success: ${results.success.length}`);
  console.log(`  Failed: ${results.failed.length}`);
  console.log(`  Skipped: ${results.skipped.length}`);

  return results;
}

function slugify(text) {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, '')
    .replace(/\s+/g, '-')
    .trim();
}

Handling 429 Responses

async function requestWithRetryAfter(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error.response?.status === 429) {
        const retryAfter = Number(error.response.headers?.['retry-after'] || 0);
        console.log(`API limit reached. Retry after ${retryAfter}s...`);
        await new Promise(r => setTimeout(r, retryAfter * 1000));
        continue;
      }
      throw error;
    }
  }
}

// Usage in bulk operations
async function createItemWithRetry(collectionKey, fields) {
  return requestWithRetryAfter(() =>
    axios.post(
      `https://api.buttercms.com/v2/content/${collectionKey}/`,
      { status: 'draft', fields },
      { headers: { 'Authorization': `Token ${WRITE_TOKEN}` } }
    )
  );
}

Progress Tracking and Logging

class BulkOperationTracker {
  constructor(totalItems) {
    this.total = totalItems;
    this.processed = 0;
    this.success = 0;
    this.failed = 0;
    this.startTime = Date.now();
  }

  recordSuccess() {
    this.processed++;
    this.success++;
    this.logProgress();
  }

  recordFailure(error) {
    this.processed++;
    this.failed++;
    this.logProgress();
  }

  logProgress() {
    const elapsed = (Date.now() - this.startTime) / 1000;
    const rate = this.processed / elapsed;
    const remaining = (this.total - this.processed) / rate;

    console.log(
      `Progress: ${this.processed}/${this.total} ` +
      `(${this.success} success, ${this.failed} failed) ` +
      `ETA: ${Math.round(remaining)}s`
    );
  }

  getSummary() {
    return {
      total: this.total,
      success: this.success,
      failed: this.failed,
      duration: (Date.now() - this.startTime) / 1000
    };
  }
}

// Usage
const tracker = new BulkOperationTracker(items.length);

for (const item of items) {
  try {
    await createItem(item);
    tracker.recordSuccess();
  } catch (error) {
    tracker.recordFailure(error);
  }
}

console.log('Summary:', tracker.getSummary());

Best Practices

Request Pacing

Add 200-500ms delay between requests to keep bulk operations stable and reduce monthly API usage spikes.

Idempotency

Check if content exists before creating to avoid duplicates during retries.

Error Logging

Log failed operations for manual review and retry.

Batch Verification

After bulk operations, verify content via Read API.
Operation TypeRecommended ConcurrencyDelay Between Requests
Create3-5 parallel200ms
Update5-10 parallel100ms
Delete5-10 parallel100ms
Migration1-3 parallel300ms
These are recommended values to keep bulk operations stable. Free/Trial accounts can be blocked when monthly API call limits are exceeded.

Next Steps

Pages - Write

Create and update pages

Collections - Write

Create collection items

Blog Posts - Write

Create blog posts

Rate Limits

API rate limiting