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/batchAuthentication
# API Key
x-api-key: YOUR_API_KEY
# Bearer Token
Authorization: Bearer YOUR_API_KEYRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
collectionId | string | Yes | ID of the knowledge collection |
sources | array | Yes | Array of source objects (max: 50) |
Source Object
Each source in the array:
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | Your unique identifier (max 255 chars) |
title | string | Yes | Display title (max 500 chars) |
content | string | Yes | Markdown content (max 500KB) |
metadata | object | No | Additional 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 processingupdated- Existing source updated and queued for reprocessingunchanged- Content hash matches, no processing neededfailed- 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 dataDaily 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
- Use batches for efficiency - Batch operations are faster than individual calls
- Process large datasets in chunks - Split datasets larger than 50 into multiple batch calls
- Handle partial success - Check individual results even on success status codes
- Track with externalIds - Use stable IDs from your source system
- Monitor summary stats - Log created/updated/unchanged/failed counts for visibility
- 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.
Related
- Sources - Single source operations (POST, GET, DELETE)
- Knowledge Base Overview - API overview and quick start