Visitor SDK trust boundary

Build the backend token endpoint for Ringnity SDK apps

Use this guide when Android, iOS, React Native, Flutter, Web, or another customer app needs a Ringnity runtime token. The app asks the customer backend for a short-lived token. The customer backend keeps the Ringnity Server API key private.

Visitor App

Android, Swift, React Native, Flutter, or Web.

Customer Backend

Authenticates user and stores Server API key.

Ringnity API

Creates short-lived SDK runtime token.

The mobile app never talks to the Ringnity Server API with a secret key. It only receives a short-lived runtime token from the customer backend.

Architecture

The backend is the security gate

Do not let mobile or browser apps create Ringnity tokens directly with the Server API key. Your backend validates the customer app user, creates trusted visitor context, and returns only the short-lived token needed by the SDK.

flow
Visitor App
  -> POST /ringnity/runtime-token
  -> Customer Backend
  -> POST /api/server/visitor-context-token
  -> Ringnity API
  <- short-lived token
  <- SDK initializes with tokenProvider

What the app receives

The app receives the smallest possible response: token, expiry, and token type. It does not receive the Server API key, tenant internal id, credential metadata, or scopes.

json
{
  "token": "eyJhbGciOi...",
  "expiresIn": 900,
  "tokenType": "Bearer"
}

Boundaries

Separate app, customer backend, and Ringnity responsibilities

This split keeps SDK integration easy for mobile teams while preserving the security model that prevents secret keys from entering client apps.

Customer backend

Owns user authentication, checks who is allowed to request support, stores the Server API key, and issues short-lived Ringnity runtime tokens.

Ringnity Server API

Validates the Server API key, tenant, scope, and trusted visitor context, then returns a short-lived token for the SDK runtime.

Mobile or web app

Calls the customer backend token endpoint, receives only a short-lived token, and initializes the visitor SDK with tokenProvider.

Server setup

Backend requirements

The sample below uses Node.js and Express, but the same contract works from Java, Go, PHP, Laravel, Rails, .NET, or any backend that can send HTTPS requests.

Runtime

Node.js 18+ for the sample server. Any backend language can use the same HTTP contract.

Transport

HTTPS in staging and production. Local HTTP is acceptable only for development.

Ringnity credential

Server API key with server:visitor-token:write. Add agent/admin token scopes only when needed.

Secret storage

Environment variable, vault, secret manager, or platform secret. Never mobile source code.

Customer auth

Validate the app user before creating a Ringnity token.

Token lifetime

Short expiry, usually 900 seconds for visitor sessions.

Scopes

Use the final Server API scope names

For visitor app samples, the customer backend needs server:visitor-token:write. Add the other token scopes only if the same backend also issues Agent or Admin SDK tokens.

server:visitor-token:write

Required for Visitor/Client App SDK token exchange.

server:agent-token:write

Only needed if this backend also issues Agent SDK tokens.

server:admin-token:write

Only needed if this backend also issues Admin SDK tokens.

server:tenant:read

Optional readiness and tenant checks.

server:health:read

Optional health check for credential validation.

Secrets

Store the Ringnity Server API key on the backend only

Create the Server API key from SDK Variables or Dashboard Developer settings, then put the secret into your backend environment or secret manager.

.env
RINGNITY_API_BASE_URL=https://api.ringnity.com
RINGNITY_SERVER_API_KEY=rn_sk_live_xxx

Do not copy the key into app code

Android APK, iOS IPA, React Native bundles, Flutter binaries, browser JavaScript, and app remote config are not safe places for Server API keys.

Node.js

Create POST /ringnity/runtime-token

This endpoint belongs to the customer backend. It validates the customer app user, asks Ringnity for a trusted visitor context token, then returns the runtime token to the app.

bash
npm install @ringnity/server-api express cors helmet
typescript
import cors from "cors";
import express from "express";
import helmet from "helmet";
import { RingnityServerApi } from "@ringnity/server-api";

const app = express();
app.use(helmet());
app.use(cors({ origin: "https://app.customer.com", credentials: true }));
app.use(express.json());

const ringnity = RingnityServerApi.create({
  apiBaseUrl: process.env.RINGNITY_API_BASE_URL || "https://api.ringnity.com",
  apiKey: process.env.RINGNITY_SERVER_API_KEY
});

async function requireCustomer(req) {
  // Replace this with your real customer auth/session lookup.
  // Return null or throw when the app user is not allowed to request support.
  return {
    id: req.body.userId || "customer-123",
    name: req.body.name || "Demo Customer",
    email: req.body.email || "customer@example.com",
    phone: req.body.phone || null
  };
}

app.post("/ringnity/runtime-token", async (req, res) => {
  try {
    const customer = await requireCustomer(req);

    const result = await ringnity.tokens.visitorContext({
      visitor: {
        externalId: customer.id,
        name: customer.name,
        email: customer.email,
        phone: customer.phone
      },
      metadata: {
        source: req.body.source || "visitor-app",
        appUserId: customer.id
      },
      expiresIn: 900
    });

    res.json({
      token: result.contextToken,
      expiresIn: result.expiresIn,
      tokenType: result.tokenType || "Bearer"
    });
  } catch (error) {
    console.error("[ringnity] runtime token failed", {
      message: error?.message,
      code: error?.code,
      status: error?.status
    });

    res.status(502).json({
      code: "RINGNITY_TOKEN_FAILED",
      message: "Unable to create Ringnity runtime token."
    });
  }
});

app.listen(3000, () => {
  console.log("Ringnity token server listening on http://localhost:3000");
});

HTTP contract

Call Ringnity directly if you are not using the Node helper

The helper package is optional. Backends in other languages can call the Server API endpoint with normal HTTPS and the same server:visitor-token:write scope.

