Overview
Integrations connect a chatbot to an external messaging channel. Three channel types are supported:
| Channel | Type value | Description |
|---|---|---|
whatsapp | WhatsApp Business API via Meta | |
| REST API | rest-api | Generic REST webhook for custom integrations |
| Web Widget | web | Embeddable chat widget for websites |
List Integrations
Returns all integrations attached to a chatbot.
GET /api/dev/v1/chatbots/{chatbot_id}/integrations
Authorization: Bearer <api-key>curl -X GET https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations \
-H "Authorization: Bearer <your-api-key>"Response — 200 OK
{
"items": [
{
"id": "01JST...",
"chatbot_id": "01JMXYZ...",
"type": "whatsapp",
"is_active": true,
"created_at": "2026-01-25T11:00:00Z"
}
],
"total": 1
}Get Integration
GET /api/dev/v1/chatbots/{chatbot_id}/integrations/{integration_id}
Authorization: Bearer <api-key>curl -X GET https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations/<integration-id> \
-H "Authorization: Bearer <your-api-key>"Delete Integration
Permanently removes an integration. Users on this channel will no longer be able to reach the chatbot.
DELETE /api/dev/v1/chatbots/{chatbot_id}/integrations/{integration_id}
Authorization: Bearer <api-key>curl -X DELETE https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations/<integration-id> \
-H "Authorization: Bearer <your-api-key>"Response — 204 No Content
WhatsApp Integration
Create WhatsApp Integration
POST /api/dev/v1/chatbots/{chatbot_id}/integrations/whatsapp
Authorization: Bearer <api-key>
Content-Type: application/jsonBody
phone_number_idstringrequiredMeta WhatsApp Business phone number ID.
access_tokenstringrequiredPermanent access token from Meta for Business.
verify_tokenstringrequiredToken you set when configuring the Meta webhook.
curl -X POST https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations/whatsapp \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"phone_number_id": "1234567890",
"access_token": "EAABwzLixnjYBO...",
"verify_token": "my-webhook-verify-token"
}'Get WhatsApp Integration
GET /api/dev/v1/chatbots/{chatbot_id}/integrations/whatsapp
Authorization: Bearer <api-key>Update WhatsApp Integration
PATCH /api/dev/v1/chatbots/{chatbot_id}/integrations/whatsapp
Authorization: Bearer <api-key>
Content-Type: application/jsonREST API Integration
Use this integration to connect the chatbot to any custom channel via a webhook.
Create REST API Integration
POST /api/dev/v1/chatbots/{chatbot_id}/integrations/rest-api
Authorization: Bearer <api-key>
Content-Type: application/jsonBody
webhook_urlstringrequiredThe URL Sarufi will POST outbound messages to.
namestringoptionalFriendly name for this REST integration.
curl -X POST https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations/rest-api \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://yourserver.com/chatbot-webhook",
"name": "My REST Integration"
}'Get / Update REST API Integration
GET /api/dev/v1/chatbots/{chatbot_id}/integrations/rest-api
PATCH /api/dev/v1/chatbots/{chatbot_id}/integrations/rest-api
Authorization: Bearer <api-key>Web Widget Integration
Embed a real-time chat widget on any website. The widget loads asynchronously, connects over Socket.IO, and supports full conversation history, buttons, lists, and media messages.
Create Web Integration
POST /api/dev/v1/chatbots/{chatbot_id}/integrations/web
Authorization: Bearer <api-key>
Content-Type: application/jsonBody
namestringrequiredA friendly name for this web integration.
allowed_domainsarrayoptionalList of domains allowed to load the widget (e.g. ["yoursite.com", "app.yoursite.com"]). Leave empty to allow all domains.
greeting_messagestringoptionalThe first message shown when a visitor opens the widget.
widget_configobjectoptionalAppearance and behaviour overrides — see the Widget Config reference below.
widget_config fields
bot_display_namestringoptionalName shown in the widget header and on bot messages. Defaults to "Assistant".
primary_colorstringoptionalHex color for the bubble, header, and user messages. Defaults to "#0066ff".
secondary_colorstringoptionalHex color for bot message bubbles. Defaults to "#f0f4ff".
background_colorstringoptionalHex color for the chat message area background.
bubble_textstringoptionalText shown on the floating bubble button. Defaults to "Need Help?".
bot_avatar_iconstringoptionalIcon key for the bot avatar. One of: "" (Sarufi default), "robot", "headset", "sparkle", "heart", "store", "graduation", "chat".
font_sizenumberoptionalBase font size in pixels (12–24). Defaults to 13.5.
positionstringoptionalWidget placement. One of "bottom-right" (default) or "bottom-left".
widthnumberoptionalChat window width in pixels (280–600). Defaults to 380.
heightnumberoptionalChat window height in pixels (400–800). Defaults to 600.
allowed_pathsarrayoptionalURL path patterns where the widget should appear. Supports exact paths ("/contact") and wildcard prefix matching ("/shop/*"). When null or empty the widget appears on every page.
curl -X POST https://beta-api.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/integrations/web \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Main Site Widget",
"allowed_domains": ["yoursite.com"],
"greeting_message": "Hi! How can I help you today?",
"widget_config": {
"bot_display_name": "Aria",
"primary_color": "#0066ff",
"bubble_text": "Need Help?",
"position": "bottom-right",
"allowed_paths": ["/contact", "/support/*"]
}
}'Response — 201 Created
{
"id": "01JUV...",
"type": "web",
"name": "Main Site Widget",
"config": {
"widget_key": "wk_AbCdEf...",
"allowed_domains": ["yoursite.com"],
"widget_config": {
"bot_display_name": "Aria",
"primary_color": "#0066ff",
"position": "bottom-right",
"allowed_paths": ["/contact", "/support/*"]
}
},
"is_active": true
}Get / Update Web Integration
GET /api/dev/v1/chatbots/{chatbot_id}/integrations/web
PATCH /api/dev/v1/chatbots/{chatbot_id}/integrations/web
Authorization: Bearer <api-key>Embedding the Widget
Once your web integration is created you get a widget_key (format: wk_...). Use it to embed the widget on your site with a single <script> tag.
Script Attributes Reference
| Attribute | Required | Description |
|---|---|---|
src | yes | Widget script URL — https://beta.sarufi.io/widget.js |
data-widget-key | yes | Your wk_... key from the integration |
data-server-url | no | Override the backend origin (useful for self-hosted deployments) |
async | recommended | Loads the script without blocking page rendering |
Plain HTML
Paste the snippet just before the closing </body> tag. This ensures the widget loads after the page content is ready.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Site</title>
</head>
<body>
<!-- your page content -->
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>
</body>
</html>Placement matters
Placing the script before </body> (rather than in <head>) means it never blocks your page from rendering. The async attribute ensures parallel loading.
Next.js (App Router)
Use the built-in next/script component with strategy="lazyOnload". This loads the widget during browser idle time — after all critical resources — so it never impacts your Core Web Vitals.
Site-wide (every page) — add to app/layout.tsx:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
strategy="lazyOnload"
/>
</body>
</html>
);
}Specific route group only — add to a nested layout:
// app/(marketing)/layout.tsx
import Script from "next/script";
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
strategy="lazyOnload"
/>
</>
);
}Why lazyOnload?
lazyOnload loads the script after the entire page — including images and fonts — has finished loading. This is the recommended strategy for chat widgets and other non-critical UI.
document.currentScript in Next.js
Next.js injects <Script> tags asynchronously, so document.currentScript will be null at the time the widget executes. The Sarufi widget handles this automatically via a querySelector fallback — no extra configuration is needed.
React (Vite / Create React App)
Option A — index.html (recommended for all pages)
Add the script tag to public/index.html (CRA) or index.html (Vite) before </body>:
<!-- public/index.html (CRA) or index.html (Vite) -->
<body>
<div id="root"></div>
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>
</body>Option B — useEffect in a component (programmatic control)
Use this when you want to conditionally load the widget based on application state (e.g. only after a user logs in).
// src/components/SarufiWidget.tsx
import { useEffect } from "react";
export function SarufiWidget() {
useEffect(() => {
if (document.querySelector('script[data-widget-key]')) return; // already loaded
const script = document.createElement("script");
script.src = "https://beta.sarufi.io/widget.js";
script.setAttribute("data-widget-key", "wk_YOUR_WIDGET_KEY");
script.async = true;
document.body.appendChild(script);
}, []);
return null;
}Then mount it once at the top of your app:
// src/App.tsx
import { SarufiWidget } from "./components/SarufiWidget";
export default function App() {
return (
<>
<Router>
{/* your routes */}
</Router>
<SarufiWidget />
</>
);
}Vue 3 (Vite)
Option A — index.html
<!-- index.html -->
<body>
<div id="app"></div>
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>
</body>Option B — composable for programmatic loading
// src/composables/useSarufiWidget.ts
import { onMounted } from "vue";
export function useSarufiWidget(widgetKey: string) {
onMounted(() => {
if (document.querySelector('script[data-widget-key]')) return;
const script = document.createElement("script");
script.src = "https://beta.sarufi.io/widget.js";
script.setAttribute("data-widget-key", widgetKey);
script.async = true;
document.body.appendChild(script);
});
}Use it in your root App.vue:
<!-- src/App.vue -->
<script setup lang="ts">
import { useSarufiWidget } from "@/composables/useSarufiWidget";
useSarufiWidget("wk_YOUR_WIDGET_KEY");
</script>
<template>
<RouterView />
</template>Angular
Option A — index.html (simplest)
<!-- src/index.html -->
<body>
<app-root></app-root>
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>
</body>Option B — AppComponent with DOCUMENT injection
// src/app/app.component.ts
import { Component, OnInit, Inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
constructor(@Inject(DOCUMENT) private document: Document) {}
ngOnInit(): void {
if (this.document.querySelector('script[data-widget-key]')) return;
const script = this.document.createElement("script");
script.src = "https://beta.sarufi.io/widget.js";
script.setAttribute("data-widget-key", "wk_YOUR_WIDGET_KEY");
script.async = true;
this.document.body.appendChild(script);
}
}Angular SSR (Universal)
If you use Angular Universal, wrap the script injection in an isPlatformBrowser check to avoid running it during server-side rendering.
import { isPlatformBrowser } from "@angular/common";
import { PLATFORM_ID } from "@angular/core";
constructor(
@Inject(DOCUMENT) private document: Document,
@Inject(PLATFORM_ID) private platformId: object
) {}
ngOnInit(): void {
if (!isPlatformBrowser(this.platformId)) return;
// ... script injection
}WordPress
Option A — Theme functions.php (recommended)
Add this to your active theme's functions.php:
<?php
function sarufi_widget_script() {
echo '<script src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async></script>';
}
add_action( 'wp_footer', 'sarufi_widget_script' );wp_footer fires just before </body>, which is the correct placement.
Option B — Insert Headers and Footers plugin
If you prefer not to edit theme files, install the Insert Headers and Footers plugin and paste the snippet into the Footer Scripts field.
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>Shopify
Option A — Theme editor (no code)
- In your Shopify admin go to Online Store → Themes → Customize.
- Click App embeds in the left panel.
- If the widget is not listed as an app embed, use Option B below.
Option B — Theme code editor
- Go to Online Store → Themes → Edit code.
- Open
layout/theme.liquid. - Paste the snippet just before
</body>:
<!-- layout/theme.liquid -->
<script
src="https://beta.sarufi.io/widget.js"
data-widget-key="wk_YOUR_WIDGET_KEY"
async
></script>
</body>
</html>Shopify Online Store 2.0
On OS2.0 themes you can also add a custom snippet under snippets/sarufi-widget.liquid and render it from theme.liquid with {%- render 'sarufi-widget' -%} to keep your layout file tidy.
Path-Based Visibility
By default the widget appears on every page. Use allowed_paths inside widget_config to restrict it to specific pages.
How matching works
| Pattern | Matches |
|---|---|
"/contact" | Exactly /contact |
"/shop/*" | /shop, /shop/, /shop/shoes, /shop/shoes/nike |
"/docs/*" | /docs, /docs/getting-started, /docs/api/auth |
When a visitor navigates to a page not in allowed_paths:
- The widget is hidden (
display: none) - The Socket.IO connection is disconnected — no idle connections on restricted pages
When they navigate back to an allowed page:
- The widget reappears
- The socket reconnects automatically, resuming the conversation from session history
SPA route changes
The widget listens for both popstate (browser back/forward) and intercepts history.pushState / history.replaceState to handle client-side navigation in React, Vue, Angular, and Next.js apps — no extra configuration needed.
Example — show only on support pages
{
"widget_config": {
"allowed_paths": ["/support", "/support/*", "/contact", "/help/*"]
}
}Example — show on all pages except checkout
There is no "deny list" — define the pages where the widget should appear:
{
"widget_config": {
"allowed_paths": ["/", "/products/*", "/about", "/contact"]
}
}