Skip to content

Ephemeral Key

Never expose the long-lived secret_key in browser code.

For Web integrations, the browser should receive only a short-lived ephemeral key. In practice, many partners implement a small server endpoint such as POST /api/ephemeral-key, and that endpoint calls the SORI Console API on the server side.

Why Use an Ephemeral Key

In a Web integration, browser code is exposed to the user. If you put your long-lived secret_key in that code, it can be extracted and misused outside your service.

An ephemeral key avoids that problem. Your server keeps the long-lived secret_key private, exchanges it for a short-lived ephemeral key, and returns only that temporary key to the browser. The browser can then use the ephemeral key for authentication and the following SDK API calls without exposing the original secret.

AudioRecognizer should usually receive the key through ephemeralKey.

1. Fetch the key inside ephemeralKey

ts
import { AudioRecognizer } from "@sorisdk/web-audio";

const recognizer = new AudioRecognizer({
  appId: "YOUR_APP_ID",
  ephemeralKey: async () => {
    const response = await fetch("/api/ephemeral-key", {
      method: "POST"
    });

    if (!response.ok) {
      throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
    }

    const { ephemeral_key } = await response.json();
    return ephemeral_key;
  }
});

recognizer.on("campaign", (event) => {
  console.log(event.campaign);
});

await recognizer.start();

2. Fetch the key first, then pass it directly

ts
import { AudioRecognizer } from "@sorisdk/web-audio";

const response = await fetch("/api/ephemeral-key", {
  method: "POST"
});

if (!response.ok) {
  throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}

const { ephemeral_key } = await response.json();

const recognizer = new AudioRecognizer({
  appId: "YOUR_APP_ID",
  ephemeralKey: ephemeral_key
});

recognizer.on("campaign", (event) => {
  console.log(event.campaign);
});

await recognizer.start();

Implementing Your Own Key Exchange Endpoint

If you operate your own endpoint, its job is simple:

  1. Receive a browser request such as POST /api/ephemeral-key.
  2. Read app_id and secret_key from server-side configuration.
  3. Call the SORI Console API ephemeral endpoint on the server.
  4. Return that short-lived response payload to the browser.

With the default SORI API base, the upstream endpoint is:

txt
https://console.soriapi.com/api/auth/ephemeral

The upstream request body is form-encoded:

txt
app_id=YOUR_APP_ID
secret_key=YOUR_SECRET_KEY

Your own endpoint should never return the long-lived secret_key.

SORI Console API Response

In sori_console, POST /api/auth/ephemeral returns this shape on success:

json
{
  "success": true,
  "ephemeral_key": "eyJhbGciOi...",
  "ephemeral_key_expires_at": "2026-03-23T12:34:56Z"
}
  • success: request result
  • ephemeral_key: short-lived key for the browser SDK
  • ephemeral_key_expires_at: expiration time in ISO 8601 format

Server Responsibilities

  • Keep app_id and secret_key in server-side environment variables or secret storage.
  • Call the SORI Console API with form-encoded data.
  • If the upstream response is not successful, return that status and stop.
  • Return only the short-lived response payload to the browser.

Example Implementations

The following examples all implement the same key exchange flow:

  • Node.js: POST /api/ephemeral-key
  • Python: POST /api/ephemeral-key
  • Java: POST /api/ephemeral-key
  • PHP: POST /api/sori_ephemeral_key.php
  • upstream call: POST https://console.soriapi.com/api/auth/ephemeral
  • browser response: same JSON shape as the SORI Console API
js
import express from "express";

const app = express();

const APP_ID = process.env.SORI_APP_ID;
const SECRET_KEY = process.env.SORI_SECRET_KEY;
const SORI_EPHEMERAL_ENDPOINT = "https://console.soriapi.com/api/auth/ephemeral";

app.post("/api/ephemeral-key", async (_req, res) => {
  if (!APP_ID || !SECRET_KEY) {
    res.status(500).json({ detail: "Missing SORI_APP_ID or SORI_SECRET_KEY" });
    return;
  }

  try {
    const upstream = await fetch(SORI_EPHEMERAL_ENDPOINT, {
      method: "POST",
      body: new URLSearchParams({
        app_id: APP_ID,
        secret_key: SECRET_KEY
      })
    });

    const raw = await upstream.text();

    if (!upstream.ok) {
      res.status(upstream.status).type("application/json").send(raw);
      return;
    }

    res.type("application/json").send(raw);
  } catch (error) {
    res.status(500).json({
      detail: error instanceof Error ? error.message : String(error)
    });
  }
});

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});
python
import os