curl
curl -X POST "https://api.ringnity.com/api/server/visitor-context-token" \
  -H "Authorization: Bearer $RINGNITY_SERVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "visitor": {
      "externalId": "customer-123",
      "name": "Demo Customer",
      "email": "customer@example.com"
    },
    "metadata": {
      "source": "android-app"
    },
    "expiresIn": 900
  }'

Test your own customer backend endpoint separately. The app should call your backend, not the Ringnity Server API.

curl
curl -X POST "https://customer-api.example.com/ringnity/runtime-token" \
  -H "Authorization: Bearer CUSTOMER_APP_SESSION" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "android-app"
  }'

App contract

Return one simple response shape to every client app

Use the same response body for Android, Swift, React Native, Flutter, and Web. That makes each SDK sample easier to maintain.

json
{
  "token": "eyJhbGciOi...",
  "expiresIn": 900,
  "tokenType": "Bearer"
}

SDK wiring

Each SDK calls the customer backend token endpoint

The app-specific SDK code is small. Each platform provides a tokenProvider function that calls the customer backend and returns the token response.

Android Kotlin tokenProvider

kotlin
suspend fun fetchRingnityToken(): RingnityRuntimeToken {
    val response = customerBackend.post<RuntimeTokenResponse>(
        "/ringnity/runtime-token",
        mapOf("source" to "android-app")
    )

    return RingnityRuntimeToken(
        token = response.token,
        expiresIn = response.expiresIn,
        tokenType = response.tokenType ?: "Bearer"
    )
}

val ringnity = Ringnity.create(
    RingnityConfig(
        tokenProvider = ::fetchRingnityToken,
        callMode = RingnityCallMode.BASIC
    )
)

Swift tokenProvider

swift
func fetchRingnityToken() async throws -> RingnityRuntimeToken {
    let request = RuntimeTokenRequest(source: "ios-app")
    let response: RuntimeTokenResponse = try await customerBackend.post(
        "/ringnity/runtime-token",
        body: request
    )

    return RingnityRuntimeToken(
        token: response.token,
        expiresIn: response.expiresIn,
        tokenType: response.tokenType ?? "Bearer"
    )
}

let ringnity = RingnityClient(
    configuration: .init(tokenProvider: fetchRingnityToken)
)

React Native tokenProvider

typescript
import { createRingnity } from "@ringnity/react-native-sdk";

async function fetchRingnityToken() {
  const response = await fetch("https://customer-api.example.com/ringnity/runtime-token", {
    method: "POST",
    headers: {
      "Authorization": "Bearer CUSTOMER_APP_SESSION",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ source: "react-native-app" })
  });

  if (!response.ok) throw new Error("Ringnity token request failed");
  return response.json();
}

export const ringnity = createRingnity({
  tokenProvider: fetchRingnityToken
});

Flutter tokenProvider

dart
Future<RingnityRuntimeToken> fetchRingnityToken() async {
  final response = await customerBackend.post(
    '/ringnity/runtime-token',
    data: {'source': 'flutter-app'},
  );

  return RingnityRuntimeToken(
    token: response.data['token'] as String,
    expiresIn: response.data['expiresIn'] as int,
    tokenType: response.data['tokenType'] as String? ?? 'Bearer',
  );
}

final ringnity = Ringnity.create(
  RingnityConfig(tokenProvider: fetchRingnityToken),
);

Lifecycle

Issue short-lived tokens and refresh them through tokenProvider

The SDK can ask tokenProvider for a fresh token when the app starts, before support begins, or after an unauthorized runtime response.

  1. 1App opens the support entry point.
  2. 2App calls POST /ringnity/runtime-token on the customer backend.
  3. 3Customer backend validates the app user/session.
  4. 4Customer backend calls Ringnity Server API with the Server API key.
  5. 5Ringnity returns a short-lived visitor context token.
  6. 6Customer backend returns only token, expiresIn, and tokenType to the app.
  7. 7SDK initializes or refreshes with tokenProvider.

Security

Production checklist

This checklist is the minimum before sharing a sample app with external developers or a real customer.

  • Never ship RINGNITY_SERVER_API_KEY in Android, iOS, Flutter, React Native, browser code, remote config, public repos, logs, crash reports, or app assets.
  • Validate the customer app user before issuing the token.
  • Keep runtime token responses small. Do not return credential id, tenant internal id, scopes, or debug payloads to the app.
  • Use HTTPS and normal customer auth protections such as session cookies, bearer tokens, or signed app requests.
  • Rate-limit the token endpoint by user, device, IP, and tenant when exposed publicly.
  • Log token issuance metadata without printing tokens or secret keys.

Troubleshooting

Common errors

Most token server issues come from missing Server API keys, missing final scopes, or app code calling runtime APIs before tokenProvider resolves.

SERVER_API_KEY_MISSING

The backend did not send Authorization: Bearer SERVER_API_KEY.

SERVER_API_SCOPE_DENIED

The Server API key does not include server:visitor-token:write. Create or update the credential from SDK Variables.

SDK_ORIGIN_NOT_ALLOWED

For web, add the real website domain. For mobile, make sure the customer backend is not forwarding an invalid Origin header.

SDK_SESSION_REQUIRED

The app tried to call SDK runtime APIs before tokenProvider returned a valid token.

MISSING_TENANT

The request did not resolve to a tenant. Check the Server API key, tenant slug, and backend environment.

Downloads

Download the backend helper and the SDK platform package

Current preview bundle: 0.2.0-beta.0. Every mobile sample should use the matching platform SDK plus the Server API SDK for the customer backend token endpoint.

Last updated: June 30, 2026

Turn Your Website Into a Real-Time Call Center

Let customers call your team directly from your website, no phone numbers and no apps required. Just add one <script>.