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)
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:
- Widget captures visitor intent
- AI bot handles first response (trained on your docs)
- Webhook fires when human escalation is needed
- Your backend routes to Slack / CRM / email
- 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;
Test your connection:
curl -X GET https://api.oscarchat.ai/v1/workspaces \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
Expected response:
{
"data": [
{
"id": "ws_01H8X...",
"name": "My Store",
"created_at": "2026-01-15T10:00:00Z",
"plan": "pro"
}
]
}
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>
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
});
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 }
});
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"
}'
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"
}
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;
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
});
}
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();
}
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 })
});
}
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
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] })
});
}
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
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
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);
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
});
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
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
- Oscar Chat API Docs — full endpoint reference
- Oscar Chat AI Chatbot — how the AI layer works
- Oscar Chat Live Chat — unified inbox docs
- Pricing & Plans — API access included from Basic plan
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)