Scopien Provisioning API

Complete API for provisioning AI assistant tenants with per-tenant S3 file storage.

Base URL
https://gateway.scopien.com/api

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

POST /api/tenants
{
  "name": "My Company",
  "identity": { "name": "Aria", "emoji": "๐Ÿค–", "personality": "Helpful assistant" },
  "config": { "model": "anthropic/claude-sonnet-4", "provider": "anthropic", "apiKey": "sk-..." }
}
Response201
{
  "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

GET/api/tenants

Query Parameters

ParamTypeDescription
statusstringrunning | stopped | all (default: all)
Response200
{
  "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
}
GET/api/tenants/{id}
Response200
{
  "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"
  }
}
DELETE/api/tenants/{id}
Response200
{
  "deleted": true,
  "tenantId": "637147e6-5519-43be-af1d-f67a4a43acb3",
  "message": "Tenant deleted successfully"
}

Container Control

POST/api/tenants/{id}/start
Response200
{ "tenantId": "637147e6-...", "status": "running", "message": "Tenant started" }
POST/api/tenants/{id}/stop
Response200
{ "tenantId": "637147e6-...", "status": "stopped", "message": "Tenant stopped" }
POST/api/tenants/{id}/restart
Response200
{ "tenantId": "637147e6-...", "status": "running", "message": "Tenant restarted" }
POST/api/tenants/{id}/rebuild

Destroys and recreates container with latest config + S3 env vars.

Response200
{
  "tenantId": "637147e6-...",
  "status": "rebuilt",
  "storage": { "bucket": "scopien-tenant-637147e6", "created": false },
  "message": "Container rebuilt with S3 storage support"
}

Status & Logs

GET/api/tenants/{id}/status
Response200
{
  "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": "๐Ÿค–" }
}
GET/api/tenants/{id}/logs

Query Parameters

ParamTypeDescription
linesnumberNumber of lines to return (default: 100)
sincestringTime filter: 1h, 30m, 24h
Response200
{
  "tenantId": "637147e6-...",
  "logs": "[gateway] listening on port 18789\n[webchat] enabled...",
  "lines": 100
}

Storage (S3)

GET/api/tenants/{id}/storage
Response200
{
  "tenantId": "637147e6-...",
  "storage": {
    "bucket": "scopien-tenant-637147e6",
    "endpoint": "http://172.17.0.1:9000",
    "region": "us-east-1",
    "status": "ready"
  }
}
POST/api/tenants/{id}/storage/upload

Multipart file upload (max 50MB)

Request

Content-Type: multipart/form-data

file: <binary>
Response200
{
  "tenantId": "637147e6-...",
  "file": {
    "key": "uploads/1708012345678-document.pdf",
    "bucket": "scopien-tenant-637147e6",
    "filename": "document.pdf",
    "size": 125430,
    "contentType": "application/pdf"
  }
}
POST/api/tenants/{id}/storage/upload-url

Get presigned upload URL for direct browser-to-S3 upload

Request Body

{
  "filename": "document.pdf",
  "contentType": "application/pdf"  // optional
}
Response200
{
  "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"
  }
}
POST/api/tenants/{id}/storage/download-url

Get presigned download URL

Request Body

{
  "key": "uploads/1708012345678-document.pdf"
}
Response200
{
  "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

PUT/api/tenants/{id}/config

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
}
Response200
{
  "tenantId": "637147e6-...",
  "config": { "model": "anthropic/claude-opus-4", "provider": "anthropic" },
  "message": "Config updated and tenant restarted"
}
PUT/api/tenants/{id}/identity

Update assistant identity. Restarts the tenant.

Request Body

{
  "name": "Nova",                         // optional
  "emoji": "๐ŸŒŸ",                          // optional
  "personality": "Enthusiastic helper"    // optional
}
Response200
{
  "tenantId": "637147e6-...",
  "identity": { "name": "Nova", "emoji": "๐ŸŒŸ", "personality": "Enthusiastic helper" },
  "message": "Identity updated and tenant restarted"
}
PUT/api/tenants/{id}/credentials

Update API credentials. Restarts the tenant.

Request Body

{
  "provider": "anthropic",    // required: anthropic | openai | google
  "apiKey": "sk-ant-..."      // required
}
Response200
{
  "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

ExtensionIcon
.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>
  );
}
๐Ÿ’ก
CORS: The API supports requests from any origin. All standard methods and headers are allowed.

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

SettingPurpose
client_max_body_size 100MAllow large file uploads to S3
proxy_set_header UpgradeEnable WebSocket connections
proxy_read_timeout 86400Keep WebSocket connections alive (24h)
proxy_buffering offStream responses immediately (required for SSE/streaming)
proxy_set_header OriginOverride 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
โš ๏ธ
Important: The Origin header override is required because tenant containers check allowedOrigins. Without it, WebSocket connections from the proxied domain will be rejected.

Error Codes & Models

CodeStatusDescription
MISSING_NAME400Tenant name required
MISSING_FILE400No file in upload
NOT_FOUND404Tenant not found
STORAGE_ERROR500S3 storage error

Supported Models

ProviderModels
Anthropicanthropic/claude-opus-4-5, anthropic/claude-sonnet-4
OpenAIopenai/gpt-4o, openai/gpt-4-turbo
Googlegoogle/gemini-pro, google/gemini-2.0-flash

Last updated: February 17, 2026 ยท v1.1.0 ยท API Status