Skip to main content
This page shows how to turn one-off request snippets into a reusable client layer that your application can share across routes, workers, and sync jobs. Use this approach when:
  • More than one service needs Uppzy access
  • You want one place for auth, timeout, retry, and error handling
  • You want testable helper methods instead of ad hoc fetch or requests calls

Design goals

Your internal client should centralize:
  • Base URL and API key handling
  • Default headers
  • Timeout settings
  • Retry and error classification
  • JSON parsing
  • Common endpoint methods
Keep business logic outside the client. The client should know how to call Uppzy, not when your product should escalate, retry a workflow, or create a support ticket.

Suggested module layout

src/integrations/uppzy/
  client.ts
  errors.ts
  types.ts
  factory.ts
  testing.ts
Use one small owned module rather than copying helper functions into unrelated services.

TypeScript types

Keep only the response fields your application actually uses.
export type UppzyChatResponse = {
  session_id: string;
  request_id: string;
  answer: string;
  confidence_level?: string;
  confidence_score?: number;
};

export type UppzyAsyncResponse = {
  session_id: string;
  request_id: string;
  status: string;
};

export type UppzyAsyncStatusResponse = {
  session_id: string;
  request_id: string;
  status: string;
  answer?: string;
  error?: string;
};

TypeScript client class

This example assumes you already use the error helper pattern from Error Handling Catalog.
import { UppzyApiError, isRetryableStatus } from "./errors";

type RequestOptions = {
  method?: string;
  body?: unknown;
  timeoutMs?: number;
  maxRetries?: number;
};

export class UppzyClient {
  constructor(
    private readonly baseUrl: string,
    private readonly apiKey: string,
    private readonly siteId: string,
  ) {}

  private async request<T>(path: string, {
    method = "GET",
    body,
    timeoutMs = 15000,
    maxRetries = 2,
  }: RequestOptions = {}): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), timeoutMs);

      try {
        const response = await fetch(`${this.baseUrl}${path}`, {
          method,
          signal: controller.signal,
          headers: {
            "X-API-Key": this.apiKey,
            "Content-Type": "application/json",
          },
          body: body ? JSON.stringify(body) : undefined,
        });

        const text = await response.text();
        const data = text ? JSON.parse(text) : null;

        if (response.ok) {
          return data as T;
        }

        const retryable = isRetryableStatus(response.status);
        if (!retryable || attempt === maxRetries) {
          throw new UppzyApiError(`Uppzy API returned HTTP ${response.status}`, {
            status: response.status,
            endpoint: path,
            details: data,
            retryable,
          });
        }

        await this.sleep(this.backoff(attempt));
      } catch (error: any) {
        if (error instanceof UppzyApiError) {
          throw error;
        }

        const isAbort = error?.name === "AbortError";
        if (!isAbort || attempt === maxRetries) {
          throw new UppzyApiError(isAbort ? "Uppzy API request timed out" : "Uppzy API request failed", {
            status: null,
            endpoint: path,
            details: null,
            retryable: isAbort,
          });
        }

        await this.sleep(this.backoff(attempt));
      } finally {
        clearTimeout(timeout);
      }
    }

    throw new Error("Unexpected Uppzy client state");
  }

  private backoff(attempt: number): number {
    const jitter = Math.floor(Math.random() * 250);
    return Math.min(4000, 300 * 2 ** attempt) + jitter;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  sendSyncChat(payload: {
    session_id: string;
    message: string;
    email?: string;
    ui_locale?: string;
    response_language?: string;
  }) {
    return this.request<UppzyChatResponse>(`/m2m/sites/${this.siteId}/chat`, {
      method: "POST",
      body: payload,
    });
  }

  sendAsyncChat(payload: {
    session_id: string;
    message: string;
    response_language?: string;
  }) {
    return this.request<UppzyAsyncResponse>(`/m2m/sites/${this.siteId}/chat/async`, {
      method: "POST",
      body: payload,
    });
  }

  getAsyncChatStatus(requestId: string) {
    return this.request<UppzyAsyncStatusResponse>(`/m2m/sites/${this.siteId}/chat/requests/${requestId}`);
  }

  createTextDocument(payload: {
    title: string;
    content: string;
    category?: string;
  }) {
    return this.request(`/m2m/sites/${this.siteId}/documents/text`, {
      method: "POST",
      body: payload,
    });
  }

  createQaDocument(payload: {
    title: string;
    question: string;
    answer: string;
    category?: string;
  }) {
    return this.request(`/m2m/sites/${this.siteId}/documents/qa`, {
      method: "POST",
      body: payload,
    });
  }

  submitFeedback(payload: {
    session_id: string;
    request_id: string;
    feedback: "good" | "bad";
  }) {
    return this.request(`/m2m/sites/${this.siteId}/chat/feedback`, {
      method: "POST",
      body: payload,
    });
  }

  closeSession(sessionId: string) {
    return this.request(`/m2m/sites/${this.siteId}/chat/session/close`, {
      method: "POST",
      body: { session_id: sessionId },
    });
  }

  getStatistics() {
    return this.request(`/m2m/sites/${this.siteId}/statistics`);
  }

  getSessions() {
    return this.request(`/m2m/sites/${this.siteId}/sessions`);
  }
}

