Cariosan

Backend integration (any language)

Cariosan is plain REST. Wire it from Node, Python, PHP, Ruby, Java, .NET, Bash — anything with an HTTP client.

7 min readUpdated Oct 11, 2026

Cariosan ships official SDKs for Go (cariosan-go) and TypeScript (@cariosan/client), but you don't have to use them. The wire surface is plain REST + WebSocket. Any backend that can speak HTTP can integrate.

This page shows you the minimum integration in five popular backends so you can copy and adapt.

What your backend needs to do

In the typical Cariosan setup, your backend has exactly one job: hand your frontend a short-lived user JWT, which it gets from Cariosan with your workspace's API key and secret. The frontend talks directly to Cariosan after that, and asks your endpoint again whenever it needs a new token.

┌─────────────┐    ┌──────────────────┐    ┌─────────────┐
│  Browser /  │ ←→ │  Your backend    │ ←→ │  Cariosan   │
│  Mobile app │    │  (1 endpoint)    │    │  REST API   │
└─────┬───────┘    └──────────────────┘    └─────────────┘
      │                                           ▲
      └────────── WebSocket (with JWT) ───────────┘

The request your backend makes to Cariosan for each token ({external_id} is your user's id, created with POST /v1/users first):

POST /v1/users/{external_id}/token HTTP/1.1
Host: api.cariosan.com
Authorization: Bearer pk_live_…:sk_live_…
Content-Type: application/json

{"ttl_seconds": 600}

The response:

{ "token": "eyJhbGciOiJIUzI1NiI...", "expires_at": "2026-10-11T10:10:00Z" }

Hand that token to your frontend. That's it.

ttl_seconds sets the lifetime (ttl_hours also works; omit both for the server default, CARIOS_JWT_TTL_HOURS). Prefer short tokens, about 10 minutes, together with a token provider on the frontend. The SDK then asks your endpoint for a new token shortly before each one expires, so a leaked token is only useful for minutes and nothing needs to be stored on the device.

Keep api_secret on the server

The sk_* secret must never reach a browser. Storing it in env vars on your server is the standard pattern. If it leaks, rotate it.

Examples

The flow in every backend is the same: upsert the user → issue a JWT → return it to the frontend. Only the syntax changes. Each endpoint is called by the frontend's token provider, both for the first token and for every renewal, so it must authenticate the caller with your own session (cookie or app token).

Node.js (Express + built-in fetch)

server.js
import express from 'express'
 
const app = express()
app.use(express.json())
 
const AUTH = `Bearer ${process.env.CARIOSAN_API_KEY}:${process.env.CARIOSAN_API_SECRET}`
const HEADERS = { Authorization: AUTH, 'Content-Type': 'application/json' }
 
app.post('/api/cariosan-token', async (req, res) => {
  const user = req.user           // pulled from your existing auth middleware
  if (!user) return res.status(401).end()
 
  // 1. Upsert the user in Cariosan.
  await fetch('https://api.cariosan.com/v1/users', {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ external_id: user.id, name: user.name }),
  })
 
  // 2. Issue a short-lived JWT scoped to that user.
  const resp = await fetch(`https://api.cariosan.com/v1/users/${encodeURIComponent(user.id)}/token`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ ttl_seconds: 600 }),
  })
  if (!resp.ok) return res.status(502).end()
  const { token, expires_at } = await resp.json()
  res.json({ token, expires_at })
})
 
app.listen(3000)

Python (Flask + requests)

import os
from urllib.parse import quote
from flask import Flask, jsonify, request
import requests

app = Flask(__name__)

HEADERS = {'Authorization': f"Bearer {os.environ['CARIOSAN_API_KEY']}:{os.environ['CARIOSAN_API_SECRET']}"}

@app.post('/api/cariosan-token')
def cariosan_token():
    user = current_user()        # your auth helper
    if not user:
        return '', 401

    requests.post(
        'https://api.cariosan.com/v1/users',
        headers=HEADERS,
        json={'external_id': user.id, 'name': user.name},
    )

    resp = requests.post(
        f'https://api.cariosan.com/v1/users/{quote(str(user.id), safe="")}/token',
        headers=HEADERS,
        json={'ttl_seconds': 600},
    )
    resp.raise_for_status()
    return jsonify(resp.json())  # {"token": ..., "expires_at": ...}

PHP (Laravel)

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;

