Heppu AI

Batch Sources

Create or update up to 50 knowledge base sources in a single request

Batch Sources

Create or update multiple sources in a single API call for efficient bulk operations.

Endpoint

POST /api/v1/kb/sources/batch

Authentication

# API Key
x-api-key: YOUR_API_KEY

# Bearer Token
Authorization: Bearer YOUR_API_KEY

Request Body

FieldTypeRequiredDescription
collectionIdstringYesID of the knowledge collection
sourcesarrayYesArray of source objects (max: 50)

Source Object

Each source in the array:

FieldTypeRequiredDescription
externalIdstringYesYour unique identifier (max 255 chars)
titlestringYesDisplay title (max 500 chars)
contentstringYesMarkdown content (max 500KB)
metadataobjectNoAdditional metadata

Response

{
  "data": {
    "results": [
      {
        "index": 0,
        "externalId": "string",
        "documentId": "uuid",
        "status": "created" | "updated" | "unchanged" | "failed",
        "message": "string",
        "error": "string|null"
      }
    ],
    "summary": {
      "total": "integer",
      "created": "integer",
      "updated": "integer",
      "unchanged": "integer",
      "failed": "integer"
    }
  },
  "meta": {
    "timestamp": "ISO 8601"
  }
}

Status values:

  • created - New source created and queued for processing
  • updated - Existing source updated and queued for reprocessing
  • unchanged - Content hash matches, no processing needed
  • failed - Validation or processing error

HTTP Status Codes:

  • 201 - All sources processed successfully (created/updated/unchanged)
  • 207 - Partial success (some failed)
  • 400 - All sources failed or validation error

Examples

Basic Batch Create

curl -X POST https://v2.heppu.ai/api/v1/kb/sources/batch \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collectionId": "550e8400-e29b-41d4-a716-446655440000",
    "sources": [
      {
        "externalId": "car-001",
        "title": "2024 Toyota Camry",
        "content": "# 2024 Toyota Camry\n\nReliable mid-size sedan..."
      },
      {
        "externalId": "car-002",
        "title": "2024 Honda Accord",
        "content": "# 2024 Honda Accord\n\nPopular family sedan..."
      },
      {
        "externalId": "car-003",
        "title": "2024 Mazda 6",
        "content": "# 2024 Mazda 6\n\nSporty mid-size sedan..."
      }
    ]
  }'
const response = await fetch('https://v2.heppu.ai/api/v1/kb/sources/batch', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    collectionId: '550e8400-e29b-41d4-a716-446655440000',
    sources: [
      {
        externalId: 'car-001',
        title: '2024 Toyota Camry',
        content: '# 2024 Toyota Camry\n\nReliable mid-size sedan...'
      },
      {
        externalId: 'car-002',
        title: '2024 Honda Accord',
        content: '# 2024 Honda Accord\n\nPopular family sedan...'
      },
      {
        externalId: 'car-003',
        title: '2024 Mazda 6',
        content: '# 2024 Mazda 6\n\nSporty mid-size sedan...'
      }
    ]
  })
});

const data = await response.json();
const { summary } = data.data;
console.log(`Created: ${summary.created}, Updated: ${summary.updated}, Unchanged: ${summary.unchanged}`);
import requests

response = requests.post(
    'https://v2.heppu.ai/api/v1/kb/sources/batch',
    headers={'x-api-key': 'YOUR_API_KEY'},
    json={
        'collectionId': '550e8400-e29b-41d4-a716-446655440000',
        'sources': [
            {
                'externalId': 'car-001',
                'title': '2024 Toyota Camry',
                'content': '# 2024 Toyota Camry\n\nReliable mid-size sedan...'
            },
            {
                'externalId': 'car-002',
                'title': '2024 Honda Accord',
                'content': '# 2024 Honda Accord\n\nPopular family sedan...'
            },
            {
                'externalId': 'car-003',
                'title': '2024 Mazda 6',
                'content': '# 2024 Mazda 6\n\nSporty mid-size sedan...'
            }
        ]
    }
)

data = response.json()
summary = data['data']['summary']
print(f"Created: {summary['created']}, Updated: {summary['updated']}, Unchanged: {summary['unchanged']}")

Inventory Sync Pattern

