Serve multiple customers
Isolate each customer's learned weights with one tenant ID and a memory name.
Use tenant_id for the customer whose knowledge you are learning or using. Use subject_id for the memory within that customer. The complete identity is project → tenant → memory. There is no tenant-creation request, agent object, or additional deployment to manage.
{
"tenant_id": "customer_123",
"subject_id": "support-agent",
"content": "Northstar accepts returns within 30 days of delivery. Return labels are prepaid.",
"max_cost_microusd": 5000000,
"activate": true
}Send that body to POST /v1/internalizations with your server key and a durable Idempotency-Key. Wait for a settled ready result with result.activated: true. Inference with the same tenant and subject then resolves that memory's active version. The learning passage does not become part of the question's context.
Choose the boundary
| Your application | tenant_id | subject_id |
|---|---|---|
| One support bot per business | Your business account ID | support-agent |
| A personal assistant per user | Your authenticated user ID | assistant |
| Several agents sharing one customer's knowledge | Your customer ID | A shared memory name |
| Separate memories inside one customer | Your customer ID | A distinct name for each memory |
Both IDs are case-sensitive, 1–128 characters, starting with a letter or number. Subsequent characters can include letters, numbers, dots, underscores, colons, and hyphens. Prefer opaque IDs over emails or other personal information.
Two tenants can both have a memory called support-agent without sharing learned state. A new tenant/memory pair starts from clean base. It does not inherit another tenant's knowledge, an unscoped memory of the same name, or a saved but inactive candidate.
Bind identity outside model input
Your server authenticates the end user and looks up their authorized tenant. Never use a tenant ID suggested by the model, a prompt, a URL parameter from an untrusted caller, or an unchecked request body as authorization.
An unrestricted project key is a trusted server credential: it can select any tenant permitted by its operation scopes. Namespace selection does not replace your application's authentication. Keep keys out of browsers and mobile clients.
For a dedicated customer deployment, create a key with Restrict to a customer in API keys. The tenant restriction is immutable. Omitted tenant IDs resolve to that key's tenant; attempts to select another return 403 tenant_scope_mismatch. Permissions still default to inference only. Enable learning separately, or use a separate management key when a service must change retained versions.
For many customers, use a shared server key and select the authorized tenant on each request. You do not need a key per customer; a project supports up to 50 non-revoked keys.
OpenAI-compatible clients
Set the tenant and memory in request headers. This keeps the normal Chat Completions body unchanged and works with clients that do not expose custom body fields.
import OpenAI from "openai";
const model = new OpenAI({
baseURL: "https://convergingthought.com/v1",
apiKey: process.env.INTERNALIZE_API_KEY,
maxRetries: 0,
});
// Resolve this from your verified session on the server.
const authorizedTenant = "customer_123";
const response = await model.chat.completions.create(
{
model: "glm-5.3",
messages: [{ role: "user", content: "Who pays for a return label?" }],
},
{
headers: {
"X-Internalize-Tenant": authorizedTenant,
"X-Internalize-Subject": "support-agent",
"Idempotency-Key": "return-label-question-001",
},
},
);Native and Chat Completions bodies also accept tenant_id. A body value and X-Internalize-Tenant must agree when both are present. Conflicts return 422 invalid_request; no operation is admitted. Chat Completions without a subject still explicitly uses base weights, even if a tenant was supplied.
For Python, use extra_headers={"X-Internalize-Tenant": authorized_tenant, "X-Internalize-Subject": "support-agent"} on create. Framework clients can pass the same headers through their provider configuration. Use per-request options or an immutable per-customer client; do not mutate a shared client's defaults during concurrent requests.
Bind the learning tool and polling together
The workspace SDK's forTenant returns an independent client. Learning, inference, polling, and activation on that client all carry the same tenant header.
import { InternalizeClient, createInternalizeTool } from "@internalize/sdk";
const shared = new InternalizeClient({
baseUrl: "https://convergingthought.com",
apiKey: process.env.INTERNALIZE_API_KEY!,
});
const customer = shared.forTenant("customer_123");
const tool = createInternalizeTool(customer, "support-agent", {
maxCostMicrousd: 5_000_000,
});
// Only content is a model-supplied argument. Your runtime persists invocationId.
const result = await tool.execute(
{
content:
"Northstar accepts returns within 30 days. Return labels are prepaid.",
},
"policy-update-001",
);
if (!result.activated) throw new Error(`Review learning job ${result.job_id}`);A tenant-bound client cannot be rebound to a different tenant. The API key's server-enforced restriction still applies regardless of which SDK is used. The SDK is a private workspace package; see TypeScript client for packaging and error behavior.
Read and manage in the same namespace
Repeat X-Internalize-Tenant on GET /v1/jobs/{id}, GET /v1/jobs, GET /v1/subjects, and POST /v1/adapters/activate. A tenant-restricted key supplies its own namespace when the header is omitted. A key without that restriction must explicitly select the tenant on reads, even when it knows the resource ID.
Job reads and lists return tenant_id; subject lists return it beside each memory name. A job or retained version from another tenant returns not_found. Pinning a version also requires the exact owning memory in the same tenant. A broad management key does not silently bypass the namespace selected on a request.
In the console, project owners can see all customer memories and activity. Memory rows show their customer ID and can be searched by either name or customer. Opening a memory in Playground preserves both identities. The sidebar's Customer & version section exposes the same namespace and version controls.
Retries, billing, and existing applications
Idempotency keys are scoped to project and tenant. Two customers may use the same operation key independently. Within one tenant, retry with the same API key, body, and operation key; a changed payload or submitting key conflicts. A tenant-restricted key produces the same request identity whether its matching tenant is explicit or omitted.
With no tenant on an unrestricted key, calls use the original, separate project namespace. Existing memory and job IDs keep working. The console labels that namespace Project memory. It is not a shared fallback for customer namespaces. To move an application, explicitly learn or import its canonical knowledge under the new tenant; changing an ID does not copy weights.
All tenants share the project's credit balance, spending limit, and admission rate limit. Tenant IDs are not independent billing accounts. Use job usage records for your own customer accounting; use separate projects when you need separate balances. There is no cross-tenant memory merge, automatic inheritance, tenant deletion endpoint, or claim of dedicated per-tenant compute.