Skip to content
Go Live+

Developer API · v1

Developer API

Connect the comments from your own platform to a Go Live Plus live, and take the live and what the AI host says to your platform.

2026-10-05

How it works — honestly

The AI host does not run in our cloud: it runs in the Go Live Plus app on the seller's own Mac or PC, usually behind a home or shop router that accepts no incoming connections. So the API lives at https://v3.aseacast.com/api/v1 and the app keeps a connection open to it (a long poll). Your event is queued for the computer running that session and handed over on that open connection.

  • Events are accepted only while the session is running — live, or rehearsing in Studio — on a signed-in computer. Otherwise you get 409 session_offline; nothing is stored for later (a comment answered ten minutes late is worse than none).
  • Latency: from your POST to the app's comment queue is one HTTPS round trip after your request — typically 100–600 ms on a normal line; a flaky shop connection can add seconds. The spoken answer then takes as long as any answer (knowledge base or language model, then voice).
  • Viewer text stays viewer text. Your comments go through the same triage, blocklist, per-minute reply caps and answer guardrail as TikTok or YouTube comments; the language model sees them fenced as untrusted text. No field can become an instruction to the host.
  • Your platform appears in Studio under the name you give it, and counts in the session report like any platform. The AI-host label stays on.

API keys and scopes

Make keys in Dashboard → Developer / API (owner or admin). A key is shown once and stored only as a hash; revoke it there at any time. Send it as Authorization: Bearer glpk_…. Keep keys on your server — for browsers use a stream token.

ScopeAllows
events:writePOST comments and events
sessions:readrooms, sessions, live status, hosts
stream:readSSE stream, host-event replay, stream tokens
webhooks:manageadd, list, test and delete webhooks
outputs:managecustom RTMP(S) outputs

A session is a Live Session inside a room (Dashboard → Rooms): its id stays the same every time it goes on air, so you configure it once. current means the team's live on air now.

Quickstart

curl
# 1. Is the live running? ("current" = the live on air now)
curl -s https://v3.aseacast.com/api/v1/sessions/current -H "Authorization: Bearer $GLP_KEY"

# 2. Push a viewer comment from your platform
curl -s -X POST https://v3.aseacast.com/api/v1/sessions/current/events \
  -H "Authorization: Bearer $GLP_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-page-1001" \
  -d '{"id":"c-1001","platform":{"id":"myshop","name":"MyShop"},
       "user":{"id":"u42","name":"Aina"},"text":"Berapa harga saiz L?"}'

# 3. Watch what the host says (Server-Sent Events)
curl -N https://v3.aseacast.com/api/v1/sessions/current/stream -H "Authorization: Bearer $GLP_KEY"
JavaScript
// Node 18+ (server side — never ship the key to a browser)
const KEY = process.env.GLP_KEY;
const API = "https://v3.aseacast.com/api/v1";

async function sendComment(sessionId, c) {
  const res = await fetch(`${API}/sessions/${sessionId}/events`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", "Idempotency-Key": c.id },
    body: JSON.stringify({ id: c.id, platform: { id: "myshop", name: "MyShop" },
                           user: { id: c.userId, name: c.userName }, text: c.text }),
  });
  if (res.status === 429) await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") ?? 1)));
  if (!res.ok && res.status !== 409) throw new Error((await res.json()).error.message);
  return res.json();
}

// Browser captions: your server makes a stream token, the page opens EventSource with it.
// server:  POST ${API}/sessions/<id>/stream-tokens  →  { streamUrl }
const es = new EventSource(streamUrl);
es.addEventListener("speech.start", (e) => showCaption(JSON.parse(e.data).data.text));
es.addEventListener("product.shown", (e) => pinProduct(JSON.parse(e.data).data.productId));
Python
import os, requests

API = "https://v3.aseacast.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['GLP_KEY']}"}

s = requests.get(f"{API}/sessions/current", headers=H, timeout=10).json()["session"]
print(s["status"], s["hosts"])

