Webhooks
Receive real-time notifications when events occur on the platform.
Overview
Webhooks allow your application to receive HTTP callbacks when specific events occur, such as when an enrichment job completes. This enables real-time integrations without polling.
Setting Up Webhooks
Prerequisites
- A publicly accessible HTTPS endpoint
- Endpoint must respond with
200 OKwithin 30 seconds
Creating a Webhook
- Navigate to Settings → Webhooks in your dashboard
- Click Create Webhook
- Configure:
- URL: Your HTTPS endpoint
- Method: POST, PUT, or PATCH
- Events: Select event types to subscribe
- Enabled: Toggle on/off
- Save the webhook
Supported Events
Product Enrichment Events
product_enrichment.start
Fired when an enrichment job begins processing.
{
"webhookId": "webhook_abc123",
"event": "product_enrichment.start",
"payload": {
"id": "job_xyz789"
}
}
product_enrichment.completed
Fired when an enrichment job finishes.
{
"webhookId": "webhook_abc123",
"event": "product_enrichment.completed",
"payload": {
"id": "job_xyz789",
"status": "completed",
"completedAt": "2024-01-15T14:30:00Z"
}
}
Status values:
completed- All products enriched successfullycompleted_with_errors- Some products failedfailed- Job failed entirely
product_enrichment.stopped
Fired when an enrichment job is manually stopped.
{
"webhookId": "webhook_abc123",
"event": "product_enrichment.stopped",
"payload": {
"id": "job_xyz789",
"status": "stopped",
"stoppedAt": "2024-01-15T14:30:00Z"
}
}
Product Validation Events
product_validation.start
Fired when a validation job begins processing.
{
"webhookId": "webhook_abc123",
"event": "product_validation.start",
"payload": {
"id": "job_val789"
}
}
product_validation.completed
Fired when a validation job finishes.
{
"webhookId": "webhook_abc123",
"event": "product_validation.completed",
"payload": {
"id": "job_val789",
"status": "completed",
"completedAt": "2024-01-15T14:30:00Z"
}
}
Status values:
completed- All products validated successfullycompleted_with_errors- Some products failedfailed- Job failed entirely
product_validation.stopped
Fired when a validation job is manually stopped.
{
"webhookId": "webhook_abc123",
"event": "product_validation.stopped",
"payload": {
"id": "job_val789",
"status": "stopped",
"stoppedAt": "2024-01-15T14:30:00Z"
}
}
Market Analysis Events
product_market_analysis.start
Fired when a market analysis job begins processing.
{
"webhookId": "webhook_abc123",
"event": "product_market_analysis.start",
"payload": {
"id": "job_ma789"
}
}
product_market_analysis.completed
Fired when a market analysis job finishes.
{
"webhookId": "webhook_abc123",
"event": "product_market_analysis.completed",
"payload": {
"id": "job_ma789",
"status": "completed",
"completedAt": "2024-01-15T14:30:00Z"
}
}
Status values:
completed- All products analysed successfullycompleted_with_errors- Some products failed
AI Import Events
ai_import.start
Fired when an AI Import job begins processing.
The payload key here is jobId, not id. Every other event uses id, so read this one explicitly rather than through a shared handler.
{
"webhookId": "webhook_abc123",
"event": "ai_import.start",
"payload": {
"jobId": "job_imp456"
}
}
ai_import.stopped
Fired when an AI Import job is manually stopped.
{
"webhookId": "webhook_abc123",
"event": "ai_import.stopped",
"payload": {
"id": "job_imp456",
"status": "stopped",
"stoppedAt": "2024-01-15T14:30:00Z"
}
}
Events that can be subscribed to but do not fire
Three event types are selectable when creating a webhook and are never dispatched by the platform today. Subscribing to them is harmless, but nothing will arrive:
product_market_analysis.stoppedai_import.completedai_import.failed
To detect a finished AI Import, poll POST /api/{organization}/ai-import-jobs/get rather than waiting for ai_import.completed.
Implementing Your Endpoint
Basic Example (Node.js/Express)
app.post('/webhooks/catalog-ai', (req, res) => {
const { event, payload } = req.body
switch (event) {
case 'product_enrichment.start':
console.log(`Enrichment job started: ${payload.id}`)
break
case 'product_enrichment.completed':
console.log(`Enrichment job completed: ${payload.id}`)
// Trigger downstream workflow
processEnrichedProducts(payload.id)
break
case 'product_enrichment.stopped':
console.log(`Enrichment job stopped: ${payload.id}`)
// Handle partial results
handlePartialEnrichment(payload.id)
break
case 'product_validation.start':
console.log(`Validation job started: ${payload.id}`)
break
case 'product_validation.completed':
console.log(`Validation job completed: ${payload.id}`)
processValidationResults(payload.id)
break
case 'product_validation.stopped':
console.log(`Validation job stopped: ${payload.id}`)
handlePartialValidation(payload.id)
break
}
// Always respond with 200 OK
res.status(200).send('OK')
})
Python (Flask)
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/catalog-ai', methods=['POST'])
def handle_webhook():
data = request.json
event = data.get('event')
payload = data.get('payload')
if event == 'product_enrichment.completed':
job_id = payload.get('id')
# Process completed job
process_enriched_products(job_id)
return 'OK', 200
Webhook Best Practices
Respond Quickly
Return 200 OK immediately, then process asynchronously:
app.post('/webhooks/catalog-ai', async (req, res) => {
// Respond immediately
res.status(200).send('OK')
// Process in background
setImmediate(() => {
processWebhook(req.body)
})
})
Handle Failures Gracefully
Implement your own retry logic for downstream failures:
async function processWebhook(data) {
try {
await syncToInventorySystem(data)
} catch (error) {
// Queue for retry
await retryQueue.add('sync-inventory', data)
}
}
Validate Event Types
Only process expected events:
const EXPECTED_EVENTS = [
'product_enrichment.start',
'product_enrichment.completed',
'product_enrichment.stopped',
'product_validation.start',
'product_validation.completed',
'product_validation.stopped',
'product_market_analysis.start',
'product_market_analysis.completed',
'ai_import.start',
'ai_import.stopped'
]
app.post('/webhooks/catalog-ai', (req, res) => {
const { event } = req.body
if (!EXPECTED_EVENTS.includes(event)) {
console.warn(`Unexpected event: ${event}`)
return res.status(200).send('OK')
}
// Process event...
res.status(200).send('OK')
})
Implement Idempotency
Handle duplicate deliveries:
const processedEvents = new Set()
app.post('/webhooks/catalog-ai', (req, res) => {
const eventKey = `${req.body.webhookId}-${req.body.event}`
if (processedEvents.has(eventKey)) {
return res.status(200).send('OK')
}
processedEvents.add(eventKey)
// Process event...
res.status(200).send('OK')
})
Managing Webhooks
Viewing Webhooks
Navigate to Settings → Webhooks to see all configured webhooks.
Editing Webhooks
Click on a webhook to update its URL, method, or event subscriptions.
Disabling Webhooks
Toggle the Enabled switch to temporarily disable without deleting.
Deleting Webhooks
Click Delete to permanently remove a webhook.
Managing Webhooks Over the API
Everything the Settings screen does is available on one endpoint. The action is chosen with the action field in the body rather than by HTTP verb, so all four operations are a POST.
Endpoint: POST /api/{organization}/webhooks
Authentication is the same API key used everywhere else. See Authentication.
List webhooks
{
"action": "list"
}
Returns every webhook for the organization, newest first.
Create a webhook
{
"action": "create",
"url": "https://example.com/webhooks/catalog-ai",
"method": "post",
"eventType": ["product_enrichment.completed", "product_validation.completed"],
"enabled": true
}
| Field | Required | Notes |
|---|---|---|
url |
Yes | Must be a valid URL |
method |
No | post, put, or patch. Defaults to post |
eventType |
Yes | Array of event types, at least one |
enabled |
No | Defaults to true |
Creating an eleventh webhook returns a 400 with Webhook limit reached. You can create up to 10 webhooks.
Update a webhook
Only the fields you send are changed.
{
"action": "update",
"id": "webhook_abc123",
"enabled": false
}
Returns 404 when the id does not belong to the organization the key is scoped to.
Delete a webhook
{
"action": "delete",
"id": "webhook_abc123"
}
Returns { "success": true, "data": { "deleted": true } }, or 404 when the id is not found in this organization.
Example
curl -X POST https://catalog-ai.tdcapps.com/api/your-org/webhooks \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"action": "create",
"url": "https://example.com/webhooks/catalog-ai",
"eventType": ["product_enrichment.completed"]
}'
Webhook Limits
| Limit | Value |
|---|---|
| Maximum webhooks per organization | 10 |
| Request timeout | 30 seconds |
| Automatic retries | None |
Troubleshooting
Webhook Not Received
Check:
- Webhook is enabled
- Event type is subscribed
- URL is publicly accessible
- Endpoint returns
200 OK - HTTPS certificate is valid
Timeout Errors
If your endpoint takes too long:
- Return
200 OKimmediately - Process the event asynchronously
- Use a queue for heavy operations
Use Cases
Sync to External Systems
Automatically sync enriched data to your inventory or PIM system:
case 'product_enrichment.completed':
const items = await getJobItems(payload.id)
await syncToInventorySystem(items)
break
Send Notifications
Alert your team when jobs complete:
case 'product_enrichment.completed':
await sendSlackNotification(
`Enrichment job ${payload.id} completed with status: ${payload.status}`
)
break
Trigger Workflows
Start downstream processes automatically:
case 'product_enrichment.completed':
if (payload.status === 'completed') {
await startProductPublishWorkflow(payload.id)
}
break