# Struere Documentation — API Reference > Filtered section from the Struere docs. Full docs: https://docs.struere.dev/llms.txt ## API Quick Start To send a message to a Struere agent, use the Chat API: POST https://.convex.site/v1/agents//chat Authorization: Bearer Content-Type: application/json {"message": "Hello, what can you help me with?"} Response: {"threadId": "...", "message": "...", "usage": {"inputTokens": ..., "outputTokens": ..., "totalTokens": ...}} - Use `/v1/agents/:slug/chat` (preferred) or `/v1/chat` with an `agentId` field - API keys: created in the dashboard under Settings > API Keys - Development keys: `sk_dev_` prefix. Production keys: `sk_prod_` prefix - Pass `threadId` from a previous response to continue a conversation - Pass `externalThreadId` to map external identifiers (e.g., `"slack:U12345678"`, `"whatsapp:+1234567890"`) - Full API docs: https://docs.struere.dev/llms-api.txt - OpenAPI spec: https://docs.struere.dev/openapi.yaml --- ## API Overview > HTTP endpoints for interacting with Struere Source: https://docs.struere.dev/api/overview.md Struere exposes 30 HTTP endpoints through its Convex backend, including the Chat API (POST /v1/agents/:slug/chat), a full Data API for entity CRUD, WhatsApp template management, CLI sync, and 6 webhook receivers for Clerk, Kapso, Flow, Polar, Resend, and studio relay. ## Base URL Your API base URL is shown in the dashboard under **Settings > API Keys**. ``` https://api.struere.dev ``` ## Authentication Chat endpoints authenticate via **Bearer token** using API keys. API keys are created in the Struere dashboard under **API Keys** and are scoped to a specific environment (`development` or `production`). ``` Authorization: Bearer sk_dev_abc123... ``` The environment of the API key determines which environment the request operates in. A development API key accesses development agents, entities, and configurations. A production API key accesses production data. There is no way to cross environments with a single key. API keys are validated by computing a SHA-256 hash and looking up the hashed value in the database. ### Permissions Each API key has a set of permissions that control which endpoints it can access. | Permission | Access | |------------|--------| | `*` | Full access to all endpoints (chat, data, sync) | | `chat` | Chat endpoints (`/v1/chat`, `/v1/agents/:slug/chat`) | | `data` | Data API endpoints (`/v1/data/*`, `/v1/entity-types`) | | `sync` | CLI sync endpoints (`/v1/sync/*`) | Keys created in the dashboard default to `*` (full access). When creating keys programmatically or through the agent settings page, you can restrict permissions to specific scopes. ## Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/health` | None | Health check | | `POST` | `/v1/chat` | Bearer token | Chat with an agent by ID or router by slug | | `POST` | `/v1/agents/:slug/chat` | Bearer token | Chat with an agent by slug | | `POST` | `/v1/routers/:slug/chat` | Bearer token | Chat via a router by slug | | `POST` | `/v1/compile-prompt` | Bearer token | Compile an agent's system prompt | | `POST` | `/v1/run-tool` | Bearer token | Run a tool as it would in a real conversation (agentSlug optional) | | `POST` | `/v1/fire-trigger` | Bearer token | Manually fire a trigger for testing | | `POST` | `/v1/sync/state` | Bearer token (sync) | Get current sync state for an environment | | `POST` | `/v1/sync/pull` | Bearer token (sync) | Pull remote resource state for local sync | | `GET` | `/v1/entity-types` | Bearer token (data) | List entity types | | `GET` | `/v1/data/:type` | Bearer token (data) | List entities | | `GET` | `/v1/data/:type/:id` | Bearer token (data) | Get entity by ID | | `POST` | `/v1/data/:type` | Bearer token (data) | Create entity | | `POST` | `/v1/data/:type/query` | Bearer token (data) | Query entities with filters | | `POST` | `/v1/data/:type/search` | Bearer token (data) | Full-text search entities | | `PATCH` | `/v1/data/:type/:id` | Bearer token (data) | Update entity | | `DELETE` | `/v1/data/:type/:id` | Bearer token (data) | Delete entity | | `POST` | `/webhook/clerk` | None | Clerk user/organization sync webhook | | `POST` | `/webhook/kapso/project` | HMAC signature | WhatsApp phone number connection events | | `POST` | `/webhook/kapso/messages` | HMAC signature | WhatsApp inbound messages and status updates | | `POST` | `/webhook/flow` | None | Flow payment status updates | | `POST` | `/webhook/polar` | HMAC signature | Polar payment/billing events | ## GET /health Returns the current server status and timestamp. **Request:** ```bash curl https://api.struere.dev/health ``` **Response:** ```json { "status": "ok", "timestamp": 1710500000000 } ``` ## POST /v1/chat Send a message to an agent identified by its Convex document ID, or route through a router by providing `routerSlug` instead. See the [Chat API](./chat) documentation for full details. ## POST /v1/agents/:slug/chat Send a message to an agent identified by its slug. This is the preferred endpoint for external integrations as slugs are human-readable and stable across deployments. See the [Chat API](./chat) documentation for full details. ## POST /v1/routers/:slug/chat Send a message through a router identified by its slug. The router evaluates routing rules or classifies the message to determine which agent handles the conversation. See the [Chat API](./chat) documentation for full details. ## POST /v1/compile-prompt Compile an agent's system prompt with template variables resolved. Useful for debugging and testing system prompt templates without sending a real message. **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `slug` | `string` | Yes | The agent slug | | `message` | `string` | No | Sample message for context | | `channel` | `string` | No | Sample channel (`api`, `whatsapp`, `widget`, `dashboard`) | | `threadMetadata` | `object` | No | Sample thread metadata for template resolution | **Response:** The compiled system prompt text and resolved template variables. ## POST /v1/run-tool Run a tool exactly as it would execute during a real agent conversation. Useful for testing tool behavior without triggering a full agent execution loop. **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `agentSlug` | `string` | No | The agent slug to run the tool as. If omitted, runs as system actor | | `toolName` | `string` | Yes | The tool to execute (e.g., `entity.query`, `whatsapp.send`) | | `args` | `object` | No | Arguments to pass to the tool | **Response:** The tool execution result, or an error with `errorType` (`not_found`, `tool_not_found`, `permission_denied`, `execution_error`). ## POST /v1/fire-trigger Manually fire a trigger by slug. The trigger executes its actions as it would in a real activation, using the system actor. Useful for testing automations without waiting for a real data mutation or cron schedule. **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `triggerSlug` | `string` | Yes | The trigger slug to fire | | `entityId` | `string` | No | Entity ID to use as trigger context | | `data` | `object` | No | Data payload to pass as trigger context (used in template variable resolution) | **Response:** ```json { "success": true, "triggerSlug": "notify-on-session", "result": { "steps": [ { "tool": "entity.get", "status": "success", "durationMs": 45 }, { "tool": "event.emit", "status": "success", "durationMs": 12 } ], "durationMs": 57 } } ``` **Error Response:** Returns an error with `errorType` (`not_found`, `permission_denied`, `execution_error`). ## Data API CRUD and query operations for entities in your data layer. Requires an API key with the `data` permission. See the [Data API](./data) documentation for full details. ## Webhook Endpoints Webhook endpoints receive events from external services. See the [Webhooks](./webhooks) documentation for details on each webhook. ## Error Responses All endpoints return JSON error responses with appropriate HTTP status codes: **401 Unauthorized** — Missing or invalid API key: ```json { "error": "Unauthorized" } ``` **400 Bad Request** — Missing required fields: ```json { "error": "agentId and message are required" } ``` **500 Internal Server Error** — Server-side execution failure: ```json { "error": "Error description" } ``` ## Rate Limiting Rate limits are enforced at the Convex platform level. Refer to your Convex plan for specific limits on function calls and bandwidth. ## CORS The Chat API and Data API endpoints set permissive CORS headers (`Access-Control-Allow-Origin: *`), so they can be called directly from browser-based applications without a proxy. ## JavaScript Client The `struere` npm package ships a typed JS client under the `struere/client` subpath. It works in browsers, Node 18+, Bun, and Deno with zero runtime dependencies. ```ts import { StruereClient, StruereApiError } from 'struere/client' const struere = new StruereClient({ apiKey: process.env.STRUERE_API_KEY!, baseUrl: 'https://api.struere.dev', }) const reply = await struere.chat({ agentSlug: 'coach', message: 'Hi!', }) const players = await struere.data.list<{ name: string }>('player', { limit: 50 }) const created = await struere.data.create('player', { name: 'Mia' }) const matches = await struere.data.query('player', { filters: { name: { $contains: 'Mia' } } }) try { await struere.data.get('player', 'missing') } catch (err) { if (err instanceof StruereApiError) { console.error(err.status, err.message, err.requestId) } } ``` The client throws `StruereApiError` on non-2xx responses with `{ status, message, code?, requestId?, details? }`. Pass a custom `fetch` implementation via the `fetch` option for environments that lack a global. --- ## Chat API > Send messages to agents via HTTP Source: https://docs.struere.dev/api/chat.md The Chat API allows you to send messages to agents and receive responses over HTTP. There are three endpoint patterns: by agent ID, by agent slug, and by router slug. ## Endpoints | Method | Path | Description | |--------|------|-------------| | `POST` | `/v1/chat` | Chat by agent ID or router slug | | `POST` | `/v1/agents/:slug/chat` | Chat by agent slug | | `POST` | `/v1/routers/:slug/chat` | Chat via a router by slug | All endpoints require a Bearer token (API key) for authentication. The API key determines which **environment** (development or production) the request operates in. ## POST /v1/chat Send a message to an agent by its Convex document ID. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "agentId": "abc123def456", "message": "Hello, can you help me schedule a session?", "threadId": "thread_xyz789", "externalThreadId": "my-app:user-123", "threadContext": { "params": { "customerId": "cust_abc123", "plan": "pro" } } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `agentId` | `string` | Conditional | The Convex document ID of the agent. Required if `routerSlug` is not provided | | `routerSlug` | `string` | Conditional | The router slug to route through. Required if `agentId` is not provided | | `message` | `string` | Yes | The user's message to the agent | | `threadId` | `string` | No | An existing thread ID to continue a conversation | | `externalThreadId` | `string` | No | An external identifier for thread reuse (e.g., `"whatsapp:+1234567890"`) | | `phoneNumber` | `string` | No | Phone number for router-based routing (used with `routerSlug`) | | `threadContext` | `object` | No | Custom context for this thread (see [Thread Context](#thread-context)) | | `threadContext.params` | `object` | No | Key-value pairs accessible to the agent via `{{threadContext.params.X}}` templates | ### Response ```json { "threadId": "thread_xyz789", "message": "I'd be happy to help you schedule a session. What subject and time works best?", "usage": { "inputTokens": 1250, "outputTokens": 45, "totalTokens": 1295 } } ``` | Field | Type | Description | |-------|------|-------------| | `threadId` | `string` | The thread ID for this conversation | | `message` | `string` | The agent's response text | | `usage` | `object` | Token usage for this interaction | | `usage.inputTokens` | `number` | Number of input tokens consumed | | `usage.outputTokens` | `number` | Number of output tokens generated | | `usage.totalTokens` | `number` | Total tokens used | ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/chat \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "agentId": "jd72k3m4n5p6q7r8", "message": "What sessions are scheduled for tomorrow?" }' ``` #### Python ```python import requests response = requests.post( "https://api.struere.dev/v1/chat", headers={ "Authorization": "Bearer sk_dev_abc123", "Content-Type": "application/json", }, json={ "agentId": "jd72k3m4n5p6q7r8", "message": "What sessions are scheduled for tomorrow?", }, ) data = response.json() print(data["message"]) print(f"Thread: {data['threadId']}") ``` ## POST /v1/agents/:slug/chat Send a message to an agent by its slug. This is the preferred endpoint for integrations as slugs are stable and human-readable. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "message": "Hello, can you help me schedule a session?", "threadId": "thread_xyz789", "externalThreadId": "my-app:user-123", "threadContext": { "params": { "email": "jane@example.com" } } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `message` | `string` | Yes | The user's message to the agent | | `threadId` | `string` | No | An existing thread ID to continue a conversation | | `externalThreadId` | `string` | No | An external identifier for thread reuse | | `threadContext` | `object` | No | Custom context for this thread (see [Thread Context](#thread-context)) | Note that `agentId` is **not** needed since the agent is identified by the `:slug` URL parameter. ### Response The response format is identical to the `/v1/chat` endpoint. ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/agents/scheduler/chat \ -H "Authorization: Bearer sk_prod_xyz789" \ -H "Content-Type: application/json" \ -d '{ "message": "Book a math session with Mr. Smith for Tuesday at 3 PM" }' ``` #### Python ```python import requests response = requests.post( "https://api.struere.dev/v1/agents/scheduler/chat", headers={ "Authorization": "Bearer sk_prod_xyz789", "Content-Type": "application/json", }, json={ "message": "Book a math session with Mr. Smith for Tuesday at 3 PM", }, ) data = response.json() print(data["message"]) ``` ## POST /v1/routers/:slug/chat Send a message through a router by its slug. The router evaluates the conversation and determines which agent should handle it based on rules or LLM classification. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `message` | `string` | Yes | The user's message | | `threadId` | `string` | No | An existing thread ID to continue a conversation | | `externalThreadId` | `string` | No | An external identifier for thread reuse | | `phoneNumber` | `string` | No | Phone number for entity-based routing rules | | `threadContext` | `object` | No | Custom context for this thread | ### Response The response format is identical to the `/v1/chat` endpoint, with two additional optional fields: | Field | Type | Description | |-------|------|-------------| | `routedToAgent` | `string` | The Convex document ID of the agent the router selected | | `routedToAgentSlug` | `string` | The slug of the agent the router selected | ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/routers/support-router/chat \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "message": "I need help with billing", "phoneNumber": "+1234567890" }' ``` ## Thread Management ### Dashboard Access Control When using the dashboard (not the API), conversation access is controlled by the user's role. Admins see all threads. Members can only see threads belonging to agents listed in their role's `agentAccess` field. Members can reply to visible threads but cannot start new conversations. See [Permissions](/platform/permissions#agent-access-conversation-filtering) for details. ### Creating New Threads If neither `threadId` nor `externalThreadId` is provided, a new thread is created automatically. The response includes the `threadId` which you should store for subsequent messages. ### Continuing Conversations Pass the `threadId` from a previous response to continue the conversation. The agent receives the full message history from the thread, maintaining context across messages. ```bash curl -X POST https://api.struere.dev/v1/agents/support/chat \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "message": "Actually, make that Thursday instead", "threadId": "jd72k3m4n5p6q7r8" }' ``` **Python:** ```python import requests API_URL = "https://api.struere.dev/v1/agents/support/chat" HEADERS = { "Authorization": "Bearer sk_dev_abc123", "Content-Type": "application/json", } first = requests.post(API_URL, headers=HEADERS, json={"message": "Schedule a session for Tuesday"}).json() thread_id = first["threadId"] followup = requests.post(API_URL, headers=HEADERS, json={ "message": "Actually, make that Thursday instead", "threadId": thread_id, }).json() print(followup["message"]) ``` ### External Thread IDs The `externalThreadId` field allows you to map external identifiers to Struere threads. If a thread with the given `externalThreadId` already exists, it is reused. Otherwise, a new thread is created. This is useful for integrations where you want to maintain a single conversation thread per external user or channel: ```json { "message": "What is my account balance?", "externalThreadId": "slack:U12345678" } ``` Common patterns: - `whatsapp:+1234567890` for WhatsApp conversations - `slack:U12345678` for Slack user threads - `app:user-abc123` for your application's user IDs ## Environment Scoping The API key determines the environment for the entire request: - **Development API keys** (`sk_dev_...`) access development agent configurations, development entities, and development threads - **Production API keys** (`sk_prod_...`) access production agent configurations, production entities, and production threads There is no way to specify the environment in the request body. It is always derived from the API key. ## Execution Flow When a chat request arrives: 1. The API key is validated and the environment is extracted 2. The agent and its configuration are loaded for the matching environment 3. A thread is retrieved or created 4. The system prompt is processed (template variables and function calls resolved) 5. The LLM is called in a loop (up to 10 iterations) to handle tool calls 6. Each tool call is permission-checked against the actor context 7. The final response, thread ID, and usage stats are returned ## Response Mode The Chat API returns a **single JSON response** after the agent finishes processing. There is no streaming or Server-Sent Events (SSE). The agent executes its full tool-call loop (up to 10 iterations) server-side, then returns the final message. For real-time updates during agent execution, use Convex React subscriptions in your frontend to watch the thread's messages table. The dashboard uses this pattern to show tool calls and intermediate results as they happen. ## Thread Context Thread context lets you pass structured data about the caller to your agent. The `threadContext.params` object is stored on the thread and made available to the agent's system prompt via `{{threadContext.params.X}}` template variables. ```bash curl -X POST https://api.struere.dev/v1/agents/support/chat \ -H "Authorization: Bearer sk_prod_xyz789" \ -H "Content-Type: application/json" \ -d '{ "message": "I need help with my account", "threadContext": { "params": { "customerId": "cust_abc123", "email": "jane@example.com", "plan": "pro" } } }' ``` The agent can then reference these values in its system prompt: ``` Customer: {{threadContext.params.email}} Plan: {{threadContext.params.plan}} ## Customer Profile {{entity.get({"type": "customer", "id": "{{threadContext.params.customerId}}"})}} ``` The channel is automatically set to `api` for API requests, accessible via `{{threadContext.channel}}`. If the agent defines `threadContextParams` in its config, the backend validates incoming params against the schema — checking required fields, validating types, and dropping unknown params. For the full template variable reference, see [System Prompt Templates](../tools/system-prompt-templates). ## Execution Metadata (optional) Agent chat responses may include `_executionMeta` with execution telemetry: ```json { "threadId": "...", "message": "...", "usage": { "inputTokens": 1250, "outputTokens": 45, "totalTokens": 1295 }, "_executionMeta": { "iterationCount": 3, "model": "anthropic/claude-sonnet-4", "durationMs": 8234, "toolCallSummary": [ { "name": "entity.query", "durationMs": 120, "status": "success" }, { "name": "entity.update", "durationMs": 80, "status": "error", "errorType": "validation", "errorMessage": "Missing required field" }, { "name": "entity.update", "durationMs": 90, "status": "success" } ], "errorCount": 1, "permissionDenialCount": 0 } } ``` This metadata is useful for debugging agent behavior, especially when agents self-correct after tool call errors. ## Error Handling ### Error Response Format All errors return a JSON object with an `error` field: ```json { "error": "Error description" } ``` ### Status Codes | Status | Error | Cause | |--------|-------|-------| | `401` | `"Unauthorized"` | Missing or invalid API key | | `400` | `"agentId or routerSlug is required"` | Missing required fields on `/v1/chat` | | `400` | `"message is required"` | Missing message on `/v1/agents/:slug/chat` | | `500` | `"Agent not found"` | Agent ID or slug does not exist | | `500` | `"No active config found for agent \"slug\" in environment"` | Agent exists but has no config in the API key's environment | ### Handling Errors in Code **TypeScript:** ```typescript const response = await fetch("https://api.struere.dev/v1/agents/scheduler/chat", { method: "POST", headers: { "Authorization": "Bearer sk_prod_xyz789", "Content-Type": "application/json", }, body: JSON.stringify({ message: "Book a session" }), }) if (!response.ok) { const error = await response.json() switch (response.status) { case 401: throw new Error("Invalid API key") case 400: throw new Error(`Bad request: ${error.error}`) default: throw new Error(`Server error: ${error.error}`) } } const data = await response.json() console.log(data.message) ``` **Python:** ```python import requests response = requests.post( "https://api.struere.dev/v1/agents/scheduler/chat", headers={ "Authorization": "Bearer sk_prod_xyz789", "Content-Type": "application/json", }, json={"message": "Book a session"}, ) if response.status_code == 401: raise Exception("Invalid API key") elif response.status_code == 400: raise Exception(f"Bad request: {response.json()['error']}") elif response.status_code >= 500: raise Exception(f"Server error: {response.json()['error']}") data = response.json() print(data["message"]) ``` --- ## Data API > CRUD operations for entities via HTTP Source: https://docs.struere.dev/api/data.md The Data API lets you create, read, update, and delete entities in your Struere data layer over HTTP. Use it to integrate your entity database with any application, backend service, or automation tool. ## Authentication Data API endpoints require an API key with the `data` or `*` (wildcard) permission. Keys created in the dashboard default to `*` which grants access to all endpoints including the Data API. ``` Authorization: Bearer sk_dev_abc123... ``` The API key determines the environment (`development` or `production`). All operations are scoped to that environment. ## Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/v1/entity-types` | List entity types | | `GET` | `/v1/data/:type` | List entities | | `GET` | `/v1/data/:type/:id` | Get entity by ID | | `POST` | `/v1/data/:type` | Create entity | | `POST` | `/v1/data/:type/query` | Query with filters | | `POST` | `/v1/data/:type/search` | Full-text search | | `PATCH` | `/v1/data/:type/:id` | Update entity | | `DELETE` | `/v1/data/:type/:id` | Delete entity | ## Rate Limits Data API endpoints are rate-limited separately from Chat: | Scope | Limit | |-------|-------| | Per API key | 60 requests/minute | | Per organization | 200 requests/minute | ## Response Shape All entity responses share the same shape: ```json { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane", "email": "jane@co.com" }, "createdAt": 1710000000000, "updatedAt": 1710000000000 } ``` | Field | Type | Description | |-------|------|-------------| | `id` | `string` | The entity's Convex document ID | | `type` | `string` | The entity type slug | | `status` | `string` | Entity status (e.g., `active`, `archived`) | | `data` | `object` | The entity's data fields | | `createdAt` | `number` | Creation timestamp (Unix ms) | | `updatedAt` | `number` | Last update timestamp (Unix ms) | List and query endpoints return a paginated wrapper: ```json { "data": [ ... ], "cursor": "k17abc...", "hasMore": true } ``` ## GET /v1/entity-types List all entity types in the current environment. ### Response ```json { "data": [ { "slug": "customer", "name": "Customer", "schema": { "name": "string", "email": "string" }, "searchFields": ["name", "email"] } ] } ``` ### Examples #### curl ```bash curl https://api.struere.dev/v1/entity-types \ -H "Authorization: Bearer sk_dev_abc123" ``` #### Python ```python import requests response = requests.get( "https://api.struere.dev/v1/entity-types", headers={"Authorization": "Bearer sk_dev_abc123"}, ) types = response.json()["data"] for t in types: print(f"{t['slug']}: {t['name']}") ``` ## GET /v1/data/:type List entities of a given type with cursor-based pagination. ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | `number` | `50` | Max results per page (1–100) | | `cursor` | `string` | — | Cursor from a previous response for the next page | | `status` | `string` | — | Filter by status (e.g., `active`, `archived`) | ### Response ```json { "data": [ { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane", "email": "jane@co.com" }, "createdAt": 1710000000000, "updatedAt": 1710000000000 } ], "cursor": "k17xyz...", "hasMore": true } ``` ### Examples #### curl ```bash curl "https://api.struere.dev/v1/data/customer?limit=10" \ -H "Authorization: Bearer sk_dev_abc123" ``` #### Pagination ```bash curl "https://api.struere.dev/v1/data/customer?limit=10&cursor=k17xyz..." \ -H "Authorization: Bearer sk_dev_abc123" ``` #### Python ```python import requests BASE = "https://api.struere.dev" HEADERS = {"Authorization": "Bearer sk_dev_abc123"} all_customers = [] cursor = None while True: params = {"limit": 50} if cursor: params["cursor"] = cursor response = requests.get(f"{BASE}/v1/data/customer", headers=HEADERS, params=params) result = response.json() all_customers.extend(result["data"]) if not result["hasMore"]: break cursor = result["cursor"] print(f"Total: {len(all_customers)}") ``` ## GET /v1/data/:type/:id Get a single entity by its ID. ### Response ```json { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane", "email": "jane@co.com" }, "createdAt": 1710000000000, "updatedAt": 1710000000000 } ``` ### Examples #### curl ```bash curl https://api.struere.dev/v1/data/customer/k17abc... \ -H "Authorization: Bearer sk_dev_abc123" ``` #### TypeScript ```typescript const response = await fetch( "https://api.struere.dev/v1/data/customer/k17abc...", { headers: { Authorization: "Bearer sk_dev_abc123" } } ) const customer = await response.json() console.log(customer.data.name) ``` ## POST /v1/data/:type Create a new entity. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "data": { "name": "Jane Doe", "email": "jane@example.com", "plan": "pro" }, "status": "active" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | `object` | Yes | The entity's data fields | | `status` | `string` | No | Initial status (defaults to `active`) | ### Response (201 Created) ```json { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane Doe", "email": "jane@example.com", "plan": "pro" }, "createdAt": 1710000000000, "updatedAt": 1710000000000 } ``` ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/data/customer \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "data": { "name": "Jane Doe", "email": "jane@example.com", "plan": "pro" } }' ``` #### Python ```python import requests response = requests.post( "https://api.struere.dev/v1/data/customer", headers={ "Authorization": "Bearer sk_dev_abc123", "Content-Type": "application/json", }, json={ "data": { "name": "Jane Doe", "email": "jane@example.com", "plan": "pro", } }, ) customer = response.json() print(f"Created: {customer['id']}") ``` #### TypeScript ```typescript const response = await fetch("https://api.struere.dev/v1/data/customer", { method: "POST", headers: { Authorization: "Bearer sk_dev_abc123", "Content-Type": "application/json", }, body: JSON.stringify({ data: { name: "Jane Doe", email: "jane@example.com", plan: "pro" }, }), }) const customer = await response.json() console.log(`Created: ${customer.id}`) ``` ## POST /v1/data/:type/query Query entities with filters. Filters support comparison operators for advanced queries. ### Request **Body:** ```json { "filters": { "plan": "pro", "age": { "$gte": 18 }, "status": { "$in": ["active", "trial"] } }, "status": "active", "limit": 25, "cursor": "k17xyz..." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `filters` | `object` | No | Field-level filters (see operators below) | | `status` | `string` | No | Filter by entity status | | `limit` | `number` | No | Max results (default 50, max 100) | | `cursor` | `string` | No | Pagination cursor from previous response | ### Filter Operators | Operator | Description | Example | |----------|-------------|---------| | (none) | Exact match | `{ "plan": "pro" }` | | `$in` | Value in array | `{ "plan": { "$in": ["pro", "enterprise"] } }` | | `$nin` | Value not in array | `{ "plan": { "$nin": ["free"] } }` | | `$ne` | Not equal | `{ "status": { "$ne": "archived" } }` | | `$gt` | Greater than | `{ "age": { "$gt": 18 } }` | | `$gte` | Greater than or equal | `{ "age": { "$gte": 18 } }` | | `$lt` | Less than | `{ "score": { "$lt": 50 } }` | | `$lte` | Less than or equal | `{ "score": { "$lte": 100 } }` | ### Response ```json { "data": [ ... ], "cursor": "k17xyz...", "hasMore": false } ``` ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/data/customer/query \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "filters": { "plan": "pro", "age": { "$gte": 18 } }, "limit": 10 }' ``` #### Python ```python import requests response = requests.post( "https://api.struere.dev/v1/data/customer/query", headers={ "Authorization": "Bearer sk_dev_abc123", "Content-Type": "application/json", }, json={ "filters": {"plan": "pro", "age": {"$gte": 18}}, "limit": 10, }, ) result = response.json() for customer in result["data"]: print(customer["data"]["name"]) ``` ## POST /v1/data/:type/search Full-text search across an entity type's search fields. Search fields are defined in the entity type's configuration. ### Request **Body:** ```json { "query": "jane", "limit": 10 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `query` | `string` | Yes | Search text | | `limit` | `number` | No | Max results (default 20, max 100) | ### Response Search returns results without pagination. The response contains only the `data` array: ```json { "data": [ { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane Doe", "email": "jane@example.com" }, "createdAt": 1710000000000, "updatedAt": 1710000000000 } ] } ``` ### Examples #### curl ```bash curl -X POST https://api.struere.dev/v1/data/customer/search \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "query": "jane", "limit": 10 }' ``` #### TypeScript ```typescript const response = await fetch( "https://api.struere.dev/v1/data/customer/search", { method: "POST", headers: { Authorization: "Bearer sk_dev_abc123", "Content-Type": "application/json", }, body: JSON.stringify({ query: "jane", limit: 10 }), } ) const { data } = await response.json() data.forEach((customer: any) => console.log(customer.data.name)) ``` ## PATCH /v1/data/:type/:id Update an entity. Data fields are shallow-merged with the existing data. ### Request **Headers:** | Header | Value | |--------|-------| | `Authorization` | `Bearer YOUR_API_KEY` | | `Content-Type` | `application/json` | **Body:** ```json { "data": { "plan": "enterprise", "notes": "Upgraded from pro" }, "status": "active" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | `object` | Yes | Fields to update (shallow merge) | | `status` | `string` | No | New status value | ### Response Returns the full updated entity. ```json { "id": "k17abc...", "type": "customer", "status": "active", "data": { "name": "Jane Doe", "email": "jane@example.com", "plan": "enterprise", "notes": "Upgraded from pro" }, "createdAt": 1710000000000, "updatedAt": 1710000050000 } ``` ### Examples #### curl ```bash curl -X PATCH https://api.struere.dev/v1/data/customer/k17abc... \ -H "Authorization: Bearer sk_dev_abc123" \ -H "Content-Type: application/json" \ -d '{ "data": { "plan": "enterprise" } }' ``` #### Python ```python import requests response = requests.patch( "https://api.struere.dev/v1/data/customer/k17abc...", headers={ "Authorization": "Bearer sk_dev_abc123", "Content-Type": "application/json", }, json={"data": {"plan": "enterprise"}}, ) updated = response.json() print(f"Updated plan: {updated['data']['plan']}") ``` ## DELETE /v1/data/:type/:id Soft-delete an entity. The entity's status is set to `deleted` and it will no longer appear in list or query results. ### Response ```json { "success": true } ``` ### Examples #### curl ```bash curl -X DELETE https://api.struere.dev/v1/data/customer/k17abc... \ -H "Authorization: Bearer sk_dev_abc123" ``` #### TypeScript ```typescript const response = await fetch( "https://api.struere.dev/v1/data/customer/k17abc...", { method: "DELETE", headers: { Authorization: "Bearer sk_dev_abc123" }, } ) const result = await response.json() console.log(result.success) ``` ## Error Handling ### Status Codes | Status | Meaning | Common Causes | |--------|---------|---------------| | `200` | Success | Request completed | | `201` | Created | Entity created (POST) | | `400` | Bad Request | Missing required fields, invalid path | | `401` | Unauthorized | Missing or invalid API key | | `403` | Forbidden | API key lacks `data` permission, or entity outside permission scope | | `404` | Not Found | Entity or entity type not found | | `429` | Too Many Requests | Rate limit exceeded (check `Retry-After` header) | | `500` | Internal Error | Server-side failure | ### Error Response Format ```json { "error": "Error description" } ``` Rate limit errors include a `Retry-After` header with the number of seconds to wait: ```json { "error": "Rate limit exceeded", "retryAt": 1710000060000 } ``` ### Handling Errors in Code **TypeScript:** ```typescript const response = await fetch("https://api.struere.dev/v1/data/customer", { headers: { Authorization: "Bearer sk_dev_abc123" }, }) if (!response.ok) { const error = await response.json() switch (response.status) { case 401: throw new Error("Invalid API key") case 403: throw new Error("Missing data permission") case 404: throw new Error("Not found") case 429: const retryAfter = response.headers.get("Retry-After") throw new Error(`Rate limited, retry in ${retryAfter}s`) default: throw new Error(`Error: ${error.error}`) } } const data = await response.json() ``` **Python:** ```python import requests import time response = requests.get( "https://api.struere.dev/v1/data/customer", headers={"Authorization": "Bearer sk_dev_abc123"}, ) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 5)) time.sleep(retry_after) elif response.status_code == 401: raise Exception("Invalid API key") elif response.status_code == 403: raise Exception("API key lacks data permission") elif response.status_code >= 400: raise Exception(f"Error: {response.json()['error']}") data = response.json() ``` --- ## Webhooks > Inbound webhook endpoints for external integrations Source: https://docs.struere.dev/api/webhooks.md Struere provides webhook endpoints for receiving events from external services. These endpoints are registered in the Convex HTTP router and process incoming events to keep your platform synchronized with third-party systems. ## Clerk Webhook **Endpoint:** `POST /webhook/clerk` Receives user, organization, and membership events from Clerk to keep the Struere database in sync with your authentication provider. ### Supported Event Types | Event Type | Action | |------------|--------| | `user.created` | Creates or updates the user record in Struere | | `user.updated` | Updates the user's email and name | | `organization.created` | Creates the organization in Struere | | `organization.updated` | Updates the organization's name and slug | | `organization.deleted` | Schedules cascading hard-delete of all organization data (agents, entities, threads, events, executions, API keys, integrations, credits, and the org record itself) | | `organizationMembership.created` | Links a user to an organization with their role | | `organizationMembership.updated` | Updates the user's role within the organization | | `organizationMembership.deleted` | Removes a user's membership from the organization | ### Role Mapping Clerk roles are mapped to Struere roles: | Clerk Role | Struere Role | |------------|--------------| | `org:admin` | `admin` | | `org:owner` | `admin` | | All other roles | `member` | ### Setup Configure the Clerk webhook in your Clerk Dashboard: 1. Navigate to **Webhooks** in the Clerk Dashboard 2. Create a new endpoint pointing to `https://api.struere.dev/webhook/clerk` 3. Select the event types listed above 4. Save the endpoint ### Payload Example ```json { "type": "user.created", "data": { "id": "user_2abc123", "email_addresses": [ { "id": "idn_2def456", "email_address": "alice@example.com" } ], "first_name": "Alice", "last_name": "Smith", "created_at": 1700000000000, "updated_at": 1700000000000 } } ``` ### Response Returns `200` with `{"received": true}` on success, or `500` if processing fails. ## WhatsApp Webhooks (Kapso) WhatsApp integration uses the Kapso service as an intermediary. Two webhook endpoints handle different aspects of the WhatsApp connection. ### Project Webhook **Endpoint:** `POST /webhook/kapso/project` Receives phone number connection events when a WhatsApp number is linked through the Kapso setup flow. **Authentication:** HMAC-SHA256 signature verification via the `X-Webhook-Signature` header. The signature is computed over the raw request body using the `KAPSO_WEBHOOK_SECRET` environment variable. **Event Types:** | Event Type | Action | |------------|--------| | `whatsapp.phone_number.created` | Updates the WhatsApp connection status to `connected` and registers the message webhook | | `whatsapp.phone_number.deleted` | Removes the WhatsApp connection and cleans up associated webhooks | When a phone number is connected, the system: 1. Finds the matching WhatsApp connection by Kapso customer ID 2. Stores the Kapso phone number ID and phone number 3. Sets the connection status to `connected` 4. Registers the messages webhook URL with Kapso for that phone number ### Messages Webhook **Endpoint:** `POST /webhook/kapso/messages` Receives inbound WhatsApp messages and message status updates. **Authentication:** HMAC-SHA256 signature verification via the `X-Webhook-Signature` header. **Event Types:** | Event Type | Action | |------------|--------| | `whatsapp.message.received` | Processes and stores the inbound message, routes to assigned agent | | `whatsapp.message.status_update` | Updates the delivery status of an outbound message | #### Inbound Message Payload (V2) ```json { "event": "whatsapp.message.received", "payload_version": "v2", "phone_number_id": "pn_abc123", "message": { "id": "wamid.HBgNNTUxMjM0NTY3ODkwFQIAERgSMDVBMkJFQkU2QUE0RTYxMjdBAA==", "timestamp": 1700000000, "type": "text", "text": "Hello, I need help with my booking", "kapso": { "content": "Hello, I need help with my booking", "transcript": null, "media_url": null } }, "conversation": { "phone_number": "+15551234567", "kapso": { "contact_name": "Alice Smith" } } } ``` #### Status Update Payload ```json { "event": "whatsapp.message.status_update", "payload_version": "v2", "phone_number_id": "pn_abc123", "message": { "id": "wamid.HBgNNTUxMjM0NTY3ODkwFQIAERgSMDVBMkJFQkU2QUE0RTYxMjdBAA==", "status": "delivered", "timestamp": 1700000005 } } ``` #### Inbound Message Flow When an inbound message arrives: 1. The phone number ID is used to look up the WhatsApp connection and determine the organization 2. The message is stored in the `messages` table with deduplication by `messageId` 3. If the message is new and contains text, `scheduleAgentRouting` is called 4. The agent routing mutation finds the connected agent and schedules `routeInboundToAgent` 5. The routing action creates or reuses a thread with `externalId` set to `whatsapp:{phoneNumber}` 6. The agent processes the message via `chatAuthenticated` using a system actor context 7. The agent's response is sent back via the Kapso API #### Status Updates Status updates (`sent`, `delivered`, `read`, `failed`) are applied to the matching outbound message record. ### Required Environment Variables | Variable | Description | |----------|-------------| | `KAPSO_WEBHOOK_SECRET` | Shared secret for HMAC signature verification | ## Flow Payment Webhook **Endpoint:** `POST /webhook/flow` Receives payment status updates from the Flow payment provider via form-encoded POST data. ### Request Format The webhook receives a `token` parameter via `application/x-www-form-urlencoded` form data. This token is used to verify the payment status with the Flow API. ### Processing Flow 1. Extract the `token` from the form data 2. Query all active Flow integration configurations 3. For each configuration, call Flow's payment verification API with the token 4. Based on the returned status: | Flow Status | Action | |-------------|--------| | `2` (Paid) | Mark the payment entity as paid with the current timestamp | | `3` (Rejected) | Mark the payment entity as failed with the status message | | `4` (Cancelled) | Mark the payment entity as failed with the status message | ### Response Always returns `200 OK` to acknowledge receipt of the webhook. ## Polar Webhook **Endpoint:** `POST /webhook/polar` Receives payment events from the Polar billing platform. **Authentication:** Standard Webhook Signature verification using `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. The signature is verified against the `POLAR_WEBHOOK_SECRET` environment variable. Timestamps older than 5 minutes are rejected. **Event Types:** | Event Type | Action | |------------|--------| | `order.paid` | Adds credits to the organization's account based on the order subtotal amount | | `order.refunded` | Deducts credits from the organization's account for the refunded amount | ### Payload Example ```json { "type": "order.paid", "data": { "id": "ord_abc123", "amount": 5000, "currency": "usd", "subtotal_amount": 5000, "customer": { "id": "cust_xyz789", "email": "admin@acme.com" }, "product": { "id": "prod_def456", "name": "500 Credits" }, "created_at": "2025-03-15T14:30:00Z" } } ``` The `subtotal_amount` (in cents) determines how many credits are added to the organization's balance. ### Required Environment Variables | Variable | Description | |----------|-------------| | `POLAR_WEBHOOK_SECRET` | Secret key for webhook signature verification | ## Resend Webhook **Endpoint:** `POST /webhook/resend` Receives email status updates from Resend. **Authentication:** Svix signature verification using `svix-id`, `svix-timestamp`, and `svix-signature` headers. The signature is verified against the `RESEND_WEBHOOK_SECRET` environment variable. Timestamps older than 5 minutes are rejected. **Event Types:** | Event Type | Action | |------------|--------| | `email.sent` | Updates the email record status to `sent` | | `email.delivered` | Updates the email record status to `delivered` | | `email.bounced` | Updates the email record status to `bounced` | | `email.complained` | Updates the email record status to `complained` | ### Required Environment Variables | Variable | Description | |----------|-------------| | `RESEND_WEBHOOK_SECRET` | Secret key for Svix webhook signature verification | ## Webhook Security All webhooks that handle sensitive operations use signature verification: | Webhook | Verification Method | |---------|-------------------| | Clerk | Configured in Clerk Dashboard (Svix signatures) | | Kapso (WhatsApp) | HMAC-SHA256 via `X-Webhook-Signature` header | | Flow | Token-based verification via Flow API callback | | Polar | Standard Webhook Signature (HMAC-SHA256 with base64) | | Resend | Svix signature verification (HMAC-SHA256 with base64) | --- ## JavaScript Client > Typed JS/TS client for the Struere HTTP API Source: https://docs.struere.dev/api/javascript-client.md The `struere` npm package ships a typed JavaScript client under the `struere/client` subpath. It wraps the Chat API and Data API with end-to-end TypeScript types, throws structured errors, and runs in browsers, Node 18+, Bun, and Deno with zero runtime dependencies. ## Installation ```bash bun add struere ``` ```ts import { StruereClient, StruereApiError } from 'struere/client' ``` The `struere/client` subpath has no dependency on the Node-only definition primitives (`defineAgent`, `defineData`, etc.), so it is safe to import from browser bundles. ## Constructor ```ts const struere = new StruereClient({ apiKey: process.env.STRUERE_API_KEY!, }) ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `apiKey` | `string` | — | API key for the target environment. Required | | `baseUrl` | `string` | `'https://api.struere.dev'` | Override the API base URL | | `fetch` | `typeof globalThis.fetch` | `globalThis.fetch` | Custom fetch implementation for tests or runtimes without a global fetch | The client throws synchronously on construction if `apiKey` is missing or if no `fetch` is available. ## Works In - **Browsers** — CORS-enabled (`Access-Control-Allow-Origin: *`) on Chat and Data endpoints, no proxy required - **Node 18+** — uses the global `fetch` - **Bun** — uses the global `fetch` - **Deno** — uses the global `fetch` ## Chat ```ts const reply = await struere.chat({ agentSlug: 'support', message: 'Hi!', }) console.log(reply.threadId, reply.message, reply.usage) ``` ### `client.chat(request)` ```ts interface ChatRequest { agentSlug?: string agentId?: string routerSlug?: string message: string threadId?: string externalThreadId?: string threadContext?: { params?: Record } } ``` Exactly one of `agentSlug`, `agentId`, or `routerSlug` is required. The client routes to: - `POST /v1/agents/:slug/chat` when `agentSlug` is set - `POST /v1/routers/:slug/chat` when only `routerSlug` is set - `POST /v1/chat` when `agentId` is set (with optional `routerSlug` in the body) ```ts interface ChatResponse { threadId: string message: string assistantMessageId?: string usage: { inputTokens: number outputTokens: number totalTokens: number reasoningTokens?: number } _executionMeta?: { iterationCount: number model: string durationMs: number toolCallSummary: Array<{ name: string; durationMs: number; status: string; errorType?: string; errorMessage?: string }> errorCount: number permissionDenialCount: number } _transferred?: { targetAgentSlug: string; targetAgent: string } } ``` Pass `threadId` to continue an existing conversation, or `externalThreadId` to deduplicate by an upstream identifier. ## Data The `client.data` namespace exposes typed CRUD and query operations against the Data API. All methods return promises and throw `StruereApiError` on non-2xx responses. ### `client.data.entityTypes()` Lists all entity types in the current environment. ```ts const { data } = await struere.data.entityTypes() data.forEach((t) => console.log(t.slug, t.name)) ``` ### `client.data.list(type, options?)` Paginated list of entities for a given type. ```ts const players = await struere.data.list<{ name: string }>('player', { limit: 50 }) console.log(players.data, players.cursor, players.hasMore) ``` | Option | Type | Description | |--------|------|-------------| | `limit` | `number` | Page size | | `cursor` | `string` | Pagination cursor returned by a previous call | | `status` | `string` | Filter by entity status | ### `client.data.get(type, id)` Fetch a single entity by ID. ```ts const player = await struere.data.get<{ name: string }>('player', 'e57abc123') ``` ### `client.data.create(type, data, options?)` Create a new entity. ```ts const created = await struere.data.create('player', { name: 'Mia', team: 'red' }) ``` ### `client.data.update(type, id, patch, options?)` Patch an existing entity. Only the fields you pass are updated. ```ts const updated = await struere.data.update('player', 'e57abc123', { team: 'blue' }) ``` ### `client.data.delete(type, id)` Delete an entity by ID. ```ts await struere.data.delete('player', 'e57abc123') ``` ### `client.data.query(type, options?)` Filtered query with pagination. ```ts const matches = await struere.data.query('player', { filters: { team: { $eq: 'red' }, score: { $gt: 100 } }, limit: 25, }) ``` | Option | Type | Description | |--------|------|-------------| | `filters` | `Record` | Field filters (see operators below) | | `status` | `string` | Filter by entity status | | `limit` | `number` | Page size | | `cursor` | `string` | Pagination cursor | #### Filter Operators A `FilterValue` is either a literal value (implicit `$eq`) or one of: | Operator | Type | Description | |----------|------|-------------| | `$eq` | `unknown` | Field equals value | | `$neq` | `unknown` | Field does not equal value | | `$in` | `unknown[]` | Field value is one of the array members | | `$contains` | `unknown` | Field contains substring or array element | | `$gt` | `unknown` | Greater than | | `$gte` | `unknown` | Greater than or equal | | `$lt` | `unknown` | Less than | | `$lte` | `unknown` | Less than or equal | | `$exists` | `boolean` | Field is present (`true`) or absent (`false`) | ```ts const active = await struere.data.query('player', { filters: { team: { $in: ['red', 'blue'] }, nickname: { $contains: 'fox' }, retiredAt: { $exists: false }, }, }) ``` ### `client.data.search(type, options)` Full-text search across the entity type's `searchFields`. ```ts const results = await struere.data.search<{ name: string }>('player', { query: 'mia', limit: 10, }) ``` ## Errors All client methods throw `StruereApiError` on non-2xx HTTP responses. ```ts import { StruereApiError } from 'struere/client' try { await struere.data.get('player', 'missing') } catch (err) { if (err instanceof StruereApiError) { console.error(err.status, err.message, err.code, err.requestId) } else { throw err } } ``` | Field | Type | Description | |-------|------|-------------| | `status` | `number` | HTTP status code | | `message` | `string` | Error message from the server, or `HTTP ` if none | | `code` | `string?` | Machine-readable error code, when provided | | `requestId` | `string?` | Request identifier for correlating with server logs | | `details` | `string?` | Additional human-readable detail | | `body` | `unknown?` | Parsed response body | ### Filter validation errors The Data API rejects filters that target unsupported top-level fields. Both errors return `400 Bad Request` and surface through `StruereApiError`. **Unsupported top-level filter field.** Top-level filters must be on indexed columns. Domain fields stored in the JSON payload must be referenced as `data.`. ```ts try { await struere.data.query('session', { filters: { teacherId: 'usr_123' }, }) } catch (err) { if (err instanceof StruereApiError) { console.error(err.status, err.message) } } ``` ```json { "error": "Unsupported filter field 'teacherId'. Queryable top-level fields: matchId. For fields stored in the entity payload, use 'data.teacherId'." } ``` **Top-level `status` filter rejected.** Top-level `status` is the entity lifecycle column, not your domain status. The error points to `data.status`. ```ts try { await struere.data.query('session', { filters: { status: 'scheduled' }, }) } catch (err) { if (err instanceof StruereApiError) { console.error(err.status, err.message) } } ``` ```json { "error": "Top-level 'status' filter is not supported (it targets the entity lifecycle column). Use filter: { 'data.status': 'scheduled' } for your domain status field." } ``` The fix in both cases is to prefix the field with `data.`: ```ts await struere.data.query('session', { filters: { 'data.status': 'scheduled', 'data.teacherId': 'usr_123' }, }) ``` ## Browser Example: Vite + React A minimal Vite component that lists entities and renders them. Requires a development API key created via `bunx struere keys create --env development` and stored in `VITE_STRUERE_API_KEY`. ```tsx import { useEffect, useState } from 'react' import { StruereClient, StruereApiError } from 'struere/client' const struere = new StruereClient({ apiKey: import.meta.env.VITE_STRUERE_API_KEY, }) interface Player { name: string team: string } export function PlayerList() { const [players, setPlayers] = useState([]) const [error, setError] = useState(null) useEffect(() => { struere.data .list('player', { limit: 25 }) .then((res) => setPlayers(res.data.map((e) => e.data))) .catch((err) => { if (err instanceof StruereApiError) { setError(`${err.status}: ${err.message}`) } else { setError(String(err)) } }) }, []) if (error) return

Error: {error}

return (
    {players.map((p, i) => (
  • {p.name} ({p.team})
  • ))}
) } ``` For chat from a browser, swap `data.list` for `chat()`: ```ts const reply = await struere.chat({ agentSlug: 'support', message: 'Hi!', }) ``` ## See Also - [API Overview](./overview) — Authentication, base URL, CORS - [Chat API](./chat) — Raw HTTP request/response for chat - [Data API](./data) — Raw HTTP request/response for entity CRUD - [struere keys](../cli/keys) — Create the API key the client uses