SimplaixSimplaix Gateway

App Builder Guide

Build frontend applications that invoke AI agents through Simplaix Gateway.

This guide is for developers building applications — web apps, dashboards, chatbots — that invoke AI agents through the Gateway. Your app authenticates users, invokes agents, handles credential flows, and responds to human-in-the-loop confirmations.

Overview

As an app builder you will:

  1. Authenticate users and obtain a JWT
  2. Invoke agents via the Gateway's invoke endpoint
  3. Handle credential flows — prompt users to connect missing services
  4. Handle confirmations — display approval prompts for high-risk operations
  5. Render responses — JSON or SSE streaming

Step 1: Authenticate the User

Option A: Gateway-issued JWT

const res = await fetch('https://<your-gateway-url>/api/v1/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'user@example.com', password: 'password' }),
});
const { token, user } = await res.json();
// Store token for subsequent requests

To refresh a token:

const res = await fetch('https://<your-gateway-url>/api/v1/auth/refresh', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
});
const { token: newToken } = await res.json();

Configure your IdP in the Gateway's .env and pass your IdP-issued JWT directly:

# Auth0 / OIDC
JWT_EXTERNAL_ISSUERS='[{
  "issuer": "https://your-tenant.auth0.com/",
  "jwksUri": "https://your-tenant.auth0.com/.well-known/jwks.json",
  "audience": "simplaix-gateway"
}]'

# Azure AD
JWT_EXTERNAL_ISSUERS='[{
  "issuer": "https://login.microsoftonline.com/TENANT/v2.0",
  "jwksUri": "https://login.microsoftonline.com/TENANT/discovery/v2.0/keys",
  "audience": "api://simplaix-gateway"
}]'

Authentication methods

The Gateway's flexible auth middleware accepts tokens in three ways:

MethodFormat
Authorization headerAuthorization: Bearer <token>
Query param (JWT)?_token=<token>
Query param (API key)?_api_key=gk_xxx

Query params are useful for agent runtimes that cannot set custom headers.


Step 2: Invoke an Agent

const res = await fetch(`https://<your-gateway-url>/api/v1/agents/${agentId}/invoke`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    messages: [{ role: 'user', content: 'Show me my recent transactions' }],
  }),
});

The request body is forwarded as-is to the agent runtime — structure it however your agent expects.

Handle the response

Agents can respond with JSON or an SSE stream:

if (res.status === 401) {
  const error = await res.json();
  if (error.code === 'CREDENTIALS_REQUIRED') {
    // Prompt user to connect missing services (see Credential Flow below)
    showAuthPrompt(error.missing, error.authUrls);
    return;
  }
}

const contentType = res.headers.get('Content-Type') ?? '';

if (contentType.includes('text/event-stream')) {
  await handleSSEStream(res);
} else {
  const { data } = await res.json();
  console.log(data);
}

Error codes

CodeMeaning
CREDENTIALS_REQUIREDOne or more credentials are missing for this agent
AGENT_NOT_FOUNDNo agent with that ID exists
AGENT_DISABLEDAgent has been disabled (kill switch)
TENANT_MISMATCHAgent belongs to a different tenant
RUNTIME_ERRORThe upstream agent runtime returned an error

Pre-check credentials (optional)

Before invoking, you can check whether all credentials are in place:

const res = await fetch(
  `https://<your-gateway-url>/api/v1/agents/${agentId}/credentials-check`,
  { headers: { Authorization: `Bearer ${token}` } },
);

if (res.status === 401) {
  const { missing, authUrls } = await res.json();
  // Show "Connect" buttons before proceeding
}

Step 3: Handle SSE Streaming

async function handleSSEStream(response: Response) {
  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() ?? '';

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const data = line.slice(6);
      if (data === '[DONE]') return;
      try {
        const event = JSON.parse(data);
        handleAgentEvent(event);
      } catch {
        console.log('Agent:', data);
      }
    }
  }
}

Step 4: Credential Flow

When credentials are missing, the Gateway returns a 401 with this shape:

{
  "code": "CREDENTIALS_REQUIRED",
  "missing": ["github", "slack"],
  "authUrls": {
    "github": "https://gateway.example.com/auth/connect?service=github",
    "slack": "https://gateway.example.com/auth/connect?service=slack"
  },
  "message": "Authentication required for: github, slack"
}

Direct the user to the authUrls for each missing service (OAuth redirect or popup). Once they complete the flow, retry the agent invocation.

Storing a manual credential (API token or JWT)

For services that use API keys rather than OAuth:

// 1. Look up the credential provider
const providerRes = await fetch(
  `https://<your-gateway-url>/api/v1/credential-providers/by-service/${serviceType}`,
  { headers: { Authorization: `Bearer ${token}` } },
);
const { provider } = await providerRes.json();

