Scopien Provisioning API
Complete API for provisioning AI assistant tenants with per-tenant S3 file storage.
Quick Start
# Create a tenant
curl -X POST https://gateway.scopien.com/api/tenants \
-H "Content-Type: application/json" \
-d '{"name": "My Company", "identity": {"name": "Aria", "emoji": "๐ค"}}'
# Response includes webchat URL and gateway token
Create Tenant
{
"name": "My Company",
"identity": { "name": "Aria", "emoji": "๐ค", "personality": "Helpful assistant" },
"config": { "model": "anthropic/claude-sonnet-4", "provider": "anthropic", "apiKey": "sk-..." }
}
{
"tenant": {
"id": "637147e6-5519-43be-af1d-f67a4a43acb3",
"name": "My Company",
"port": 18801,
"status": "running",
"url": "https://gateway.scopien.com/t/18801/",
"gatewayToken": "tenant-637147e6-6cf9585c"
}
}
List / Get / Delete Tenants
Query Parameters
| Param | Type | Description |
|---|---|---|
status | string | running | stopped | all (default: all) |
{
"tenants": [
{
"id": "637147e6-5519-43be-af1d-f67a4a43acb3",
"name": "My Company",
"port": 18801,
"status": "running",
"url": "https://gateway.scopien.com/t/18801/",
"identity": { "name": "Aria", "emoji": "๐ค" },
"createdAt": "2026-02-16T15:31:51.807Z"
}
],
"total": 1
}
{
"tenant": {
"id": "637147e6-5519-43be-af1d-f67a4a43acb3",
"name": "My Company",
"containerName": "scopien-tenant-637147e6",
"port": 18801,
"status": "running",
"url": "https://gateway.scopien.com/t/18801/",
"gatewayToken": "tenant-637147e6-6cf9585c",
"identity": { "name": "Aria", "emoji": "๐ค", "personality": "Helpful assistant" },
"config": { "model": "anthropic/claude-sonnet-4", "provider": "anthropic" },
"createdAt": "2026-02-16T15:31:51.807Z",
"updatedAt": "2026-02-16T15:31:51.807Z"
}
}
{
"deleted": true,
"tenantId": "637147e6-5519-43be-af1d-f67a4a43acb3",
"message": "Tenant deleted successfully"
}
Container Control
{ "tenantId": "637147e6-...", "status": "running", "message": "Tenant started" }
{ "tenantId": "637147e6-...", "status": "stopped", "message": "Tenant stopped" }
{ "tenantId": "637147e6-...", "status": "running", "message": "Tenant restarted" }
Destroys and recreates container with latest config + S3 env vars.
{
"tenantId": "637147e6-...",
"status": "rebuilt",
"storage": { "bucket": "scopien-tenant-637147e6", "created": false },
"message": "Container rebuilt with S3 storage support"
}
Status & Logs
{
"tenantId": "637147e6-...",
"name": "My Company",
"port": 18801,
"url": "https://gateway.scopien.com/t/18801/",
"container": {
"status": "running",
"running": true,
"startedAt": "2026-02-16T15:33:18.000Z",
"health": "healthy"
},
"identity": { "name": "Aria", "emoji": "๐ค" }
}
Query Parameters
| Param | Type | Description |
|---|---|---|
lines | number | Number of lines to return (default: 100) |
since | string | Time filter: 1h, 30m, 24h |
{
"tenantId": "637147e6-...",
"logs": "[gateway] listening on port 18789\n[webchat] enabled...",
"lines": 100
}
Storage (S3)
{
"tenantId": "637147e6-...",
"storage": {
"bucket": "scopien-tenant-637147e6",
"endpoint": "http://172.17.0.1:9000",
"region": "us-east-1",
"status": "ready"
}
}
Multipart file upload (max 50MB)
Request
Content-Type: multipart/form-data
file: <binary>
{
"tenantId": "637147e6-...",
"file": {
"key": "uploads/1708012345678-document.pdf",
"bucket": "scopien-tenant-637147e6",
"filename": "document.pdf",
"size": 125430,
"contentType": "application/pdf"
}
}
Get presigned upload URL for direct browser-to-S3 upload
Request Body
{
"filename": "document.pdf",
"contentType": "application/pdf" // optional
}
{
"tenantId": "637147e6-...",
"upload": {
"url": "https://gateway.scopien.com/s3/scopien-tenant-637147e6/uploads/1708012345678-document.pdf?X-Amz-Algorithm=...",
"key": "uploads/1708012345678-document.pdf",
"bucket": "scopien-tenant-637147e6",
"fileUrl": "https://gateway.scopien.com/s3/scopien-tenant-637147e6/uploads/1708012345678-document.pdf"
}
}
Get presigned download URL
Request Body
{
"key": "uploads/1708012345678-document.pdf"
}
{
"tenantId": "637147e6-...",
"download": {
"url": "https://gateway.scopien.com/s3/scopien-tenant-637147e6/uploads/1708012345678-document.pdf?X-Amz-Algorithm=...",
"key": "uploads/1708012345678-document.pdf"
}
}
Configuration
Update AI model configuration. Restarts the tenant.
Request Body
{
"model": "anthropic/claude-opus-4", // optional
"provider": "anthropic", // optional: anthropic | openai | google
"apiKey": "sk-ant-..." // optional
}
{
"tenantId": "637147e6-...",
"config": { "model": "anthropic/claude-opus-4", "provider": "anthropic" },
"message": "Config updated and tenant restarted"
}
Update assistant identity. Restarts the tenant.
Request Body
{
"name": "Nova", // optional
"emoji": "๐", // optional
"personality": "Enthusiastic helper" // optional
}
{
"tenantId": "637147e6-...",
"identity": { "name": "Nova", "emoji": "๐", "personality": "Enthusiastic helper" },
"message": "Identity updated and tenant restarted"
}
Update API credentials. Restarts the tenant.
Request Body
{
"provider": "anthropic", // required: anthropic | openai | google
"apiKey": "sk-ant-..." // required
}
{
"tenantId": "637147e6-...",
"provider": "anthropic",
"message": "Credentials updated and tenant restarted"
}
JavaScript API Client
Complete client for managing tenants from your frontend:
const API = 'https://gateway.scopien.com/api';
// Create tenant
async function createTenant(data) {
const res = await fetch(`${API}/tenants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return res.json();
}
// List tenants
async function listTenants() {
const res = await fetch(`${API}/tenants`);
return res.json();
}
// Upload file to tenant storage
async function uploadFile(tenantId, file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${API}/tenants/${tenantId}/storage/upload`, {
method: 'POST',
body: formData
});
return res.json();
}
// Usage
const { tenant } = await createTenant({
name: 'My Company',
identity: { name: 'Aria', emoji: '๐ค' }
});
console.log('Chat URL:', tenant.url);
console.log('Token:', tenant.gatewayToken);
File Downloads
Agents return file references in the format FILE_REF::{key}. Use the ScopienFiles utility to parse messages and generate download links.
Include the Script
<script src="https://gateway.scopien.com/docs/scopien-files.js"></script>
Basic Usage
// Initialize with your tenant ID
const fileHandler = new ScopienFiles('637147e6-5519-43be-af1d-f67a4a43acb3');
// Agent message containing a file reference
const message = `Here's your report: **Monthly Sales**
FILE_REF::generated/1708012345678-report.xlsx`;
// Parse and get HTML with download link
const html = await fileHandler.parseMessage(message);
// Result: "Here's your report: **Monthly Sales**
// <a href="https://..." download="report.xlsx">๐ report.xlsx</a>"
document.getElementById('chat').innerHTML = html;
Structured Data (for React/Vue)
// Get structured data instead of HTML
const { text, files } = await fileHandler.parseMessageStructured(message);
// text: "Here's your report: **Monthly Sales**"
// files: [
// {
// key: "generated/1708012345678-report.xlsx",
// filename: "report.xlsx",
// icon: "๐",
// url: "https://gateway.scopien.com/s3/..."
// }
// ]
// Render files separately
files.forEach(file => {
console.log(`${file.icon} ${file.filename}: ${file.url}`);
});
React Component Example
import { useState, useEffect } from 'react';
function ChatMessage({ tenantId, message }) {
const [parsed, setParsed] = useState({ text: message, files: [] });
useEffect(() => {
const handler = new ScopienFiles(tenantId);
handler.parseMessageStructured(message).then(setParsed);
}, [message, tenantId]);
return (
<div className="message">
<p>{parsed.text}</p>
{parsed.files.length > 0 && (
<div className="files">
{parsed.files.map(file => (
<a key={file.key} href={file.url} download={file.filename}>
{file.icon} {file.filename}
</a>
))}
</div>
)}
</div>
);
}
Custom Link Renderer
const html = await fileHandler.parseMessage(message, {
customRenderer: (ref, url, icon) => {
return `<button onclick="window.open('${url}')">
${icon} Download ${ref.filename}
</button>`;
}
});
File Icons
| Extension | Icon |
|---|---|
.xlsx, .xls, .csv | ๐ |
.pdf | ๐ |
.docx, .doc, .txt | ๐ |
.png, .jpg, .gif | ๐ผ๏ธ |
.json | ๐ |
.zip | ๐ฆ |
| Other | ๐ |
WebSocket Chat Connection
Connect directly to a tenant's chat via WebSocket:
class ScopienChat {
constructor(tenantUrl, gatewayToken) {
// Convert HTTPS URL to WSS
this.wsUrl = tenantUrl.replace('https://', 'wss://') + 'ws';
this.token = gatewayToken;
this.ws = null;
this.messageHandlers = [];
}
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
// Authenticate
this.ws.send(JSON.stringify({
type: 'auth',
token: this.token
}));
resolve();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.messageHandlers.forEach(h => h(data));
};
this.ws.onerror = reject;
});
}
sendMessage(text) {
this.ws.send(JSON.stringify({
type: 'message',
content: text
}));
}
onMessage(handler) {
this.messageHandlers.push(handler);
}
disconnect() {
if (this.ws) this.ws.close();
}
}
// Usage
const chat = new ScopienChat(
'https://gateway.scopien.com/t/18801/',
'tenant-637147e6-6cf9585c'
);
await chat.connect();
chat.onMessage((msg) => {
console.log('Assistant:', msg.content);
});
chat.sendMessage('Hello!');
Embed Chat Widget
Embed the chat interface directly in your website:
Option 1: Simple Iframe
<!-- Full page embed -->
<iframe
src="https://gateway.scopien.com/t/18801/"
style="width: 100%; height: 600px; border: none; border-radius: 12px;"
allow="microphone"
></iframe>
Option 2: Floating Chat Button
<!-- Floating chat widget -->
<style>
.scopien-widget {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}
.scopien-toggle {
width: 60px;
height: 60px;
border-radius: 50%;
background: #6366f1;
border: none;
cursor: pointer;
font-size: 24px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.scopien-chat {
display: none;
position: absolute;
bottom: 70px;
right: 0;
width: 380px;
height: 500px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.scopien-chat.open { display: block; }
.scopien-chat iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
<div class="scopien-widget">
<div class="scopien-chat" id="scopien-chat">
<iframe src="https://gateway.scopien.com/t/18801/"></iframe>
</div>
<button class="scopien-toggle" onclick="toggleChat()">๐ฌ</button>
</div>
<script>
function toggleChat() {
document.getElementById('scopien-chat').classList.toggle('open');
}
</script>
Option 3: Dynamic Widget Loader
<!-- Load widget dynamically -->
<script>
(function() {
const TENANT_URL = 'https://gateway.scopien.com/t/18801/';
// Create widget container
const widget = document.createElement('div');
widget.innerHTML = `
<div id="scopien-widget" style="
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
">
<div id="scopien-frame" style="
display: none; width: 380px; height: 500px;
border-radius: 12px; overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
margin-bottom: 10px;
">
<iframe src="${TENANT_URL}" style="width:100%;height:100%;border:none;"></iframe>
</div>
<button onclick="document.getElementById('scopien-frame').style.display =
document.getElementById('scopien-frame').style.display === 'none' ? 'block' : 'none'"
style="width:60px;height:60px;border-radius:50%;background:#6366f1;
border:none;cursor:pointer;font-size:24px;float:right;">๐ฌ</button>
</div>
`;
document.body.appendChild(widget);
})();
</script>
React Integration
React components for Scopien integration:
Chat Iframe Component
import React, { useState } from 'react';
// Simple Chat Embed
export function ScopienChat({ tenantUrl, height = 500 }) {
return (
<iframe
src={tenantUrl}
style={{
width: '100%',
height: height,
border: 'none',
borderRadius: '12px'
}}
allow="microphone"
/>
);
}
// Floating Widget
export function ScopienWidget({ tenantUrl }) {
const [open, setOpen] = useState(false);
return (
<div style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 9999 }}>
{open && (
<div style={{
width: 380, height: 500,
borderRadius: 12, overflow: 'hidden',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
marginBottom: 10
}}>
<iframe src={tenantUrl} style={{ width: '100%', height: '100%', border: 'none' }} />
</div>
)}
<button
onClick={() => setOpen(!open)}
style={{
width: 60, height: 60, borderRadius: '50%',
background: '#6366f1', border: 'none',
cursor: 'pointer', fontSize: 24, float: 'right'
}}
>
{open ? 'โ' : '๐ฌ'}
</button>
</div>
);
}
// Usage
function App() {
return (
<div>
<h1>My App</h1>
<ScopienWidget tenantUrl="https://gateway.scopien.com/t/18801/" />
</div>
);
}
React Hook for API
import { useState, useCallback } from 'react';
const API = 'https://gateway.scopien.com/api';
export function useScopien() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = useCallback(async (endpoint, options = {}) => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${API}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
return data;
} catch (e) {
setError(e.message);
throw e;
} finally {
setLoading(false);
}
}, []);
const createTenant = (data) => request('/tenants', {
method: 'POST',
body: JSON.stringify(data)
});
const listTenants = () => request('/tenants');
const getTenant = (id) => request(`/tenants/${id}`);
const deleteTenant = (id) => request(`/tenants/${id}`, { method: 'DELETE' });
return { createTenant, listTenants, getTenant, deleteTenant, loading, error };
}
// Usage
function TenantManager() {
const { createTenant, listTenants, loading, error } = useScopien();
const [tenants, setTenants] = useState([]);
useEffect(() => {
listTenants().then(data => setTenants(data.tenants));
}, []);
const handleCreate = async () => {
const { tenant } = await createTenant({ name: 'New Tenant' });
setTenants([...tenants, tenant]);
};
return (
<div>
<button onClick={handleCreate} disabled={loading}>Create Tenant</button>
{error && <p style={{color: 'red'}}>{error}</p>}
{tenants.map(t => <div key={t.id}>{t.name} - {t.url}</div>)}
</div>
);
}
Nginx Proxy Configuration
Complete nginx configuration to proxy the Scopien API and tenant WebSocket connections with HTTPS.
Full Configuration
server {
server_name gateway.scopien.com;
# Allow large file uploads
client_max_body_size 100M;
# API Documentation
location = /docs {
return 301 /docs/;
}
location /docs/ {
alias /path/to/docs/;
index index.html;
try_files $uri $uri/ /docs/index.html;
}
# S3/MinIO proxy for file storage
location /s3/ {
rewrite ^/s3/(.*) /$1 break;
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M;
proxy_read_timeout 300;
proxy_send_timeout 300;
}
# Provisioning API (port 3005)
location /api {
proxy_pass http://127.0.0.1:3005;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# Tenant routing: /t/{port}/ -> localhost:{port}/
# Supports WebSocket connections for chat
location ~ ^/t/(\d+)(/.*)?$ {
set $tenant_port $1;
# Remove /t/{port} prefix from path
rewrite ^/t/\d+(/.*)?$ $1 break;
rewrite ^/t/\d+$ / break;
proxy_pass http://127.0.0.1:$tenant_port;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For 127.0.0.1;
proxy_set_header X-Forwarded-Proto $scheme;
# Override Origin to bypass allowedOrigins check
proxy_set_header Origin https://gateway.scopien.com;
# Long timeout for WebSocket connections
proxy_read_timeout 86400;
proxy_buffering off;
}
# Default root (optional: main instance or landing page)
location / {
proxy_pass http://127.0.0.1:18790;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
# SSL (managed by certbot)
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/gateway.scopien.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gateway.scopien.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name gateway.scopien.com;
return 301 https://$host$request_uri;
}
Key Configuration Points
| Setting | Purpose |
|---|---|
client_max_body_size 100M | Allow large file uploads to S3 |
proxy_set_header Upgrade | Enable WebSocket connections |
proxy_read_timeout 86400 | Keep WebSocket connections alive (24h) |
proxy_buffering off | Stream responses immediately (required for SSE/streaming) |
proxy_set_header Origin | Override origin to bypass CORS checks |
location ~ ^/t/(\d+) | Regex routing for dynamic tenant ports |
SSL with Let's Encrypt
Set up free SSL certificates using Certbot:
Install Certbot
# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx
# CentOS/RHEL
sudo yum install certbot python3-certbot-nginx
Get Certificate
# Obtain certificate (nginx plugin auto-configures)
sudo certbot --nginx -d gateway.scopien.com
# Or just get the certificate (manual config)
sudo certbot certonly --nginx -d gateway.scopien.com
Auto-Renewal
# Test renewal
sudo certbot renew --dry-run
# Certbot adds a cron job automatically, or add manually:
echo "0 12 * * * /usr/bin/certbot renew --quiet" | sudo tee -a /etc/crontab
Apply Configuration
# Test nginx config
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
Origin header override is required because tenant containers check allowedOrigins. Without it, WebSocket connections from the proxied domain will be rejected.Error Codes & Models
| Code | Status | Description |
|---|---|---|
MISSING_NAME | 400 | Tenant name required |
MISSING_FILE | 400 | No file in upload |
NOT_FOUND | 404 | Tenant not found |
STORAGE_ERROR | 500 | S3 storage error |
Supported Models
| Provider | Models |
|---|---|
| Anthropic | anthropic/claude-opus-4-5, anthropic/claude-sonnet-4 |
| OpenAI | openai/gpt-4o, openai/gpt-4-turbo |
google/gemini-pro, google/gemini-2.0-flash |
Last updated: February 17, 2026 ยท v1.1.0 ยท API Status