API Reference
Endpoints
These HTTP endpoints make up the host-app integration surface. All authenticate with a workspace API key (Authorization: Bearer cqai_…) sent as a Bearer token.
POST/api/{workspaceKey}/embed-tokens
Mint a short-lived JWT for one end-user of your workspace — it names no endpoint.
Request body
{
"endUserHandle": "user_42", // required, max 255 chars; must not start with "org-"
// configEnd is no longer accepted (annotation 3.0) — sending it is a 400
"openingContext": { // optional — see /docs/opening-context
"intent": "The user opened the chat from the Listings page.",
"toolClass": "ListingsTool"
},
"attachments": { "enabled": true, "allowedTypes": ["image/png", "image/jpeg"], "camera": true }, // absent = no attach button, no paste, no ctrl+v
"ttlSeconds": 3600 // optional, default 86400
// see /docs/embed for the full claims reference (organizationId, openingContext, feedback, attachments)
}Response
{
"token": "eyJhbGciOi...",
"jti": "01HXYZ...",
"expiresAt": "2026-05-13T18:00:00Z"
} The token's jti is recorded server-side and can be revoked. Send the attachments block on every mint: without it the widget shows no attach button, no paste and no ctrl+v. Hand the token to the embed SDK on your page — full claims reference in Embed Token & SDK.
Read & write config data
Values an end-user collected during a chat are stored in confiqure. Read them back — or write them directly — over the three routes below. configEnd is your object's end — from @Confiqure.Setting(end = …), @Confiqure.List(end = …) or their User.* pair — with the leading slash stripped (/suppliers → suppliers); endUserHandle is the same id you minted the embed token with.
Record shape (returned by every route below)
{
"confiqureKey": "7299ac44b18b", // stable per-record id (see note)
"configEnd": "/suppliers",
"configType": "MULTI", // SINGLE = a Setting object, MULTI = a List object
"endUserHandle": "user-28",
"endpointVersion": 3,
"data": { /* typed field values, or null — deserialize THIS into your DTO */ },
"errors": { /* fieldName → reason it couldn't be populated (hard) */ },
"warnings": { /* fieldName → soft / in-progress reason */ },
"values": { /* DEPRECATED — alias for data */ },
"callbackStatus": "DELIVERED", // PENDING = draft, DELIVERED = finalized
"completedAt": "2026-06-13T17:42:11Z" // last-write time until completion — non-null on drafts too
}Deserialize data, not values
data always deserializes into your DTO: a structured field holds valid typed data or null — never a status or error string. Any reason a field is missing or in progress lives out-of-band in errors / warnings, which you can surface to users or ignore. values is a deprecated alias for data kept for one transition release — switch your reads to response.get("data").
GET/api/{workspaceKey}/endpoints/{configEnd}/data/{endUserHandle}
All saved data for this user at this endpoint. A Setting object (SINGLE) returns one record object (404 if nothing has been saved yet); a List object (MULTI) returns an array of records, each with its own confiqureKey. The list includes drafts — tell them apart by callbackStatus, never completedAt. A record held mid-migration returns 409 with {"status":"migrating"} — retry shortly.
GET/api/{workspaceKey}/endpoints/{configEnd}/data/{endUserHandle}/{confiqureKey}
Fetch one record of a List object by its stable confiqureKey — a targeted lookup instead of pulling the whole list and matching on a name. Returns a single record object, or 404 if no record carries that key.
POST/api/{workspaceKey}/endpoints/{configEnd}/data/{endUserHandle}
Create or update values. Writes deep-merge into the existing record, so you can patch one nested field without clobbering the rest. Omit confiqureKey to create a new List record (a key is minted and returned); include it to target an existing one — an unknown key is a 404, never a silent create. On a List object with an @Confiqure.Identity field, a write that would give a second record the same identity is a 409 {"code":"IDENTITY_CONFLICT","identityField":…,"existingKey":…} naming the record that already holds it. This POST is the only write: there is no PUT (it returns 405), and a host write never fires a callback — records you write stay PENDING until a conversation finalizes them.
Request body
{
"confiqureKey": "7299ac44b18b", // optional — omit to create a new MULTI record
"data": { // "values" still accepted this release
"name": "Acme Supply Co.",
"leadDays": 5
}
}Returns the saved record in the shape above. The response's confiqureKey is the one to keep.
Conditional writes (If-Match)
Every read of one record carries an ETag header, and every write answers with the record's new ETag. Send the stamp you last read back as If-Match to write only if nobody changed the record in between: on a match the write goes through as usual; on a stale stamp the answer is 412 {"error":"PRECONDITION_FAILED","message":…} with the record's current ETag, and nothing is written — read the record again and retry. The stamp is compared as an opaque value, so send it exactly as you received it (the weak W/"…" form and the bare quoted form are both accepted). If-Match: * means "the record exists": it writes when it does and is 412 when it does not. A stamp on a write that addresses no existing record — a List write without a confiqureKey, or a Setting that was never saved — is a 400: a create has nothing to match. A write without If-Match behaves as before. The check and the write are two steps a few milliseconds apart, not one atomic operation: two conditional writes arriving at the same instant can both pass.
Finalizing with the write (complete)
A record written through this API stays a draft (callbackStatus PENDING) until it is completed. To write and finalize in one call, send the envelope with "complete": true beside data: {"data": {…}, "complete": true}. After the write succeeds that one record is finalized exactly as the chat finalizes it: its values are coerced to the class, it becomes DELIVERED, and your signed onComplete callback fires with that record's confiqureKey (and conversationId null, since no chat was involved). The response is the usual record with callbackStatus DELIVERED. The flag is read only in the envelope form — in a bare body every key is a value, so a field of your own named complete is stored as a value. Without the flag, or with false, the write behaves as before.
What confiqureKey is
A stable, backend-minted id for one record. It's assigned once on first save and never changes when the values are edited — so it's the reliable handle for a List record. Store it on your side and use the by-key route; don't match on a display name, which a user can change. A Setting object has exactly one record per owner.
DELETE/api/{workspaceKey}/endpoints/{configEnd}/data/{endUserHandle}/{confiqureKey}
Delete one record by its confiqureKey — the same targeting as the by-key GET. The record is soft-deleted (it disappears from every read immediately, and stays staff-recoverable) and its stored secrets are purged. The same config.deleted callback fires as a chat-initiated delete, so your webhook picture is identical however a record is removed. Returns 200 with { "confiqureKey": "…", "deleted": true }, or 404 if no live record carries that key (already-deleted reads as gone).
GET/api/{workspaceKey}/objects/{confiqureKey}
Resolve any record by its confiqureKey alone — no endpoint or handle needed (keys are global object ids). Returns the raw stored values with the key injected; 404 when missing or deleted. This is the natural provisioning read after an onComplete callback.
PUT/api/{workspaceKey}/users/{endUserHandle}/language
Set the end user's preferred language server-side (GET the same path to read it back) so their next conversation opens in it. Body {"language": "tr-TR"} — a BCP-47 tag or a plain name, ≤ 35 chars. Last write wins; per-member, never org-shared. Full semantics in Embed Token & SDK → User language.
GET/api/{workspaceKey}/costs
confiqure's billable rate for AI usage (includes platform margin), in USD — not confiqure credits — for one end user or one org, rolled up over every chat call (and any atomic-agent call carrying a conversation) in that scope. Build your own end-user pricing on top of it if you charge your users.
Query parameters
endUserHandle— roll up costs for this end user.orgId— roll up costs for this org.after/before— optionalYYYY-MM-DDbounds (inclusive).
Exactly one of endUserHandle/orgId is required — both present or both absent is a 400. There is currently no whole-workspace rollup.
Response
{
"workspaceId": 42,
"scope": "USER",
"scopeKey": "user-42",
"totals": { "calls": 5, "tokensIn": 1000, "tokensOut": 200, "cachedTokens": 800, "costUsd": 0.123456 },
"byModel": [
{ "provider": "GOOGLE", "model": "gemini-3.1-flash-lite", "calls": 4, "tokensIn": 900, "tokensOut": 150, "cachedTokens": 800, "costUsd": 0.1 }
]
}POST/api/{workspaceKey}/upload
The CLI's push calls this endpoint. You normally don't talk to it directly — but it's documented so you can replicate the upload from a custom build script.
Request — multipart/form-data
manifest— JSON part describing the change set.sources[]— File parts for each source file referenced in the manifest.
Manifest shape
{
"workspaceKey": "abcdef",
"gitRef": "main",
"headSha": "abc123...",
"language": "java",
"changes": [
{
"op": "CREATE",
"classUniqueId": "com.example.SignupConfig",
"className": "SignupConfig",
"configEnd": "/signup",
"objectKind": "SETTING", // SETTING | LIST | USER_SETTING | USER_LIST | FACTS | TOOL_CLASS
"filePath": "src/main/java/com/example/SignupConfig.java",
"gitSha": "abc123..."
}
],
"files": [
{ "path": "src/main/java/com/example/SignupConfig.java", "sha": "abc123..." } // git blob SHA
]
// plus toolClasses[] — each @Confiqure.Tool class: name, FLOW Javadoc, operations
}Response
{
"totalClasses": 5,
"accepted": 4,
"rejected": 1,
"items": [
{
"classUniqueId": "com.example.SignupConfig",
"className": "SignupConfig",
"status": "ACCEPTED", // ACCEPTED | REJECTED | DELETED
"pushHistoryId": 12345,
"error": null
}
]
}GET/api/{workspaceKey}/upload/status/{pushHistoryId}
Poll this after upload to know when endpoint generation finishes. The CLI hits it once per second during its watch loop.
Response
{
"pushHistoryId": 12345,
"className": "SignupConfig",
"configEnd": "/signup",
"pushTime": "2026-05-13T17:30:00Z",
"playbookReady": true,
"tombstoned": false
}Next:
Callbacks
The outbound webhook your host app receives when a chat completes.
Error Codes
HTTP statuses you can expect from each endpoint.