// 2. Store the credential
await fetch('https://<your-gateway-url>/api/v1/credentials/jwt', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ providerId: provider.id, token: apiToken }),
});

// 3. Retry agent invocation

For API key credentials:

await fetch('https://<your-gateway-url>/api/v1/credentials/apikey', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ providerId: provider.id, apiKey: keyValue }),
});

List and delete credentials

// List
const { credentials } = await fetch('https://<your-gateway-url>/api/v1/credentials', {
  headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());

// Delete
await fetch(`https://<your-gateway-url>/api/v1/credentials/${credentialId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${token}` },
});

Step 5: Human-in-the-Loop Confirmations

When an agent triggers a tool that requires confirmation, the Gateway pauses execution and emits an SSE event. Your app listens on the confirmation stream and responds.

Listen for confirmations

// EventSource doesn't support custom headers — use _token query param
const stream = new EventSource(
  `https://<your-gateway-url>/api/v1/stream?_token=${token}`
);

stream.addEventListener('CONFIRMATION_REQUIRED', (event) => {
  const req = JSON.parse(event.data);
  // {
  //   id: "conf_123",
  //   tool: { name: "delete_file", description: "...", provider: { id, name } },
  //   arguments: { path: "/report.pdf" },
  //   risk: { level: "high" },
  //   agent: { id: "agent_1", name: "File Manager" },
  //   timestamp: "2025-01-15T10:30:00Z"
  // }
  showConfirmationDialog(req);
});

stream.addEventListener('CONFIRMATION_RESOLVED', (event) => {
  const { id, confirmed } = JSON.parse(event.data);
  dismissConfirmationDialog(id);
});

// 30-second heartbeat keeps the connection alive
stream.addEventListener('heartbeat', () => {});

Confirm or reject

async function respond(confirmationId: string, approved: boolean, reason?: string) {
  const action = approved ? 'confirm' : 'reject';
  await fetch(`https://<your-gateway-url>/api/v1/confirmations/${confirmationId}/${action}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ reason }),
  });
}

Poll for pending confirmations (non-SSE fallback)

const { confirmations } = await fetch(
  'https://<your-gateway-url>/api/v1/confirmations/list?status=pending',
  { headers: { Authorization: `Bearer ${token}` } },
).then((r) => r.json());

CopilotKit Integration

The Gateway works natively with CopilotKit. Point the CopilotKit runtime at the Gateway's invoke endpoint:

// app/api/copilotkit/route.ts
import { CopilotRuntime, ExperimentalEmptyAdapter, copilotRuntimeNextJSAppRouterEndpoint } from '@copilotkit/runtime';

const runtime = new CopilotRuntime({
  remoteEndpoints: [
    {
      // Use _token query param since CopilotKit's HttpAgent cannot set headers
      url: `${process.env.GATEWAY_API_URL}/api/v1/agents/${agentId}/invoke?_token=${userToken}`,
    },
  ],
});

export const POST = copilotRuntimeNextJSAppRouterEndpoint({
  runtime,
  serviceAdapter: new ExperimentalEmptyAdapter(),
  endpoint: '/api/copilotkit',
}).POST;
// In your React app
import { CopilotKit } from '@copilotkit/react-core';
import { CopilotChat } from '@copilotkit/react-ui';

export function Chat() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <CopilotChat labels={{ title: 'Assistant' }} />
    </CopilotKit>
  );
}

MCP Proxy

To call MCP servers directly from your app, route through the Gateway's MCP proxy — it handles auth, policy enforcement, and audit logging transparently:

const res = await fetch(
  `https://<your-gateway-url>/api/v1/mcp-proxy/${providerId}/mcp`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      Accept: 'application/json, text/event-stream',
    },
    body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }),
  },
);

API Reference

EndpointMethodDescription
/api/v1/auth/loginPOSTLogin — returns JWT + user
/api/v1/auth/registerPOSTRegister a new user
/api/v1/auth/refreshPOSTRefresh JWT
/api/v1/agents/:id/invokePOSTInvoke an agent
/api/v1/agents/:id/credentials-checkGETPre-check credentials
/api/v1/agents/:idGETGet agent info
/api/v1/credentialsGETList user credentials
/api/v1/credentials/jwtPOSTStore JWT / token credential
/api/v1/credentials/apikeyPOSTStore API key credential
/api/v1/credentials/:idDELETEDelete a credential
/api/v1/credential-providersGETList credential providers
/api/v1/credential-providers/by-service/:typeGETLook up provider by service type
/api/v1/streamGETSSE confirmation stream
/api/v1/confirmations/listGETList confirmations (poll fallback)
/api/v1/confirmations/:id/confirmPOSTConfirm a request
/api/v1/confirmations/:id/rejectPOSTReject a request
/api/v1/mcp-proxy/:providerId/mcpPOSTMCP proxy (Streamable HTTP)

On this page