Sync inventory from your system - creates new items and updates existing ones:

curl -X POST https://v2.heppu.ai/api/v1/kb/sources/batch \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collectionId": "550e8400-e29b-41d4-a716-446655440000",
    "sources": [
      {
        "externalId": "vin-5UXCR6C05R9S12345",
        "title": "2024 BMW X5 xDrive40i",
        "content": "# 2024 BMW X5 xDrive40i\n\n**Price:** $65,200\n**Mileage:** New\n**Color:** Alpine White\n\n## Features\n- Panoramic sunroof\n- Harman Kardon sound\n- M Sport package",
        "metadata": {
          "vin": "5UXCR6C05R9S12345",
          "price": 65200,
          "status": "available"
        }
      },
      {
        "externalId": "vin-WBAPH5C55BA123456",
        "title": "2023 BMW 5 Series 530i",
        "content": "# 2023 BMW 5 Series 530i\n\n**Price:** $52,900\n**Mileage:** 8,500 miles\n**Color:** Carbon Black\n\n## Features\n- Sport line\n- Navigation\n- Heated seats",
        "metadata": {
          "vin": "WBAPH5C55BA123456",
          "price": 52900,
          "status": "available"
        }
      }
    ]
  }'
async function syncInventory(collectionId, vehicles) {
  // Transform vehicles to source format
  const sources = vehicles.map(vehicle => ({
    externalId: `vin-${vehicle.vin}`,
    title: `${vehicle.year} ${vehicle.make} ${vehicle.model}`,
    content: formatVehicleContent(vehicle),
    metadata: {
      vin: vehicle.vin,
      price: vehicle.price,
      status: vehicle.status,
      lastUpdated: new Date().toISOString()
    }
  }));

  // Process in batches of 50
  const batchSize = 50;
  const results = [];

  for (let i = 0; i < sources.length; i += batchSize) {
    const batch = sources.slice(i, i + batchSize);

    const response = await fetch('https://v2.heppu.ai/api/v1/kb/sources/batch', {
      method: 'POST',
      headers: {
        'x-api-key': 'YOUR_API_KEY',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        collectionId,
        sources: batch
      })
    });

    const data = await response.json();
    results.push(data.data.summary);

    console.log(`Batch ${Math.floor(i / batchSize) + 1}: ${data.data.summary.created} created, ${data.data.summary.updated} updated`);
  }

  return results;
}

function formatVehicleContent(vehicle) {
  return `# ${vehicle.year} ${vehicle.make} ${vehicle.model}

**Price:** $${vehicle.price.toLocaleString()}
**Mileage:** ${vehicle.mileage === 0 ? 'New' : `${vehicle.mileage.toLocaleString()} miles`}
**Color:** ${vehicle.exteriorColor}

## Features
${vehicle.features.map(f => `- ${f}`).join('\n')}

## Description
${vehicle.description}`;
}
def sync_inventory(collection_id, vehicles):
    """Sync vehicle inventory to knowledge base"""

    # Transform vehicles to source format
    sources = [{
        'externalId': f"vin-{v['vin']}",
        'title': f"{v['year']} {v['make']} {v['model']}",
        'content': format_vehicle_content(v),
        'metadata': {
            'vin': v['vin'],
            'price': v['price'],
            'status': v['status'],
            'lastUpdated': datetime.utcnow().isoformat() + 'Z'
        }
    } for v in vehicles]

    # Process in batches of 50
    batch_size = 50
    results = []

    for i in range(0, len(sources), batch_size):
        batch = sources[i:i + batch_size]

        response = requests.post(
            'https://v2.heppu.ai/api/v1/kb/sources/batch',
            headers={'x-api-key': 'YOUR_API_KEY'},
            json={
                'collectionId': collection_id,
                'sources': batch
            }
        )

        data = response.json()
        summary = data['data']['summary']
        results.append(summary)

        print(f"Batch {i // batch_size + 1}: {summary['created']} created, {summary['updated']} updated")

    return results


def format_vehicle_content(vehicle):
    """Format vehicle data as markdown"""
    mileage = "New" if vehicle['mileage'] == 0 else f"{vehicle['mileage']:,} miles"
    features = '\n'.join(f"- {f}" for f in vehicle['features'])

    return f"""# {vehicle['year']} {vehicle['make']} {vehicle['model']}

**Price:** ${vehicle['price']:,}
**Mileage:** {mileage}
**Color:** {vehicle['exteriorColor']}

## Features
{features}

## Description
{vehicle['description']}"""

