Embed live chat on upuah.com infrastructure with one CDN script, or call API v1 from your server. Widget = browser-safe. Server key = conversations + tickets.

Overview

Upuah has two surfaces. Use the CDN chat widget for visitor live chat on your site. Use the server API (sesh_ key) for backend chat and tickets.

Base URL: https://upuah.com/api/v1 · CDN: https://upuah.com

Never put a sesh_ server key in browser code. The embed loader uses a public emb_ token scoped to allowed origins.

Chat widget (CDN)

Design the widget in Dashboard → Integrations → Widget (colors, greeting, launcher icon, allowed origins). Copy the snippet before </body>.

Visitor flow: name → mobile → “connected shortly” → status pending until an agent Accepts in Live inbox → agent join message → open chat. Close ends the conversation for both sides.

Each integration gets a unique emb_… loader. Shared core: https://upuah.com/cdn/widget.js

Embed snippet

<script src="https://upuah.com/cdn/embed/emb_xxxxxxxx.js" async></script>

Widget API (browser-safe)

No server secret. Routes are under /api/v1/widget/{embed_key}/… and respect allowed_origins from widget settings.

GET /config — theme, greeting, default channel. GET /auto-replies — FAQ list. POST /conversations — start with name + phone (pending). GET …/messages — poll status + messages (includes can_chat). POST …/messages — visitor reply (only when status is open).

Start chat (widget)

POST https://upuah.com/api/v1/widget/{embed_key}/conversations
Content-Type: application/json
Accept: application/json

{
  "channel_id": 1,
  "name": "Alex Guest",
  "phone": "+971501234567"
}

Poll messages

curl https://upuah.com/api/v1/widget/{embed_key}/conversations/{uuid}/messages \
  -H "Accept: application/json" \
  -H "Origin: https://your-site.example"

Server authentication

Server requests use the integration API key (sesh_…) created when you add an integration. The key is bound to one tenant and one integration.

Send it as a Bearer token or with the X-Sesh-Key header. Rotate keys from Dashboard → Integrations.

Authorization: Bearer sesh_xxxxxxxx
X-Sesh-Key: sesh_xxxxxxxx
Accept: application/json

Channels

Create at least one active channel in the dashboard (for example Technical Support or Sales) before chats or tickets.

The widget uses the default channel from widget settings (or channel_id in the request). Tickets use the workspace default ticket channel from Settings unless you pass channel_id.

Live chat API (server)

Same inbox as the widget, but from your backend with a sesh_ key. Opens as an open conversation (not the widget pending flow).

POST /api/v1/conversations — required: channel_id, name, subject, and either email or phone. Optional message for the first line.

GET /api/v1/conversations/{uuid}/messages — list messages. POST …/messages — visitor message. POST …/close — close chat.

curl -X POST https://upuah.com/api/v1/conversations \
  -H "Authorization: Bearer sesh_xxx" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "channel_id": 1,
    "name": "Nora",
    "email": "nora@example.com",
    "subject": "Checkout stuck",
    "message": "The pay button stays disabled on mobile."
  }'

Messages & status

Widget conversations: pending → open (after Accept) → closed. Visitor cannot send until open (can_chat: true).

If a conversation was transferred, status becomes transferred and new visitor messages are rejected on the old uuid. Poll messages for the system notice and open the new uuid from transferred_to.

curl https://upuah.com/api/v1/conversations/{uuid}/messages \
  -H "Authorization: Bearer sesh_xxx" \
  -H "Accept: application/json"

Auto replies

Managed in Dashboard → Auto replies. Returned to the widget (if enabled) and via GET /api/v1/auto-replies with a server key.

curl https://upuah.com/api/v1/auto-replies \
  -H "Authorization: Bearer sesh_xxx" \
  -H "Accept: application/json"

Tickets API

Separate from live chat. Use tickets for async support forms, attachments, and threaded replies — not the contact widget.

POST /api/v1/tickets (multipart for files). Required: name, subject, body, and email or phone. Optional: channel_id, attachments[].

