Skip to content

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 configureWhereSecret?
API keyYour server / secrets managerYes
Webhook secretYour webhook handlerYes
Session routeYour API (e.g. /api/chat/session)Returns JWT only
API URLYour server envNo — public endpoint

{API_URL} is your Sendsar API base URL, for example https://api.sendsar.com/v1.


Credentials

Each tenant receives:

CredentialFormatUsed for
API keysk_…Server-side REST (x-api-key), minting session tokens
Webhook secretwhsec_…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:

VariableExamplePurpose
SENDSAR_API_URLhttps://api.sendsar.com/v1Sendsar API base URL
SENDSAR_API_KEYsk_live_…Tenant API key
SENDSAR_WEBHOOK_SECRETwhsec_…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):

  1. Authenticate with your auth (not shown — use your existing login/session).
  2. POST {API_URL}/users with x-api-key.
  3. POST {API_URL}/auth/token with x-api-key.
  4. Return JSON to the client (include apiUrl from 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,
  }
end

Response 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


Checklist

  • [ ] API key stored server-side only
  • [ ] Session route protected by your authentication
  • [ ] chatUserId mapped stably from your user ID
  • [ ] apiUrl in 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)

Next steps

Sendsar — headless chat for B2B platforms