Route::post('/cariosan-token', function () {
    $user = auth()->user();
    if (! $user) {
        return response('', 401);
    }

    $auth = [
        'Authorization' => 'Bearer '.env('CARIOSAN_API_KEY').':'.env('CARIOSAN_API_SECRET'),
    ];

    Http::withHeaders($auth)->post('https://api.cariosan.com/v1/users', [
        'external_id' => $user->id,
        'name' => $user->name,
    ]);

    $resp = Http::withHeaders($auth)->post(
        'https://api.cariosan.com/v1/users/'.rawurlencode((string) $user->id).'/token',
        ['ttl_seconds' => 600],
    );

    return response()->json($resp->throw()->json()); // {"token": ..., "expires_at": ...}
})->middleware('auth');

Ruby (Rails)

class CariosanController < ApplicationController
  before_action :authenticate_user!

  def token
    auth = "Bearer #{ENV['CARIOSAN_API_KEY']}:#{ENV['CARIOSAN_API_SECRET']}"
    headers = { 'Authorization' => auth, 'Content-Type' => 'application/json' }

    HTTParty.post(
      'https://api.cariosan.com/v1/users',
      headers: headers,
      body: { external_id: current_user.id, name: current_user.name }.to_json,
    )

    resp = HTTParty.post(
      "https://api.cariosan.com/v1/users/#{ERB::Util.url_encode(current_user.id.to_s)}/token",
      headers: headers,
      body: { ttl_seconds: 600 }.to_json,
    )

    render json: resp.parsed_response.slice('token', 'expires_at')
  end
end

Bash (curl, for quick CI / scripting)

terminal
AUTH="Bearer $CARIOSAN_API_KEY:$CARIOSAN_API_SECRET"
USER_ID="alice"
 
# 1. Upsert
curl -X POST https://api.cariosan.com/v1/users \
  -H "Authorization: $AUTH" -H 'Content-Type: application/json' \
  -d "{\"external_id\":\"$USER_ID\",\"name\":\"Alice\"}"
 
# 2. Issue a 10-minute JWT
curl -X POST "https://api.cariosan.com/v1/users/$USER_ID/token" \
  -H "Authorization: $AUTH" -H 'Content-Type: application/json' \
  -d '{"ttl_seconds":600}'

The frontend side: a token provider

Give the SDK a function that calls your endpoint instead of a fixed token. It is called for the first token, shortly before each token expires, and when Cariosan rejects one; the open WebSocket moves to each new token without reconnecting.

chat.ts
import { Cariosan } from "@cariosan/client";
 
const client = new Cariosan({
  apiUrl: "https://api.cariosan.com",
  token: async () => {
    const res = await fetch("/api/cariosan-token", { method: "POST", credentials: "include" });
    if (!res.ok) throw new Error(`chat token: ${res.status}`); // the SDK reports it and retries
    const { token } = await res.json();
    return token; // Cariosan tokens carry iat/exp, so the SDK knows when to renew
  },
});
await client.connect();

Return a freshly issued token each time: the SDK times renewal from the token's lifetime, counted from when it arrives. Create the client once per signed-in user. Details: Tokens.

When to use an official SDK

The official SDKs (cariosan-go, @cariosan/client) give you typed responses, automatic retries, and convenience methods for non-JWT operations (uploading attachments, building channel queries). If your backend is already in Go or TypeScript, the SDK is the easier path.

For everything else, raw HTTP is fully supported and never going to go away. The REST surface is documented in the API reference.

Optional: webhooks

If you want to react to events (a new message arrived, a channel was created, a user joined), point Cariosan at a URL on your backend and verify the X-Cariosan-Signature header. See Webhooks guide.

This is also language-agnostic: the signature is HMAC-SHA256 of the raw body using your api_secret as the key.

Optional: server-side operations

Anything you can do via the SDK, you can do via raw HTTP:

  • Create channels: POST /v1/channels
  • Add/remove members: POST /v1/channels/{id}/members
  • Issue user tokens: POST /v1/users/{external_id}/token, and revoke them all: POST /v1/users/{external_id}/revoke-tokens

Sending messages and presigning attachments are user actions: they take a user JWT, not the API key.

Browse the full surface in the API reference.

Minimum-viable backend without any framework

If you don't have a backend yet and don't want to spin up Express / Flask / Rails just for this, use a serverless function:

  • Vercel / Netlify functions — single .js or .ts file, deploys with git push.
  • Cloudflare Workers — same shape, runs at the edge.
  • AWS Lambda + API Gateway — slightly more setup but free tier is generous.

The Node example above is a complete Vercel function with no edits needed (just rename to api/cariosan-token.js).

Next steps

Was this page helpful?