r = requests.post(f"{API}/sessions/{s['id']}/events", headers=H, timeout=10, json={
    "platform": {"id": "myshop", "name": "MyShop"},
    "events": [
        {"kind": "join", "user": {"name": "Ali"}},
        {"id": "c-7", "kind": "question", "user": {"name": "Siti"}, "text": "Ada warna hitam?",
         "productId": "p_tudung"},
        {"kind": "gift", "user": {"name": "Mei"}, "meta": {"giftName": "Rose", "giftValue": 10}},
    ],
})
print(r.status_code, r.json())

# Stream host events
with requests.get(f"{API}/sessions/{s['id']}/stream", headers=H, stream=True, timeout=None) as st:
    for line in st.iter_lines(decode_unicode=True):
        if line.startswith("data:"):
            print(line[5:])

Events in

POST /sessions/{id}/events takes one event or {"platform": …, "events": [ … up to 50 ]}. All or nothing: one invalid event refuses the batch and names its index.

FieldNotes
kindcomment (default), question, join, like, follow, share, gift, subscribe, purchase, purchase_intent. Comments and questions are answered; joins are greeted; gifts, follows, shares, subscriptions and purchases are thanked — by the seller's own interaction rules.
platform"myshop" or {"id": "myshop", "name": "MyShop"} — shown in Studio (≤ 24 characters).
user{id?, name}; the name (≤ 64) may be spoken in a greeting, cleaned of links and symbols.
text≤ 500 characters; required for comment/question. Control characters and template braces are removed.
idYour id; the same id within 15 minutes is a duplicate and is not sent again.
hostIdMulti-host rooms: the host who should answer (ids in GET /sessions/{id}). Otherwise the room's routing rule decides.
productIdThe product the comment is about — helps routing and the answer.
metagiftName, giftValue, count, likeCount, likeTotal, productId, productName, amount, currency, quantity. Other fields are dropped.

Answer: 202 with each event's status (queued / duplicate). What the app then did with it arrives as an event.result host event (accepted, duplicate, blocked, rate_limited, denied — e.g. no free platform slot on the plan). Send Idempotency-Key to make retries safe (24 h).

Host events: SSE and replay

GET /sessions/{id}/stream is a Server-Sent Events stream: a ready event with the session status, then one event per thing the host says or does. Resume with Last-Event-ID. The stream closes after 30 minutes; EventSource reconnects by itself. GET /sessions/{id}/host-events?after=<id> returns the same events as JSON (last 500, two hours).

event shapes
{ "type": "speech.start", "id": "41", "sessionId": "cm…", "runId": "cm…", "at": "2026-10-05T09:12:03.120Z",
  "data": { "utteranceId": "u17", "text": "Saiz L harga RM39, Aina!", "hostId": "host_1", "hostName": "Salmah",
            "kind": "answer", "replyTo": { "eventId": "c-1001", "platform": "MyShop", "user": "Aina",
                                           "text": "Berapa harga saiz L?" } } }
{ "type": "speech.end",     "data": { "utteranceId": "u17", "hostId": "host_1" } }
{ "type": "product.shown",  "data": { "productId": "p_tudung", "name": "Tudung Bawal", "price": "RM39" } }
{ "type": "product.hidden", "data": { "productId": "p_tudung" } }
{ "type": "event.result",   "data": { "eventId": "c-1001", "status": "accepted" } }
{ "type": "session.started" | "session.live" | "session.ended", "data": { "mode": "live" } }

For a web page, make a stream token on your server — POST /sessions/{id}/stream-tokens → streamUrl (read-only, one session, one hour) — and open it with new EventSource(streamUrl). Host events are uploaded by the app only while someone listens (an open stream or a webhook), so they cost the seller nothing otherwise.

Webhooks

Add a URL (https, public address) in the dashboard or with POST /webhooks. We POST {id, createdAt, events: [ … ]} signed with the webhook's secret (shown once): GLP-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.rawBody")>. Answer 2xx within 5 seconds. Failed deliveries are retried after 10 s, 1 min, 5 min and 30 min with the same delivery id — de-duplicate on it. Redirects are not followed.

