Catalog AI
Sign In

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 OK within 30 seconds

Creating a Webhook

  1. Navigate to Settings → Webhooks in your dashboard
  2. Click Create Webhook
  3. Configure:
    • URL: Your HTTPS endpoint
    • Method: POST, PUT, or PATCH
    • Events: Select event types to subscribe
    • Enabled: Toggle on/off
  4. 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 successfully
  • completed_with_errors - Some products failed
  • failed - 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 successfully
  • completed_with_errors - Some products failed
  • failed - 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 successfully
  • completed_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.stopped
  • ai_import.completed
  • ai_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 OK immediately
  • 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