import requests
from fastapi import FastAPI
from fastapi.responses import JSONResponse, Response

app = FastAPI()

APP_ID = os.getenv("SORI_APP_ID")
SECRET_KEY = os.getenv("SORI_SECRET_KEY")
SORI_EPHEMERAL_ENDPOINT = "https://console.soriapi.com/api/auth/ephemeral"


@app.post("/api/ephemeral-key")
def create_sori_ephemeral_key():
    if not APP_ID or not SECRET_KEY:
        return JSONResponse(
            status_code=500,
            content={"detail": "Missing SORI_APP_ID or SORI_SECRET_KEY"},
        )

    try:
        upstream = requests.post(
            SORI_EPHEMERAL_ENDPOINT,
            data={
                "app_id": APP_ID,
                "secret_key": SECRET_KEY,
            },
            timeout=10,
        )
    except requests.RequestException as exc:
        return JSONResponse(status_code=500, content={"detail": str(exc)})

    if not upstream.ok:
        return Response(
            content=upstream.text,
            status_code=upstream.status_code,
            media_type="application/json",
        )

    return Response(content=upstream.text, media_type="application/json")
java
package example.sori;

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class SoriEphemeralKeyController {
    private final OkHttpClient httpClient = new OkHttpClient();
    private final ObjectMapper objectMapper = new ObjectMapper();

    private final String appId = System.getenv("SORI_APP_ID");
    private final String secretKey = System.getenv("SORI_SECRET_KEY");
    private static final String SORI_EPHEMERAL_ENDPOINT =
        "https://console.soriapi.com/api/auth/ephemeral";

    @PostMapping(value = "/api/ephemeral-key", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> createEphemeralKey() {
        if (isBlank(appId) || isBlank(secretKey)) {
            return json(500, Map.of(
                "detail", "Missing SORI_APP_ID or SORI_SECRET_KEY"
            ));
        }

        RequestBody body = new FormBody.Builder()
            .add("app_id", appId)
            .add("secret_key", secretKey)
            .build();

        Request request = new Request.Builder()
            .url(SORI_EPHEMERAL_ENDPOINT)
            .post(body)
            .build();

        try (Response response = httpClient.newCall(request).execute()) {
            String rawBody = response.body() != null ? response.body().string() : "{}";

            if (!response.isSuccessful()) {
                return ResponseEntity.status(response.code())
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(rawBody);
            }

            return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(rawBody);
        } catch (Exception error) {
            return json(500, Map.of("detail", error.getMessage()));
        }
    }

    private ResponseEntity<String> json(int status, Map<String, String> body) {
        try {
            return ResponseEntity.status(status)
                .contentType(MediaType.APPLICATION_JSON)
                .body(objectMapper.writeValueAsString(body));
        } catch (Exception error) {
            return ResponseEntity.status(status)
                .contentType(MediaType.APPLICATION_JSON)
                .body("{\"detail\":\"Unexpected server error\"}");
        }
    }

    private static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }
}
php
<?php

// /api/sori_ephemeral_key.php
header('Content-Type: application/json');

$appId = getenv('SORI_APP_ID');
$secretKey = getenv('SORI_SECRET_KEY');
$soriEphemeralEndpoint = 'https://console.soriapi.com/api/auth/ephemeral';

if (!$appId || !$secretKey) {
    http_response_code(500);
    echo json_encode(['detail' => 'Missing SORI_APP_ID or SORI_SECRET_KEY']);
    exit;
}

$curl = curl_init($soriEphemeralEndpoint);
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'app_id' => $appId,
        'secret_key' => $secretKey,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/x-www-form-urlencoded',
    ],
]);

$raw = curl_exec($curl);
$statusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);

if ($raw === false) {
    http_response_code(500);
    echo json_encode(['detail' => curl_error($curl)]);
    curl_close($curl);
    exit;
}

curl_close($curl);

if ($statusCode < 200 || $statusCode >= 300) {
    http_response_code($statusCode);
    echo $raw;
    exit;
}

echo $raw;

Notes

  • The browser should call only your short-lived key endpoint, not your long-lived secret management path.
  • If you host multiple SORI apps, your relay can select different app_id values based on the request, tenant, or deployment environment.
  • If you need framework-specific middleware or authentication, keep the browser contract simple and stable.