Better conversations.
Built together.
Bring AI-powered support into your product. Connect your tools, build customer experiences, and make every conversation count.
Developers
OpenAPI JSON
Dashboard
Bring AI-powered support into your product. Connect your tools, build customer experiences, and make every conversation count.
Make a read-only request from your server to list the agents your workspace account can access.
Sign in at app.tuolen.com and open Settings → API Tokens. Create a personal access token and store the value immediately: it is shown only once. Token management requires an org_admin, admin, or developer role. Ask your workspace administrator if you do not have access.
Tuolen onboarding is invite-only. If you do not have an account, request access on the product site. Do not use a publishable widget key for this request.
https://api.tuolen.com is the production origin. Endpoint paths already include /api; do not add a second prefix. The interactive reference uses the API host that serves this page. If you have a separate test deployment, substitute its origin deliberately.
Set TUOLEN_TOKEN in your server environment or secret manager. Never commit it, put it in frontend code, or include it in a URL. Avoid pasting real credentials into shell history or shared logs.
# Set TUOLEN_TOKEN securely in your server environment first.
: "${TUOLEN_TOKEN:?Set TUOLEN_TOKEN to your personal access token}"
curl --fail-with-body --silent --show-error --max-time 15 \
'https://api.tuolen.com/api/agents' \
--header "Authorization: Bearer $TUOLEN_TOKEN" \
--header 'Accept: application/json'// Save as list-agents.mjs. Run with Node.js 20+ on your server.
const token = process.env.TUOLEN_TOKEN;
if (!token) throw new Error('Set TUOLEN_TOKEN in your environment');
const response = await fetch('https://api.tuolen.com/api/agents', {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
redirect: 'error',
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}; request ID: ${response.headers.get('x-request-id') ?? 'unavailable'}`);
}
console.log(JSON.stringify(await response.json(), null, 2));The Node.js example needs no package installation. After setting the environment variable, run node list-agents.mjs. Both examples perform only GET /api/agents, with a timeout and no automatic retries.
A successful request returns HTTP 200 and a data array, not a paginated object. An empty array is valid when no agents are accessible. Below is a synthetic example; its IDs and values are not live workspace data.
{
"success": true,
"data": [
{
"id": "11111111-1111-4111-8111-111111111111",
"organizationId": "22222222-2222-4222-8222-222222222222",
"name": "Support",
"description": "Customer support assistant",
"systemPrompt": "You are a helpful assistant.",
"emailSignature": "",
"emailSignatureHtml": "",
"model": null,
"provider": null,
"temperature": 0.7,
"maxTokens": 2048,
"welcomeMessage": "Hello! How can I help you today?",
"ratingEnabled": false,
"inactivityTimeoutMinutes": null,
"handoffMode": "human",
"ticketProvider": null,
"handoffFallbackMinutes": null,
"requireIdentityVerification": false,
"initialForm": null,
"isActive": true,
"hasIdentitySecret": false,
"createdAt": "2026-10-04T12:00:00.000Z",
"updatedAt": "2026-10-04T12:00:00.000Z"
}
]
}hasIdentitySecret reports configuration, never the secret itself. model and provider may be null. See List agents for the field contract.
The reference can call production. “Try it out” is opt-in; write and delete operations can change real data or send external messages. Authorization is kept only in this page’s memory, not persisted across reloads.
Choose the right credential, page through results, or troubleshoot a failed request.
Workspace management, visitor interactions, and reporting use different credentials. Check the security requirement on each operation.
Authorization: Bearer <token>BearerAuth accepts a dashboard JWT or personal access token (PAT). Permissions still depend on your role, workspace, agent membership, and feature entitlement.
Create a PAT at Dashboard → Settings → API Tokens. You can have at most 10 active PATs per user; optional expiry must be a future ISO date-time. The raw token is returned once. Revoke unused tokens and replace credentials that may have been exposed. Generation also has a separate rate limit.
Authorization: Bearer <customer-jwt>WorkspaceJwtAuth requires a fresh customer JWT, not a PAT. Billing, plans, notifications, and guided WhatsApp onboarding use this scheme. A role change or session revocation can invalidate a previously issued credential; sign in again rather than retrying the same rejected token.
X-API-Key: <widget-key>ApiKeyAuth is the publishable, agent-scoped widget key with widget:chat scope. If the operation also requires VisitorTokenAuth, send X-Visitor-Token for that visitor. Both headers are required in that case.
A widget key cannot manage your workspace. Never substitute a PAT or reporting key in browser widget code. Visitor tokens and recovery tokens must not be shared across visitors.
X-API-Key: <reporting-key>ReportingKeyAuth is an agent-scoped, server-only reporting key with reporting:read scope. Use it for /api/public/v1 reporting operations. Reporting and widget keys are not interchangeable; do not embed a reporting key in a website.
API key creation and rotation return the raw key only once. Rotation revokes the old key, preserves its audience and expiry, and does not extend an expired key. Coordinate the replacement in your application.
Store PATs, reporting keys, webhook signing secrets, and provider connection credentials in a secret manager. Do not send them to support or store them in URLs, analytics, screenshots, or frontend bundles. The interactive reference does not persist authorization.
A 401 usually means missing, invalid, expired, or wrong-type credentials. A 403 may indicate role, workspace, membership, origin, or entitlement restrictions. Documentation visibility does not grant access. See errors and retries.
Pagination is operation-specific. Do not assume every list returns data.items, a total count, or a next cursor.
| Endpoint | Parameters | Response / stop condition |
|---|---|---|
GET /api/agents | No pagination | data array; newest created first. |
GET /api/conversations | page=1, limit=20; maximum page 10,000 / limit 100 | data.items, total, page, limit, totalPages. Stop on an empty page or when page reaches totalPages. |
GET /api/knowledge-bases/agent/{agentId} | offset=0, limit=100; maximum limit 200 | data array + top-level pagination: {limit, offset}. No total. |
GET /api/documents/knowledge-base/{kbId} | offset=0, limit=100; maximum limit 500 | Same offset envelope. Increment offset by the returned limit; stop when data.length is less than limit. |
GET /api/public/v1/conversations | page=1, limit=50; maximum page 100,000. Limit cap is deployment-configured, default 100; see the operation. | Page envelope plus messageContentIncluded and messageMetadataIncluded in data. Requires a reporting key. |
GET /api/conversations/visitor/history | limit=20, maximum 50; optional cursor | data.items and data.nextCursor. Pass the returned cursor unchanged; stop when it is null. |
{
"success": true,
"data": {
"items": [],
"total": 0,
"page": 1,
"limit": 20,
"totalPages": 0
}
}For knowledge bases and documents, the envelope is different:
{
"success": true,
"data": [],
"pagination": {
"limit": 100,
"offset": 0
}
}Use the query parameters listed on the individual operation. Dashboard conversation search is limited to 200 characters. Reporting supports date, status, rating, and message-inclusion filters; never assume it returns message content by default.
Dashboard conversations are sorted by updatedAt descending. Knowledge bases and documents are sorted by createdAt descending. These lists are not snapshots: concurrent writes can move or remove records while you page. Deduplicate by resource ID, keep filters stable, and use bounded loops.
Dashboard conversation detail includes at most its latest 200 messages, in chronological order. Attachment URLs are signed, temporary links. Webhook delivery logs return only the latest 100 records with no pagination or replay API.
Use the HTTP status first. Error codes and extra fields vary by operation; do not depend on every failure having the same body.
{
"success": false,
"error": "Too many requests, please try again later."
}Many JSON errors also include an optional code. Validation details may be omitted in production. Some operations return other content types or a specialized error result, such as the webhook test endpoint’s HTTP 502 delivery envelope.
| Status | What to check |
|---|---|
| 400 | Correct the input, required fields, formats, and conditional validation. Do not retry unchanged input. |
| 401 / 403 | Check credential type, expiry, workspace/agent access, role and feature entitlement. Refresh a session only through your normal sign-in flow. |
| 404 | Verify the resource ID and workspace. A missing response can also conceal a resource you cannot access. |
| 409 | Read the operation-specific state/version/billing conflict. Refresh the resource before deciding what action is safe. |
| 429 | Honor Retry-After when present. Slow down all requests sharing the same budget, not just one process. |
| 5xx / timeout | For safe reads, use bounded exponential backoff with jitter. For writes, the outcome may be unknown; reconcile before trying again. |
Current default application limits include a general API budget of 100 requests/minute per IP, reporting at 60/minute per API key, authentication at 20 attempts/15 minutes per IP, and widget requests at 30/minute keyed by API key (falling back to IP). Reporting and other routes under /api are also subject to the shared IP budget.
Uploads, token generation, voice, exports, and other routes have additional limits. These defaults are not a reserved throughput allowance. Observe the response rate-limit headers and Retry-After; custom 429 responses may not include that header.
Retry-After is present, do not retry sooner. If it exceeds your deadline, stop and defer the work.Idempotency-Key header. Use an idempotency field only when the specific operation documents it; a random header does not make a write safe.Capture the HTTP method, redacted path, UTC time, status, and returned X-Request-Id when available. Contact hello@tuolen.com with those details. Remove tokens, signing secrets, provider credentials, customer message content, and sensitive query values from diagnostics.
Receive signed outbound events at a public endpoint you control. Webhook configuration requires the paid webhook entitlement.
Use a public HTTP(S) URL; HTTPS is recommended. Replace the example URL below before running the POST. Supported roles are org_admin, admin, and developer. A PATCH must contain at least one supported field.
# This POST creates a real webhook. Use an approved receiver you control.
: "${TUOLEN_TOKEN:?Set TUOLEN_TOKEN to your personal access token}"
curl --fail-with-body --silent --show-error --max-time 15 \
--request POST 'https://api.tuolen.com/api/webhooks' \
--header "Authorization: Bearer $TUOLEN_TOKEN" \
--header 'Content-Type: application/json' \
--data '{"name":"Conversation events","url":"https://example.com/webhooks/tuolen","events":["conversation.created","conversation.resolved"],"isActive":true}'If you omit secret, the response returns a generated signingSecret at the top level, once only. Store it securely before closing the response. If you supply a secret (16–500 characters), that field is omitted from the response. Subsequent reads expose hasSecret, never encrypted secret material.
{
"id": "66666666-6666-4666-8666-666666666666",
"event": "conversation.created",
"createdAt": "2026-10-04T12:00:00.000Z",
"data": {
"conversationId": "33333333-3333-4333-8333-333333333333",
"agentId": "11111111-1111-4111-8111-111111111111",
"source": "web_widget"
}
}| Event | Event data |
|---|---|
conversation.created | conversationId, agentId, source |
conversation.resolved | conversationId, agentId |
message.created | messageId, conversationId, sender, content, metadata |
tool.executed | agentId, toolId, toolName, nullable conversationId, channel, success, nullable statusCode, durationMs |
integration.sync.completed / integration.sync.failed | runId, itemsProcessed, nullable error |
Payloads can contain customer content. Apply least-privilege access and your retention policy. The event id identifies this signed delivery, not the underlying conversation or message.
Requests use Content-Type: application/json, X-Tuolen-Event, X-Tuolen-Delivery, X-Tuolen-Timestamp (Unix seconds), and X-Tuolen-Signature (sha256=<hex>).
The signature is HMAC-SHA256 of the exact timestamp header, a period, and the original raw request bytes. Capture a Buffer before JSON middleware—for example, a route-specific express.raw({ type: 'application/json', limit: '1mb' }) before express.json(). Choose a body-size limit suitable for your receiver. Do not reserialize JSON for verification.
// Save as verify-tuolen-webhook.mjs. Node.js 20+, server-side only.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyTuolenWebhook({
rawBody, timestamp, signature, secret, now = Date.now(),
}) {
// Reject missing/duplicate headers and malformed signatures before decoding.
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
if (typeof timestamp !== 'string' || !/^[0-9]{1,12}$/.test(timestamp)) return false;
if (typeof signature !== 'string' || !/^sha256=[a-fA-F0-9]{64}$/.test(signature)) return false;
const seconds = Number(timestamp);
const nowSeconds = Math.floor(now / 1000);
// Receiver policy: at most 5 minutes old, at most 30 seconds in the future.
if (!Number.isSafeInteger(seconds) || !Number.isFinite(nowSeconds)) return false;
if (seconds < nowSeconds - 300 || seconds > nowSeconds + 30) return false;
// rawBody must be the original Buffer, captured BEFORE JSON parsing.
const expected = createHmac('sha256', secret)
.update(timestamp + '.')
.update(rawBody)
.digest();
const received = Buffer.from(signature.slice(7), 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}Pass the raw Buffer, the two signature/timestamp header strings, and your stored secret to verifyTuolenWebhook. Reject verification failures without processing the event. Keep your server clock synchronized; the five-minute age and 30-second future allowance above are receiver policy, not a Tuolen delivery guarantee.
After verification, parse and validate the event body. Use its signed id as a durable deduplication key, and atomically record acceptance with your processing job before acknowledging. A previously accepted duplicate should not repeat the side effect. Do not deduplicate using the unsigned X-Tuolen-Delivery header alone. Route by the verified body’s event, not an unverified header.
id is not the signed event ID.POST /api/webhooks/{id}/test takes no body and sends a real tool.executed event with data: {test: true, organizationId}, even if that event is not selected. This test data differs from an ordinary tool event.
The test API returns HTTP 200 if your receiver responds 2xx, or HTTP 502 with {success: false, data: {success, statusCode, error, durationMs}} on failure. A successful test checks that delivery only; it does not enable retries or prove future availability.
This changelog records documentation improvements. It does not imply an API behavior change or a new compatibility guarantee.
/docs and /developers redirect to the developer portal.openapi: 3.1.0 is the specification format. info.version: 1.0.0 identifies the documentation; it does not mean every API path is versioned. Only the reporting API uses the /api/public/v1 prefix. Use operation paths exactly as listed.
Some older operations still document only a common response envelope. They are marked x-tuolen-response-coverage: envelope-only and include a warning in the reference. Format-only export descriptions are also marked. Do not infer missing fields from Swagger’s generated sample or assume full generated-SDK coverage.
Where runtime validation has conditional rules that cannot be expressed by the schema generator, read the operation notes as well. Plan/provider availability, role checks, and tenant isolation remain enforced at runtime.
No general deprecation notice period or API compatibility SLA is published here. Before adopting an operation whose contract is incomplete, confirm the required fields and behavior with hello@tuolen.com. Monitor this changelog and the OpenAPI specification when updating your integration.
Explore the customer API. Select an endpoint to view its contract and try a request.
Check your connection and whether API documentation is enabled.