Factory pattern

Keep environment wiring separate from business code.
import { UppzyClient } from "./client";

export function createUppzyClient() {
  const baseUrl = process.env.UPPZY_BASE_URL || "https://api.uppzy.com/api/v1";
  const apiKey = process.env.UPPZY_API_KEY;
  const siteId = process.env.UPPZY_SITE_ID;

  if (!apiKey || !siteId) {
    throw new Error("Missing Uppzy configuration");
  }

  return new UppzyClient(baseUrl, apiKey, siteId);
}
This keeps API keys in runtime configuration and out of route files, queue definitions, and application templates.

Example usage in an API route

import { createUppzyClient } from "./integrations/uppzy/factory";

export async function postAssistantQuestion(req, res) {
  const uppzy = createUppzyClient();
  const { visitorId, message } = req.body;

  const result = await uppzy.sendSyncChat({
    session_id: `visitor_${visitorId}`,
    message,
    response_language: "en",
  });

  return res.json({
    request_id: result.request_id,
    answer: result.answer,
    confidence_level: result.confidence_level,
  });
}

Python client class

For Python services, use a small class around requests.Session.
import os
import time
import random
import requests


class UppzyApiError(RuntimeError):
    def __init__(self, message, status=None, endpoint=None, details=None, retryable=False):
        super().__init__(message)
        self.status = status
        self.endpoint = endpoint
        self.details = details
        self.retryable = retryable


def is_retryable_status(status):
    return status == 429 or (status is not None and 500 <= status <= 599)


class UppzyClient:
    def __init__(self, base_url: str, api_key: str, site_id: str):
        self.base_url = base_url.rstrip("/")
        self.site_id = site_id
        self.session = requests.Session()
        self.session.headers.update({"X-API-Key": api_key})

    def _backoff(self, attempt: int) -> float:
        return min(4.0, 0.3 * (2 ** attempt)) + random.random() * 0.25

    def _request(self, path: str, method: str = "GET", json_body=None, timeout: int = 15, max_retries: int = 2):
        for attempt in range(max_retries + 1):
            try:
                response = self.session.request(
                    method,
                    f"{self.base_url}{path}",
                    json=json_body,
                    timeout=timeout,
                )

                data = response.json() if response.text else None

                if response.ok:
                    return data

                retryable = is_retryable_status(response.status_code)
                if not retryable or attempt == max_retries:
                    raise UppzyApiError(
                        f"Uppzy API returned HTTP {response.status_code}",
                        status=response.status_code,
                        endpoint=path,
                        details=data,
                        retryable=retryable,
                    )

                time.sleep(self._backoff(attempt))
            except requests.Timeout:
                if attempt == max_retries:
                    raise UppzyApiError(
                        "Uppzy API request timed out",
                        status=None,
                        endpoint=path,
                        details=None,
                        retryable=True,
                    )
                time.sleep(self._backoff(attempt))

    def send_sync_chat(self, payload: dict):
        return self._request(f"/m2m/sites/{self.site_id}/chat", method="POST", json_body=payload)

    def get_statistics(self):
        return self._request(f"/m2m/sites/{self.site_id}/statistics")

Testing with a fake transport

Keep your client testable without live network calls. One approach in JavaScript is to inject a transport function.
type Transport = (url: string, init: RequestInit) => Promise<Response>;

export class TestableUppzyClient {
  constructor(
    private readonly baseUrl: string,
    private readonly apiKey: string,
    private readonly siteId: string,
    private readonly transport: Transport = fetch,
  ) {}

  async getStatistics() {
    const response = await this.transport(
      `${this.baseUrl}/m2m/sites/${this.siteId}/statistics`,
      {
        method: "GET",
        headers: { "X-API-Key": this.apiKey },
      },
    );

    return response.json();
  }
}
This lets you test:
  • Correct path construction
  • Header injection
  • Error mapping
  • Retry decisions
  • Timeout behavior
without depending on a live endpoint in every unit test.

Boundaries to keep

Put these inside the client layer:
  • HTTP construction
  • Authentication headers
  • Timeout and retry logic
  • Response parsing
  • Shared endpoint helpers
Keep these outside the client layer:
  • UI messaging
  • CRM handoff rules
  • Queue orchestration
  • Content approval decisions
  • Customer-specific business authorization

Production checklist

  • One owned Uppzy client module per codebase
  • API key and site ID loaded from runtime configuration
  • Shared error type used by routes and workers
  • Timeout and retry defaults defined once
  • Tests cover success, 401, 403, 429, 5xx, and timeout paths
  • Business logic lives above the client layer