DEV Community

Kseniia Shevchenko
Kseniia Shevchenko

Posted on

Building an AI-Powered Customer Support Layer with Oscar Chat API: A Developer's Guide

If you're a founder who writes code, you've probably built your product's support flow wrong at least once. I know I did — a patchwork of Intercom webhooks, a custom Node.js handler, a Slack bot that never quite worked, and three different inboxes nobody checked consistently.

This guide is the thing I wish existed when I started. We'll build a proper AI-first customer support system using the Oscar Chat API, cover the architecture decisions that matter, and look at real integration patterns for Shopify, webhooks, and custom automation flows.


The Architecture Before We Write a Single Line

Before touching the API, understand the mental model. Oscar Chat exposes four core primitives:

┌─────────────────────────────────────────────────────┐
│                   Oscar Chat API                     │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  │
│  │ Widgets  │  │  Inbox   │  │   AI Bot Engine  │  │
│  │  (embed) │  │ (unified)│  │  (GPT-4o / etc.) │  │
│  └────┬─────┘  └────┬─────┘  └────────┬─────────┘  │
│       │              │                  │             │
│  ┌────▼──────────────▼──────────────────▼──────────┐│
│  │              REST API + Webhooks                 ││
│  └──────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────┘
         │                    │
   Your frontend         Your backend
   (widget.js)          (webhook handler,
                         CRM sync, Slack alerts)
Enter fullscreen mode Exit fullscreen mode

Widgets — the front-facing chat/popup components embedded on your site.

Inbox — unified conversation store across all channels (web chat, WhatsApp, email).

AI Bot Engine — the layer that handles first-response automation, trained on your content.

REST API + Webhooks — how you connect all of this to your stack.

The pattern we'll build:

  1. Widget captures visitor intent
  2. AI bot handles first response (trained on your docs)
  3. Webhook fires when human escalation is needed
  4. Your backend routes to Slack / CRM / email
  5. Agent responds from unified inbox

Step 1: Authentication

Oscar Chat uses Bearer token auth. Get your API key from the dashboard under Settings → API.

// config/oscarchat.js
const OSCAR_API_BASE = 'https://api.oscarchat.ai/v1';
const OSCAR_API_KEY = process.env.OSCAR_API_KEY; // never hardcode this

