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:
- Authenticate users and obtain a JWT
- Invoke agents via the Gateway's invoke endpoint
- Handle credential flows — prompt users to connect missing services
- Handle confirmations — display approval prompts for high-risk operations
- 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 requestsTo 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();Option B: External Identity Provider (recommended for production)
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:
| Method | Format |
|---|---|
| Authorization header | Authorization: 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
| Code | Meaning |
|---|---|
CREDENTIALS_REQUIRED | One or more credentials are missing for this agent |
AGENT_NOT_FOUND | No agent with that ID exists |
AGENT_DISABLED | Agent has been disabled (kill switch) |
TENANT_MISMATCH | Agent belongs to a different tenant |
RUNTIME_ERROR | The 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 invocationFor 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
| Endpoint | Method | Description |
|---|---|---|
/api/v1/auth/login | POST | Login — returns JWT + user |
/api/v1/auth/register | POST | Register a new user |
/api/v1/auth/refresh | POST | Refresh JWT |
/api/v1/agents/:id/invoke | POST | Invoke an agent |
/api/v1/agents/:id/credentials-check | GET | Pre-check credentials |
/api/v1/agents/:id | GET | Get agent info |
/api/v1/credentials | GET | List user credentials |
/api/v1/credentials/jwt | POST | Store JWT / token credential |
/api/v1/credentials/apikey | POST | Store API key credential |
/api/v1/credentials/:id | DELETE | Delete a credential |
/api/v1/credential-providers | GET | List credential providers |
/api/v1/credential-providers/by-service/:type | GET | Look up provider by service type |
/api/v1/stream | GET | SSE confirmation stream |
/api/v1/confirmations/list | GET | List confirmations (poll fallback) |
/api/v1/confirmations/:id/confirm | POST | Confirm a request |
/api/v1/confirmations/:id/reject | POST | Reject a request |
/api/v1/mcp-proxy/:providerId/mcp | POST | MCP proxy (Streamable HTTP) |