Appearance
Server setup
New here?
Start with the Quick start, then use this page for environment variables, multi-language session routes, and production checklist.
Configure your backend to call the Sendsar API and issue session tokens to your apps.
Your server holds the API key (sk_…, long-lived). Client apps never receive it — they use a session JWT from your session route. See Authentication for what each credential can do, then Client SDKs to connect and build chat UI.
No server SDK yet
There is no official server-side SDK. Every example below is a raw HTTPS call with x-api-key — same steps in every language.
Architecture
Your backend Sendsar API Browser / mobile
│ │ │
│ API key (x-api-key) │ │
├──── POST /v1/users ─────────►│ │
├──── POST /v1/auth/token ────►│ │
│ │ │
│ GET /api/chat/session │ │
│◄──── logged-in user ─────────┼──────────────────────────┤
│ │ │
│ { token, apiUrl, chatUserId } │
├──────────────────────────────┼─────────────────────────►│
│ │◄──── session JWT + WS ───┤| You configure | Where | Secret? |
|---|---|---|
| API key | Your server / secrets manager | Yes |
| Webhook secret | Your webhook handler | Yes |
| Session route | Your API (e.g. /api/chat/session) | Returns JWT only |
| API URL | Your server env | No — public endpoint |
{API_URL} is your Sendsar API base URL, for example https://api.sendsar.com/v1.
Credentials
Each tenant receives:
| Credential | Format | Used for |
|---|---|---|
| API key | sk_… | Server-side REST (x-api-key), minting session tokens |
| Webhook secret | whsec_… | Verifying inbound webhook signatures |
Store both in your secrets manager and rotate on your normal schedule.
API key is server-only
Never put the API key in front-end code, mobile app bundles, or environment variables exposed to the browser.
Environment variables
Set these on your application server:
| Variable | Example | Purpose |
|---|---|---|
SENDSAR_API_URL | https://api.sendsar.com/v1 | Sendsar API base URL |
SENDSAR_API_KEY | sk_live_… | Tenant API key |
SENDSAR_WEBHOOK_SECRET | whsec_… | Webhook signature verification (optional) |
Session token flow
Your session route performs these two API requests (in order) for each authenticated user:
Why upsert first?
Sendsar does not know your users until you register them. POST /auth/token returns 404 if the user does not exist. Upsert is safe on every session request — it creates or updates the profile. See Authentication for details.
1. Upsert user — POST {API_URL}/users
bash
curl -s -X POST "${SENDSAR_API_URL}/users" \
-H "x-api-key: ${SENDSAR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "id": "user_abc123", "username": "Alice" }'2. Mint session token — POST {API_URL}/auth/token
bash
curl -s -X POST "${SENDSAR_API_URL}/auth/token" \
-H "x-api-key: ${SENDSAR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "userId": "user_abc123", "expiresIn": 3600 }'Example token response:
json
{
"token": "<session-jwt>",
"expiresAt": "2026-05-30T12:00:00.000Z"
}Session route
Add one authenticated endpoint on your backend. Apps call your route — not Sendsar directly.
Example path: GET /api/chat/session
Steps (same in every language):
- Authenticate with your auth (not shown — use your existing login/session).
POST {API_URL}/userswithx-api-key.POST {API_URL}/auth/tokenwithx-api-key.- Return JSON to the client (include
apiUrlfrom your env).
js
// Express, Next.js Route Handler, etc.
const API_URL = process.env.SENDSAR_API_URL;
const API_KEY = process.env.SENDSAR_API_KEY;
export async function getChatSession(req, res) {
const user = req.user; // your auth middleware
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const chatUserId = String(user.id);
const headers = {
"Content-Type": "application/json",
"x-api-key": API_KEY,
};
const upsertRes = await fetch(`${API_URL}/users`, {
method: "POST",
headers,
body: JSON.stringify({ id: chatUserId, username: user.displayName }),
});
if (!upsertRes.ok) {
return res.status(503).json({ error: "Chat unavailable" });
}
const tokenRes = await fetch(`${API_URL}/auth/token`, {
method: "POST",
headers,
body: JSON.stringify({ userId: chatUserId, expiresIn: 3600 }),
});
if (!tokenRes.ok) {
return res.status(503).json({ error: "Chat unavailable" });
}
const token = await tokenRes.json();
return res.json({
token: token.token,
expiresAt: token.expiresAt,
apiUrl: API_URL,
chatUserId,
displayName: user.displayName,
});
}php
Route::middleware('auth:sanctum')->get('/chat/session', function (Request $request) {
$user = $request->user();
$chatUserId = (string) $user->id;
$apiUrl = config('sendsar.api_url');
$apiKey = config('sendsar.api_key');
$headers = ['x-api-key' => $apiKey];
Http::withHeaders($headers)
->post("{$apiUrl}/users", [
'id' => $chatUserId,
'username' => $user->name,
])
->throw();
$token = Http::withHeaders($headers)
->post("{$apiUrl}/auth/token", [
'userId' => $chatUserId,
'expiresIn' => 3600,
])
->throw()
->json();
return response()->json([
'token' => $token['token'],
'expiresAt' => $token['expiresAt'],
'apiUrl' => $apiUrl,
'chatUserId' => $chatUserId,
'displayName' => $user->name,
]);
});py
API_URL = os.environ["SENDSAR_API_URL"]
API_KEY = os.environ["SENDSAR_API_KEY"]
HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"}
@router.get("/api/chat/session")
async def chat_session(user=Depends(get_current_user)):
chat_user_id = str(user.id)
async with httpx.AsyncClient() as client:
upsert = await client.post(
f"{API_URL}/users",
headers=HEADERS,
json={"id": chat_user_id, "username": user.display_name},
)
upsert.raise_for_status()
token_res = await client.post(
f"{API_URL}/auth/token",
headers=HEADERS,
json={"userId": chat_user_id, "expiresIn": 3600},
)
token_res.raise_for_status()
token = token_res.json()
return {
"token": token["token"],
"expiresAt": token["expiresAt"],
"apiUrl": API_URL,
"chatUserId": chat_user_id,
"displayName": user.display_name,
}go
func ChatSession(w http.ResponseWriter, r *http.Request) {
user, ok := auth.UserFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"Unauthorized"}`, http.StatusUnauthorized)
return
}
apiURL := os.Getenv("SENDSAR_API_URL")
apiKey := os.Getenv("SENDSAR_API_KEY")
chatUserID := strconv.FormatInt(user.ID, 10)
upsertBody, _ := json.Marshal(map[string]string{
"id": chatUserID, "username": user.Name,
})
upsertReq, _ := http.NewRequest(http.MethodPost, apiURL+"/users", bytes.NewReader(upsertBody))
upsertReq.Header.Set("Content-Type", "application/json")
upsertReq.Header.Set("x-api-key", apiKey)
upsertRes, err := http.DefaultClient.Do(upsertReq)
if err != nil || upsertRes.StatusCode >= 300 {
http.Error(w, `{"error":"Chat unavailable"}`, http.StatusServiceUnavailable)
return
}
upsertRes.Body.Close()
tokenBody, _ := json.Marshal(map[string]any{
"userId": chatUserID, "expiresIn": 3600,
})
tokenReq, _ := http.NewRequest(http.MethodPost, apiURL+"/auth/token", bytes.NewReader(tokenBody))
tokenReq.Header.Set("Content-Type", "application/json")
tokenReq.Header.Set("x-api-key", apiKey)
tokenRes, err := http.DefaultClient.Do(tokenReq)
if err != nil || tokenRes.StatusCode >= 300 {
http.Error(w, `{"error":"Chat unavailable"}`, http.StatusServiceUnavailable)
return
}
defer tokenRes.Body.Close()
var token struct {
Token string `json:"token"`
ExpiresAt string `json:"expiresAt"`
}
json.NewDecoder(tokenRes.Body).Decode(&token)
json.NewEncoder(w).Encode(map[string]any{
"token": token.Token, "expiresAt": token.ExpiresAt,
"apiUrl": apiURL, "chatUserId": chatUserID, "displayName": user.Name,
})
}ruby
def show
chat_user_id = current_user.id.to_s
api_url = ENV.fetch("SENDSAR_API_URL")
api_key = ENV.fetch("SENDSAR_API_KEY")
headers = { "x-api-key" => api_key, "Content-Type" => "application/json" }
upsert = Faraday.post("#{api_url}/users", { id: chat_user_id, username: current_user.name }.to_json, headers)
raise "Chat unavailable" unless upsert.success?
token_res = Faraday.post(
"#{api_url}/auth/token",
{ userId: chat_user_id, expiresIn: 3600 }.to_json,
headers,
)
raise "Chat unavailable" unless token_res.success?
token = JSON.parse(token_res.body)
render json: {
token: token["token"],
expiresAt: token["expiresAt"],
apiUrl: api_url,
chatUserId: chat_user_id,
displayName: current_user.name,
}
endResponse to your mobile/web app
json
{
"token": "<session-jwt>",
"expiresAt": "2026-05-30T12:00:00.000Z",
"apiUrl": "https://api.sendsar.com/v1",
"chatUserId": "user_abc123",
"displayName": "Alice"
}The client SDK uses token and apiUrl for REST and realtime. apiUrl must be your production Sendsar API URL.
Reference
- REST examples: Server API examples
- Webhooks: Webhooks
- Push notifications: Push notifications
Checklist
- [ ] API key stored server-side only
- [ ] Session route protected by your authentication
- [ ]
chatUserIdmapped stably from your user ID - [ ]
apiUrlin session response points to production Sendsar API - [ ] Session tokens refreshed before expiry
- [ ] Webhook signatures verified (if enabled)
- [ ] Push: tenant app configured and device tokens registered (if using notifications)