Handle Partial Success

# Response with partial success (HTTP 207)
{
  "data": {
    "results": [
      {
        "index": 0,
        "externalId": "car-001",
        "documentId": "abc-123",
        "status": "created",
        "message": "Source queued for processing"
      },
      {
        "index": 1,
        "externalId": "car-002",
        "status": "failed",
        "error": "Validation error: content is required"
      },
      {
        "index": 2,
        "externalId": "car-003",
        "documentId": "def-456",
        "status": "unchanged",
        "message": "Content unchanged, no processing required"
      }
    ],
    "summary": {
      "total": 3,
      "created": 1,
      "updated": 0,
      "unchanged": 1,
      "failed": 1
    }
  }
}
async function syncWithErrorHandling(collectionId, sources) {
  const response = await fetch('https://v2.heppu.ai/api/v1/kb/sources/batch', {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ collectionId, sources })
  });

  const data = await response.json();
  const { results, summary } = data.data;

  console.log(`Summary: ${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.failed} failed`);

  // Handle failures
  const failures = results.filter(r => r.status === 'failed');
  if (failures.length > 0) {
    console.log('\nFailed sources:');
    failures.forEach(f => {
      console.log(`- ${f.externalId}: ${f.error}`);
    });

    // Optionally retry with corrected data
    const retryData = failures
      .filter(f => f.error?.includes('content'))
      .map(f => {
        const original = sources.find(s => s.externalId === f.externalId);
        return {
          ...original,
          content: original.content || 'No content available'
        };
      });

    if (retryData.length > 0) {
      console.log(`\nRetrying ${retryData.length} sources...`);
      return syncWithErrorHandling(collectionId, retryData);
    }
  }

  return data;
}
def sync_with_error_handling(collection_id, sources):
    """Sync sources with error handling and retry logic"""

    response = requests.post(
        'https://v2.heppu.ai/api/v1/kb/sources/batch',
        headers={'x-api-key': 'YOUR_API_KEY'},
        json={'collectionId': collection_id, 'sources': sources}
    )

    data = response.json()
    results = data['data']['results']
    summary = data['data']['summary']

    print(f"Summary: {summary['created']} created, {summary['updated']} updated, "
          f"{summary['unchanged']} unchanged, {summary['failed']} failed")

    # Handle failures
    failures = [r for r in results if r['status'] == 'failed']
    if failures:
        print('\nFailed sources:')
        for f in failures:
            print(f"- {f['externalId']}: {f.get('error', 'Unknown error')}")

        # Optionally log for investigation
        failed_ids = [f['externalId'] for f in failures]
        with open('failed_sources.json', 'w') as file:
            json.dump({'failed': failures, 'timestamp': datetime.utcnow().isoformat()}, file)

    return data

Daily Inventory Sync Script

Complete example for daily inventory synchronization:

