Configure connector_base_url and the generated connector_secret in the dashboard. Every request is POST {base}{path} with a JSON body and X-Supportal-Signature header. Timeout 10s; responses ≤ 32KB.
Request body
{
"requestId": "req_abc",
"tenant": "acme",
"user": { "id": "user_123" },
"conversationId": "conv_xyz",
"action": "get_user_context",
"input": {},
"timestamp": 1710000000123
}The user.id is always taken from the verified session — never from model arguments. Unverified sessions do not receive connector tools.
Signature
Header format: X-Supportal-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(connector_secret, "{t}.{rawBody}"). Reject if |now - t| > 300 seconds.
Verify — Node.js
import crypto from "node:crypto";
export function verifySupportalSignature(rawBody, header, secret) {
const m = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(header ?? "");
if (!m) return false;
const t = Number(m[1]);
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return expected === m[2];
}Verify — Python
import hmac, hashlib, re, time
def verify_supportal_signature(raw_body: bytes, header: str, secret: str) -> bool:
m = re.match(r"t=(\d+),v1=([a-f0-9]+)", header or "", re.I)
if not m:
return False
t = int(m.group(1))
if abs(time.time() - t) > 300:
return False
msg = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return expected == m.group(2)Verify — Go
func VerifySupportalSignature(rawBody []byte, header, secret string) bool {
re := regexp.MustCompile(`^t=(\d+),v1=([a-f0-9]+)$`)
m := re.FindStringSubmatch(header)
if m == nil {
return false
}
t, _ := strconv.ParseInt(m[1], 10, 64)
if math.Abs(float64(time.Now().Unix()-t)) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%d.", t)))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return expected == m[2]
}User context
Default path /user-context (configurable). Return arbitrary JSON about the user — plan, orders, entitlements. Fetched lazily as the get_user_context tool.
Custom actions
Define actions in the dashboard with name, description, JSON Schema input, path, and requires_confirmation. The model may call enabled actions; confirmed actions follow the same awaiting_user flow as refunds.
Refunds
POST {base}/refunds with input { orderId, amountMinor, currency, reason, dryRun }. Response { status: "approved"|"rejected"|"pending", refundId?, message? }. Always honor dryRun: true for eligibility checks before executing a real refund.
{
"orderId": "ord_99",
"amountMinor": 1299,
"currency": "USD",
"reason": "Damaged item",
"dryRun": true
}