verify — Node
import crypto from "node:crypto";

// Use the RAW body exactly as received, before JSON.parse.
export function verify(secret, header, rawBody, toleranceS = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=", 2)));
  const t = Number(parts.t);
  if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceS) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length &&
         crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Express: app.post("/glp-webhook", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verify(process.env.GLP_WHSEC, req.get("GLP-Signature"), req.body.toString("utf8"))) return res.sendStatus(400);
//   const { id, events } = JSON.parse(req.body);   // de-duplicate on id
//   res.sendStatus(200);
// });
verify — Python
import hmac, hashlib, time

def verify(secret: str, header: str, raw_body: bytes, tolerance_s: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if "v1" not in parts or abs(time.time() - t) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Taking the live to your platform

Push (available now). Register your RTMP or RTMPS ingest — Mux, Cloudflare Stream, AWS IVS, Livepeer, your own MediaMTX/nginx-rtmp, or any player service — with POST /sessions/{id}/outputs or in the dashboard. When that session goes on air, the seller's computer pushes the finished live (avatar or clip, voice, product cards, the AI-host label) to it, alongside any other platform. The URL and stream key are encrypted and never shown again; only the app of the same team receives them. Each output is one more encoder on the seller's computer and uses one platform slot of the plan; at most 2 per session.

Pull (watch it in your own player). Point an output at a service that gives you a playback URL (HLS or WebRTC/WHEP) and embed that — your service handles viewers and bandwidth. A Go Live Plus–hosted playback URL would mean relaying every viewer's video through our servers; it is not offered yet. On the seller's own computer, the app's local output page (Studio → output link) can also be used as a browser source.

What the host says comes as host events (SSE / webhooks) — use them for captions, pinning the product being shown, or showing which host is speaking.

On the same computer as the app

For software running on the seller's own computer (a kiosk, a local chat bridge, OBS scripts) the app serves the same event and stream calls on 127.0.0.1, with a token made for that installation. Requests with a browser Origin are refused, so web pages cannot use it.

local (127.0.0.1)
# Same computer as the Go Live Plus app — no internet round trip.
# The port is the app's comment bridge (first free of 47850–47859); the token is
# in Studio → Sediakan Live → Output → "Developer API (this computer)".
curl -s -X POST http://127.0.0.1:47850/dev/v1/sessions/current/events \
  -H "Authorization: Bearer $GLP_LOCAL_TOKEN" -H "Content-Type: application/json" \
  -d '{"platform":"kiosk","user":{"name":"Walk-in"},"text":"Ada stok saiz M?"}'

curl -N http://127.0.0.1:47850/dev/v1/sessions/current/stream -H "Authorization: Bearer $GLP_LOCAL_TOKEN"

Limits

Requests per key300 / minute
Events per session20 / second, 600 / minute (all keys together)
Events per request50; body ≤ 64 KB
Open streams per key5
Keys / webhooks / outputs10 active keys and 5 webhooks per team; 2 outputs per session

These protect the seller's computer: it answers one question at a time, so more than a few comments a second only fills its queue (the seller's reply-per-minute cap and queue size still decide what is answered). Over a limit you get 429 with Retry-After. Every answer carries X-RateLimit-Limit/Remaining/Reset.

Errors

Every error is {"error": {"code", "message"}}. Codes are stable; messages may change.

StatusCodeMeaning
401unauthorizedno key, wrong key, revoked key, expired stream token
403insufficient_scope / forbidden / plan_requiredthe key lacks the scope; token for another session; API not on this plan
404not_foundnot in this team
409session_offlinenothing is running that session now
409conflict / limit_reachedIdempotency-Key reused or in progress; too many keys/webhooks/outputs
413 / 415 / 422payload_too_large / unsupported_media_type / invalid_requestbody too big, not JSON, failed validation
429rate_limitedslow down; see Retry-After
500internalour fault; retry with the same Idempotency-Key

Spec: /developers/openapi.yaml (OpenAPI 3.1). Questions: contact us.