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.