async function dailyInventorySync() {
  const collectionId = process.env.KB_COLLECTION_ID;
  const apiKey = process.env.HEPPU_API_KEY;

  console.log('Starting daily inventory sync...');

  // 1. Fetch current inventory from your system
  const currentInventory = await fetchInventoryFromDealerSystem();
  console.log(`Fetched ${currentInventory.length} vehicles`);

  // 2. Get existing sources from Heppu
  const existingResponse = await fetch(
    `https://v2.heppu.ai/api/v1/kb/sources?collectionId=${collectionId}&limit=100`,
    { headers: { 'x-api-key': apiKey } }
  );
  const existingData = await existingResponse.json();
  const existingIds = new Set(existingData.data.sources.map(s => s.externalId));

  // 3. Sync current inventory
  const sources = currentInventory.map(vehicle => ({
    externalId: `vin-${vehicle.vin}`,
    title: `${vehicle.year} ${vehicle.make} ${vehicle.model}`,
    content: formatVehicleContent(vehicle),
    metadata: { vin: vehicle.vin, price: vehicle.price, status: 'available' }
  }));

  // Process in batches
  for (let i = 0; i < sources.length; i += 50) {
    const batch = sources.slice(i, i + 50);
    await fetch('https://v2.heppu.ai/api/v1/kb/sources/batch', {
      method: 'POST',
      headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({ collectionId, sources: batch })
    });
    console.log(`Synced batch ${Math.floor(i / 50) + 1}`);
  }

  // 4. Delete sold vehicles
  const currentIds = new Set(sources.map(s => s.externalId));
  const soldIds = [...existingIds].filter(id => !currentIds.has(id));

  if (soldIds.length > 0) {
    console.log(`Deleting ${soldIds.length} sold vehicles...`);
    await fetch('https://v2.heppu.ai/api/v1/kb/sources', {
      method: 'DELETE',
      headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({ collectionId, externalIds: soldIds })
    });
  }

  console.log('Daily sync complete!');
}
def daily_inventory_sync():
    """Complete daily inventory sync workflow"""

    collection_id = os.environ['KB_COLLECTION_ID']
    api_key = os.environ['HEPPU_API_KEY']

    print('Starting daily inventory sync...')

    # 1. Fetch current inventory from your system
    current_inventory = fetch_inventory_from_dealer_system()
    print(f'Fetched {len(current_inventory)} vehicles')

    # 2. Get existing sources from Heppu
    existing_response = requests.get(
        'https://v2.heppu.ai/api/v1/kb/sources',
        headers={'x-api-key': api_key},
        params={'collectionId': collection_id, 'limit': 100}
    )
    existing_data = existing_response.json()
    existing_ids = {s['externalId'] for s in existing_data['data']['sources']}

    # 3. Sync current inventory
    sources = [{
        'externalId': f"vin-{v['vin']}",
        'title': f"{v['year']} {v['make']} {v['model']}",
        'content': format_vehicle_content(v),
        'metadata': {'vin': v['vin'], 'price': v['price'], 'status': 'available'}
    } for v in current_inventory]

    # Process in batches of 50
    for i in range(0, len(sources), 50):
        batch = sources[i:i + 50]
        requests.post(
            'https://v2.heppu.ai/api/v1/kb/sources/batch',
            headers={'x-api-key': api_key},
            json={'collectionId': collection_id, 'sources': batch}
        )
        print(f'Synced batch {i // 50 + 1}')

    # 4. Delete sold vehicles
    current_ids = {s['externalId'] for s in sources}
    sold_ids = list(existing_ids - current_ids)

    if sold_ids:
        print(f'Deleting {len(sold_ids)} sold vehicles...')
        requests.delete(
            'https://v2.heppu.ai/api/v1/kb/sources',
            headers={'x-api-key': api_key},
            json={'collectionId': collection_id, 'externalIds': sold_ids}
        )

    print('Daily sync complete!')


if __name__ == '__main__':
    daily_inventory_sync()

Error Responses

Exceeds Batch Limit

{
  "error": {
    "message": "Validation error: Maximum 50 sources per batch",
    "status": 400,
    "timestamp": "ISO 8601"
  }
}

Collection Not Found

{
  "error": {
    "message": "Collection not found or does not belong to your organization",
    "status": 404,
    "timestamp": "ISO 8601"
  }
}

All Sources Failed

{
  "data": {
    "results": [
      {"index": 0, "externalId": "x", "status": "failed", "error": "content is required"},
      {"index": 1, "externalId": "y", "status": "failed", "error": "content is required"}
    ],
    "summary": {"total": 2, "created": 0, "updated": 0, "unchanged": 0, "failed": 2}
  },
  "meta": {"timestamp": "ISO 8601"}
}

HTTP 400

Best Practices

  1. Use batches for efficiency - Batch operations are faster than individual calls
  2. Process large datasets in chunks - Split datasets larger than 50 into multiple batch calls
  3. Handle partial success - Check individual results even on success status codes
  4. Track with externalIds - Use stable IDs from your source system
  5. Monitor summary stats - Log created/updated/unchanged/failed counts for visibility
  6. Implement retry logic - Network issues may cause temporary failures

Rate Limits

  • 50 sources per batch - Maximum sources per request
  • 5 concurrent processing - Inngest processes up to 5 sources simultaneously

For larger syncs, implement a queue or process batches sequentially with appropriate delays.

On this page