GET /api/v1/tickets/{uuid} · POST /api/v1/tickets/{uuid}/replies

curl -X POST https://upuah.com/api/v1/tickets \
  -H "Authorization: Bearer sesh_xxx" \
  -F "name=Omar" \
  -F "email=omar@example.com" \
  -F "subject=Invoice PDF" \
  -F "body=Link is broken" \
  -F "attachments[]=@/path/file.pdf"

Webhooks

Optional webhook URL when creating an integration. Verify X-Sesh-Signature = HMAC SHA-256 of the raw JSON body with your webhook secret. X-Sesh-Event names the event.

Events: conversation.pending, conversation.accepted, message.created, conversation.closed, conversation.transferred, ticket.created, ticket.replied.

{
  "event": "message.created",
  "created_at": "2026-08-25T12:00:00+00:00",
  "data": {
    "conversation_uuid": "...",
    "message_uuid": "...",
    "body": "Thanks — checking now",
    "sender_type": "agent"
  }
}

Verify signature (Node)

import crypto from 'crypto';

function verifyUpuahWebhook(rawBody, signatureHeader, secret) {
  const digest = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(digest),
    Buffer.from(signatureHeader),
  );
}

Connect with JavaScript

For custom UIs on your server (Node) or a backend proxy. Prefer the CDN widget for site visitors.

From the browser, call your own backend that holds the key — never expose sesh_ keys in front-end code.

Node / backend

const API = 'https://upuah.com/api/v1';
const KEY = process.env.SESH_API_KEY;

async function startChat({ channelId, name, email, subject, message }) {
  const res = await fetch(`${API}/conversations`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({
      channel_id: channelId,
      name,
      email,
      subject,
      message,
    }),
  });

  if (!res.ok) throw new Error(await res.text());
  const { data } = await res.json();
  return data.uuid;
}

async function sendMessage(uuid, body) {
  const res = await fetch(`${API}/conversations/${uuid}/messages`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify({ body }),
  });
  return res.json();
}

async function listMessages(uuid) {
  const res = await fetch(`${API}/conversations/${uuid}/messages`, {
    headers: {
      Authorization: `Bearer ${KEY}`,
      Accept: 'application/json',
    },
  });
  return res.json();
}

Connect with PHP

Store the key in your .env (for example SESH_API_KEY). Use cURL or Guzzle from your server.

PHP cURL

$api = 'https://upuah.com/api/v1';
$key = getenv('SESH_API_KEY');

$payload = json_encode([
    'channel_id' => 1,
    'name' => 'Nora',
    'email' => 'nora@example.com',
    'subject' => 'Checkout stuck',
    'message' => 'Pay button stays disabled.',
]);

$ch = curl_init($api.'/conversations');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.$key,
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);

Connect with Python

Use requests (or httpx). Keep the key in an environment variable on the server.

Python requests

import os
import requests

API = 'https://upuah.com/api/v1'
KEY = os.environ['SESH_API_KEY']
HEADERS = {
    'Authorization': f'Bearer {KEY}',
    'Accept': 'application/json',
}

r = requests.post(
    f'{API}/conversations',
    headers={**HEADERS, 'Content-Type': 'application/json'},
    json={
        'channel_id': 1,
        'name': 'Nora',
        'email': 'nora@example.com',
        'subject': 'Checkout stuck',
        'message': 'Pay button stays disabled.',
    },
)
r.raise_for_status()
uuid = r.json()['data']['uuid']

messages = requests.get(
    f'{API}/conversations/{uuid}/messages',
    headers=HEADERS,
).json()

Errors

401 unauthenticated — missing/invalid API key.

403 forbidden — Origin not in allowed_origins for the widget.

422 channel_required — create a channel / set default ticket channel.

422 contact_required — provide email or phone (server chat / tickets).

422 Waiting for an agent to join — visitor message while status is pending.

422 validation — field errors in the JSON body.