const oscarClient = {
  headers: {
    'Authorization': `Bearer ${OSCAR_API_KEY}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  },
  baseUrl: OSCAR_API_BASE
};

module.exports = oscarClient;
Enter fullscreen mode Exit fullscreen mode

Test your connection:

curl -X GET https://api.oscarchat.ai/v1/workspaces \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json"
Enter fullscreen mode Exit fullscreen mode

Expected response:

{
  "data": [
    {
      "id": "ws_01H8X...",
      "name": "My Store",
      "created_at": "2026-01-15T10:00:00Z",
      "plan": "pro"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Embed the Widget (the Right Way)

The basic embed is one script tag. But for production, you want async loading with proper initialization:

<!-- Load async, don't block render -->
<script>
  (function(w, d, s, o) {
    var js = d.createElement(s);
    js.id = o;
    js.async = true;
    js.src = 'https://widget.oscarchat.ai/loader.js';
    js.setAttribute('data-widget-id', 'YOUR_WIDGET_ID');
    d.head.appendChild(js);

    // Queue commands before widget loads
    w[o] = w[o] || function() {
      (w[o].q = w[o].q || []).push(arguments);
    };
  })(window, document, 'script', 'OscarChat');
</script>
Enter fullscreen mode Exit fullscreen mode

Passing User Context

If your user is authenticated, pass their data to Oscar Chat so your agents have context:

// After user logs in
OscarChat('identify', {
  user_id: user.id,           // your internal ID
  email: user.email,
  name: user.name,
  plan: user.subscription,    // custom attributes
  shopify_store: user.store_url,
  total_orders: user.orderCount,
  created_at: user.joinedAt
});
Enter fullscreen mode Exit fullscreen mode

This populates the agent's sidebar with real customer data — no more tab-switching to your CRM during conversations.

Triggering the Chat Programmatically

Don't rely only on the bubble. Open the chat based on user behavior:

// Open chat when user clicks a custom button
document.getElementById('help-btn').addEventListener('click', () => {
  OscarChat('open');
});

// Pre-fill a message
OscarChat('open', {
  message: 'I need help with my order #' + orderId
});

// Show a specific bot flow
OscarChat('startFlow', {
  flow_id: 'return_request',
  context: { order_id: orderId }
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Webhooks — the Real Power

This is where it gets interesting. Webhooks let your backend react to conversation events in real time.

Register a Webhook Endpoint

curl -X POST https://api.oscarchat.ai/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/oscarchat",
    "events": [
      "conversation.created",
      "conversation.assigned",
      "message.created",
      "conversation.resolved",
      "bot.escalated"
    ],
    "secret": "your_webhook_signing_secret"
  }'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "id": "wh_01H9K...",
  "url": "https://yourapp.com/webhooks/oscarchat",
  "events": ["conversation.created", "message.created", "bot.escalated"],
  "secret": "your_webhook_signing_secret",
  "created_at": "2026-04-29T10:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Build the Webhook Handler

// routes/webhooks.js (Express)
const express = require('express');
const crypto = require('crypto');
const router = express.Router();

// Verify signature — ALWAYS do this
function verifySignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const computed = 'sha256=' + hmac.update(payload).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(signature)
  );
}

router.post('/oscarchat', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-oscar-signature'];
  const payload = req.body;

  // Reject unverified requests
  if (!verifySignature(payload, signature, process.env.OSCAR_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Acknowledge immediately — process async
  res.status(200).json({ received: true });

  const event = JSON.parse(payload);
  await processWebhookEvent(event);
});

async function processWebhookEvent(event) {
  switch (event.type) {
    case 'bot.escalated':
      await handleBotEscalation(event.data);
      break;
    case 'conversation.created':
      await handleNewConversation(event.data);
      break;
    case 'message.created':
      await handleNewMessage(event.data);
      break;
    case 'conversation.resolved':
      await handleResolved(event.data);
      break;
    default:
      console.log('Unhandled event type:', event.type);
  }
}

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Handle Bot Escalation (Most Important)

When the AI can't answer, you want to know immediately:

async function handleBotEscalation(data) {
  const { conversation_id, contact, last_message, reason } = data;

  // 1. Notify team on Slack
  await notifySlack({
    channel: '#support-urgent',
    text: `🚨 Bot escalation needed`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Customer:* ${contact.name} (${contact.email})\n*Reason:* ${reason}\n*Last message:* "${last_message}"`
        }
      },
      {
        type: 'actions',
        elements: [{
          type: 'button',
          text: { type: 'plain_text', text: 'Open Conversation' },
          url: `https://app.oscarchat.ai/conversations/${conversation_id}`,
          style: 'primary'
        }]
      }
    ]
  });

  // 2. Assign to available agent via API
  const agent = await getAvailableAgent();
  if (agent) {
    await assignConversation(conversation_id, agent.id);
  }

  // 3. Log to your CRM
  await updateCRMContact(contact.email, {
    last_support_escalation: new Date().toISOString(),
    escalation_reason: reason
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Conversations API — Read and Write

Pull conversation data into your own systems:

// Get all open conversations
async function getOpenConversations(workspaceId) {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/workspaces/${workspaceId}/conversations?status=open&limit=50`,
    { headers: oscarClient.headers }
  );
  return response.json();
}

// Get a specific conversation with full history
async function getConversation(conversationId) {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/conversations/${conversationId}?include=messages,contact,assignee`,
    { headers: oscarClient.headers }
  );
  return response.json();
}

// Send a message as a bot/agent
async function sendMessage(conversationId, content, type = 'outgoing') {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/conversations/${conversationId}/messages`,
    {
      method: 'POST',
      headers: oscarClient.headers,
      body: JSON.stringify({
        content,
        message_type: type,       // 'outgoing' = agent, 'activity' = system note
        content_type: 'text',     // 'text' | 'markdown' | 'html'
        private: false            // true = internal note, not visible to customer
      })
    }
  );
  return response.json();
}

// Assign conversation to agent
async function assignConversation(conversationId, agentId) {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/conversations/${conversationId}/assignments`,
    {
      method: 'POST',
      headers: oscarClient.headers,
      body: JSON.stringify({ assignee_id: agentId })
    }
  );
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Shopify Integration Pattern

This is the most common use case — sync Oscar Chat with your Shopify store so agents see order data inline.

// server/integrations/shopify-oscar.js
const Shopify = require('@shopify/shopify-api');

// When a conversation starts, enrich with Shopify data
async function enrichConversationWithShopifyData(event) {
  const { conversation_id, contact } = event.data;

  if (!contact.email) return;

  try {
    // Find customer in Shopify
    const customers = await shopify.rest.Customer.all({
      session,
      email: contact.email
    });

    if (!customers.data.length) return;

    const customer = customers.data[0];

    // Get their recent orders
    const orders = await shopify.rest.Order.all({
      session,
      customer_id: customer.id,
      limit: 5,
      status: 'any'
    });

    // Add as internal note to the conversation
    const orderSummary = orders.data.map(o => 
      `#${o.order_number}${o.financial_status} — $${o.total_price} (${o.created_at.split('T')[0]})`
    ).join('\n');

    await sendMessage(conversation_id, 
      `📦 Shopify Customer Data:\n` +
      `Total orders: ${customer.orders_count}\n` +
      `Total spent: $${customer.total_spent}\n` +
      `Recent orders:\n${orderSummary}`,
      'activity'  // internal note, agent sees but customer doesn't
    );

    // Also update the contact's custom attributes
    await updateContact(contact.id, {
      shopify_customer_id: customer.id,
      total_orders: customer.orders_count,
      total_spent: customer.total_spent,
      shopify_tags: customer.tags
    });

  } catch (error) {
    console.error('Shopify enrichment failed:', error);
  }
}

async function updateContact(contactId, attributes) {
  return fetch(`https://api.oscarchat.ai/v1/contacts/${contactId}`, {
    method: 'PATCH',
    headers: oscarClient.headers,
    body: JSON.stringify({ custom_attributes: attributes })
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 6: AI Bot Configuration via API

You don't have to use the UI to configure your bot. The API lets you update bot behavior programmatically:

// Update bot knowledge base with new content
async function addKnowledgeSource(botId, source) {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/bots/${botId}/knowledge_sources`,
    {
      method: 'POST',
      headers: oscarClient.headers,
      body: JSON.stringify({
        type: source.type,      // 'url' | 'document' | 'text'
        url: source.url,        // for type: 'url'
        content: source.content, // for type: 'text'
        name: source.name
      })
    }
  );
  return response.json();
}

// Trigger re-training when your docs update
async function syncDocsToBot(botId) {
  const pages = [
    'https://yourapp.com/docs/getting-started',
    'https://yourapp.com/docs/api-reference',
    'https://yourapp.com/pricing',
    'https://yourapp.com/faq'
  ];

  for (const url of pages) {
    await addKnowledgeSource(botId, {
      type: 'url',
      url,
      name: url.split('/').pop()
    });
  }

  // Trigger sync
  await fetch(`https://api.oscarchat.ai/v1/bots/${botId}/sync`, {
    method: 'POST',
    headers: oscarClient.headers
  });

  console.log('Bot knowledge base updated');
}

// Call this in your CI/CD pipeline after docs deploy
// e.g., in your GitHub Actions workflow:
// - name: Sync docs to Oscar Chat bot
//   run: node scripts/sync-bot-docs.js
Enter fullscreen mode Exit fullscreen mode

Step 7: Custom Escalation Logic

The default AI escalation is good, but you can build smarter rules:

// Intercept messages before the bot responds
// Use this for custom routing logic

async function smartEscalationRouter(event) {
  const { conversation_id, message, contact } = event.data;
  const content = message.content.toLowerCase();

  // High-priority keywords → skip bot, go straight to human
  const urgentKeywords = ['refund', 'fraud', 'hack', 'lawsuit', 'cancel subscription', 'charge'];
  const isUrgent = urgentKeywords.some(kw => content.includes(kw));

  if (isUrgent) {
    // Immediately assign to senior support agent
    const seniorAgent = await getSeniorAgent();
    await assignConversation(conversation_id, seniorAgent.id);

    // Add high-priority label
    await addLabel(conversation_id, 'urgent');

    // Send to priority Slack channel
    await notifySlack({ channel: '#support-priority', data: event.data });
    return;
  }

  // VIP customers → always get a human
  const isVIP = contact.custom_attributes?.plan === 'enterprise' || 
                parseFloat(contact.custom_attributes?.total_spent) > 1000;

  if (isVIP) {
    const agent = await getAvailableAgent();
    await assignConversation(conversation_id, agent.id);
    await addLabel(conversation_id, 'vip');
    return;
  }

  // Everything else → let the AI handle it
  // Bot responds automatically, no action needed
}

async function addLabel(conversationId, label) {
  return fetch(`https://api.oscarchat.ai/v1/conversations/${conversationId}/labels`, {
    method: 'POST',
    headers: oscarClient.headers,
    body: JSON.stringify({ labels: [label] })
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Analytics and Reporting

Pull support metrics into your own dashboard:

// Get conversation stats for a date range
async function getSupportMetrics(workspaceId, startDate, endDate) {
  const response = await fetch(
    `https://api.oscarchat.ai/v1/workspaces/${workspaceId}/reports/summary?` +
    `since=${startDate}&until=${endDate}&type=account`,
    { headers: oscarClient.headers }
  );

  const data = await response.json();

  return {
    totalConversations: data.conversations_count,
    avgFirstResponseTime: data.avg_first_response_time,    // seconds
    avgResolutionTime: data.avg_resolution_time,            // seconds
    botResolutionRate: data.bot_resolution_rate,            // percentage
    csat: data.csat_survey_score                            // 0-5
  };
}

// Example: weekly Slack report
async function sendWeeklyReport() {
  const endDate = Math.floor(Date.now() / 1000);
  const startDate = endDate - (7 * 24 * 60 * 60);

  const metrics = await getSupportMetrics('YOUR_WORKSPACE_ID', startDate, endDate);

  const botRate = (metrics.botResolutionRate * 100).toFixed(1);
  const avgResponseMins = (metrics.avgFirstResponseTime / 60).toFixed(1);

  await notifySlack({
    channel: '#support-metrics',
    text: `📊 Weekly Support Report`,
    blocks: [{
      type: 'section',
      fields: [
        { type: 'mrkdwn', text: `*Total Conversations*\n${metrics.totalConversations}` },
        { type: 'mrkdwn', text: `*Bot Resolution Rate*\n${botRate}%` },
        { type: 'mrkdwn', text: `*Avg First Response*\n${avgResponseMins} min` },
        { type: 'mrkdwn', text: `*CSAT Score*\n${metrics.csat}/5` }
      ]
    }]
  });
}

// Schedule with node-cron
const cron = require('node-cron');
cron.schedule('0 9 * * 1', sendWeeklyReport); // Every Monday at 9am
Enter fullscreen mode Exit fullscreen mode

Production Checklist

Before you ship this to production:

Infrastructure
├── [ ] API key in environment variables (never in code)
├── [ ] Webhook signing secret verified on every request
├── [ ] Webhook handler returns 200 immediately, processes async
├── [ ] Retry logic for failed API calls (exponential backoff)
└── [ ] Rate limit handling (Oscar Chat API: 300 req/min)

Reliability
├── [ ] Queue webhook events (Redis/BullMQ) — don't process inline
├── [ ] Idempotency checks (same event can arrive twice)
├── [ ] Dead letter queue for failed webhook processing
└── [ ] Monitoring on webhook endpoint (PagerDuty / Sentry)

Testing
├── [ ] Local webhook testing with ngrok or Cloudflare Tunnel
├── [ ] Test each event type with mock payloads
└── [ ] Load test your webhook handler before traffic spikes
Enter fullscreen mode Exit fullscreen mode

Rate Limiting Pattern

// Simple rate limiter for Oscar Chat API
const Bottleneck = require('bottleneck');

const limiter = new Bottleneck({
  minTime: 200,        // 5 requests per second max
  maxConcurrent: 10,
  reservoir: 300,      // 300 requests
  reservoirRefreshAmount: 300,
  reservoirRefreshInterval: 60 * 1000  // per minute
});

// Wrap all API calls
const fetchWithRateLimit = limiter.wrap(fetch);
Enter fullscreen mode Exit fullscreen mode

Webhook Queue Pattern

// Use BullMQ to process webhooks reliably
const { Queue, Worker } = require('bullmq');
const IORedis = require('ioredis');

const connection = new IORedis(process.env.REDIS_URL);
const webhookQueue = new Queue('oscar-webhooks', { connection });

// In your webhook endpoint — just enqueue, return fast
router.post('/oscarchat', async (req, res) => {
  // verify signature first...

  await webhookQueue.add('process', JSON.parse(req.body), {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  });

  res.status(200).json({ received: true });
});

// Separate worker process
const worker = new Worker('oscar-webhooks', async (job) => {
  await processWebhookEvent(job.data);
}, { connection });

worker.on('failed', (job, err) => {
  console.error(`Webhook job ${job.id} failed:`, err);
  // Alert your team
});
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here's the complete data flow in production:

Visitor lands on your site
       │
       ▼
Oscar Chat widget loads (async)
       │
       ▼
User identified → context passed via OscarChat('identify', {...})
       │
       ▼
User sends message
       │
       ▼
AI Bot evaluates:
  ├── Known question → AI answers instantly
  └── Unknown / urgent / VIP → escalates
             │
             ▼
         Webhook fires → your backend
             │
        ┌────┴─────────────────────┐
        │                          │
        ▼                          ▼
  Slack notification          CRM updated
  with conversation link      (Shopify data enriched)
        │
        ▼
  Agent picks up from
  unified inbox with full context
Enter fullscreen mode Exit fullscreen mode

That's the system. No tab-switching, no "let me look that up", no 4-hour response times. Your AI handles 70-80% of conversations. The rest go to humans who have all the context they need before they type the first word.


Further Reading


If you build something with this, drop a comment — always curious what people are integrating. And if you find a gap in the API docs, their support team is surprisingly responsive.

Tags: javascript node api chatbot shopify customer-support webhooks automation

Top comments (0)