Data API
The endpoints an app's browser code uses to read and change the database, files, runs, sessions, and approvals of its environment, and to search its Retrieval. The app's access rules filter every request.
On this page
Every path is on the app's own host under /_connic/, so requests carry the visitor's session and need no API key. Each request reads or changes the data of the app's environment and goes through the app's access rules: the Rule column below names the rule that applies. Data requests don't start agent runs.
Requests
- Only the app's own pages can use these paths. connic refuses requests from other sites with 403, judged by the
OriginandSec-Fetch-Siteheaders browsers send. - Every request other than GET, including reads such as
find, must send the app'sOriginandcontent-type: application/json. That includes requests without a body, such as cancelling a run or deleting a session. - Bodies are JSON objects up to 1 MiB. Unknown fields are rejected.
- Responses are JSON and never cached. Errors have the form
{"error": "..."}; see Errors. - On an app that requires sign-in, a request without a valid session gets 401. Send the visitor to
/_connic/login?return=<path>.
async function connic(path, body) {
const response = await fetch(`/_connic/${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body ?? {}),
});
if (response.status === 401) {
location.assign(`/_connic/login?return=${encodeURIComponent(location.pathname)}`);
}
const result = await response.json();
if (!response.ok) throw new Error(result.error);
return result;
}
const { documents, total } = await connic("db/tickets/find", {
filter: { status: "open" },
sort: { _created_at: -1 },
limit: 20,
});The examples on this page use this connic() helper.
The visitor
GET /_connic/me returns the visitor and the app. It needs no rule. On a public app, id is the browser's visitor ID, email and name are null, and signed_in is false.
{
"user": {
"id": "8d2f6c1a-4b7e-4f0a-9c3d-2e5b7a9f1c64",
"email": "ada@example.com",
"name": "Ada Lovelace",
"groups": ["support"],
"signed_in": true
},
"app": {"key": "portal", "name": "Support portal"}
}Database
| Request | Body or query | Response | Rule |
|---|---|---|---|
| POST /_connic/db/<collection>/find | {filter?, sort?, limit?, skip?, fields?} | {documents, count, total} | read |
| POST /_connic/db/<collection>/count | {filter?} | {count} | read |
| POST /_connic/db/<collection>/insert | {document} or {documents}, up to 50 | 201 {inserted, inserted_count} | insert |
| POST /_connic/db/<collection>/update | {filter, update} | {updated} | update |
| POST /_connic/db/<collection>/delete | {filter} | {deleted} | delete |
- Filters use the operators of the database tools:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$and,$or,$not,$nor,$exists,$contains,$elemMatch,$regex, and$near. The system fields are_id,_created_at, and_updated_at. findreturns 25 documents by default and up to 100 withlimit.sortmaps fields to1or-1,skippages through results, andfieldslists the fields to return.countis the number of documents returned,totalthe number of allowed documents that match.updateanddeleteneed a non-emptyfilter.updatesets top-level fields and removes those set tonull, like db_update.- Inserts get the
setfields of the rules.insertedlists the stored documents with their system fields.
Files
| Request | Body or query | Response | Rule |
|---|---|---|---|
| POST /_connic/files/list | {folder, filter?, limit?, cursor?} | {folder, folders, files, count, total, next_cursor} | read |
| POST /_connic/files/download | {path} | {url, expires_at} | read |
| POST /_connic/files/upload | {path, size, mime_type?, metadata?} | 201, see Upload a file | write |
| POST /_connic/files/update | {path, new_path?, metadata?} | The file | update |
| POST /_connic/files/delete | {path} | {deleted} | delete |
listshows one folder: the files the visitor can read and the subfolders that hold readable files; see Folders. It returns 25 files by default and up to 100; passnext_cursorascursorfor the next page. Filters take the database operators on_path,_folder,_name,_size,_mime_type,_created_at,_updated_at, and metadata keys.updatemoves or renames a file withnew_pathand mergesmetadatainto the file's metadata;nullremoves a key. Moving onto a path that is taken fails.- Files have the fields described in Files.
Upload a file
Uploads go in parts, up to 1 GiB per file. The write rule checks the path, size, MIME type, and metadata before the upload starts.
POST /_connic/files/uploadwith the path and size, and optionallymime_typeandmetadata. Withoutmime_type, connic guesses it from the file name.Send each part with
PUT <upload_url>/parts/<n>?token=<token>, numbered from 1. Every part except the last is exactlypart_byteslong.Finish with
POST <upload_url>/complete?token=<token>. The response is the stored file. To give up instead, sendDELETE <upload_url>?token=<token>.
{
"upload_id": "5f1d8a2c-7b3e-4c9a-a1d4-0e6b2f8c9d31",
"path": "attachments/screenshot.png",
"size": 48213,
"part_bytes": 16777216,
"part_count": 1,
"upload_url": "/_connic/files/transfer/uploads/5f1d8a2c-7b3e-4c9a-a1d4-0e6b2f8c9d31",
"token": "...",
"expires_at": "2026-10-29T10:30:00+00:00"
}const file = input.files[0];
const upload = await connic("files/upload", {
path: `attachments/${file.name}`,
size: file.size,
metadata: { ticket_id: ticketId },
});
const token = encodeURIComponent(upload.token);
for (let part = 1; part <= upload.part_count; part++) {
const start = (part - 1) * upload.part_bytes;
await fetch(`${upload.upload_url}/parts/${part}?token=${token}`, {
method: "PUT",
body: file.slice(start, start + upload.part_bytes),
});
}
const stored = await fetch(`${upload.upload_url}/complete?token=${token}`, { method: "POST" })
.then((response) => response.json());upload_url is a path on the app's host, so no Content-Security-Policy change is needed. Part requests need a Content-Length header, which browsers set for Blob bodies, can be up to 17 MiB, and must come from the app's origin. The upload expires at expires_at. An upload never replaces a file: when the path is taken, the file is stored under a numbered name, such as screenshot (2).png.
Download a file
POST /_connic/files/download returns a link on the app's host, /_connic/files/transfer/download?token=<token>, that works until expires_at. Use it as a link target or fetch it; it supports range requests. It works from the app's own pages, bookmarks, and typed addresses, and answers 403 when another site opens it.
On an app that requires sign-in, transfers under /_connic/files/transfer/ need the visitor's session too.
Retrieval
| Request | Body or query | Response | Rule |
|---|---|---|---|
| POST /_connic/retrieval/search | {namespace, query, limit?, min_score?} | {results}, best match first | search |
const { results } = await connic("retrieval/search", {
namespace: "help",
query: "How do I change my invoice address?",
limit: 5,
});{
"results": [
{
"entry_id": "invoice-address",
"namespace": "help.billing",
"content": "To change the invoice address, open Settings and choose Billing...",
"score": 0.81,
"metadata": {"title": "Change your invoice address", "url": "https://help.example.com/billing/invoice-address"}
}
]
}namespacemust be a namespace listed in access.retrieval or one below it. The search covers that namespace and every namespace below it, such ashelp.billingwhen searchinghelp.queryis natural language of up to 1,000 characters.limitis 5 by default and up to 20.min_score, from 0 to 1 and 0.3 by default, leaves out weaker matches.- Each result is one matching passage of an entry:
contentis its text,scoreits relevance from 0 to 1, andmetadatathe entry's metadata, such as a title or a URL to link to. - Visitors who may search a namespace can read the content and metadata of every entry in it. Keep content that not every visitor may see in a namespace the app doesn't list.
- For search as you type, wait until the visitor pauses before sending a search; see Limits.
Runs
| Request | Body or query | Response | Rule |
|---|---|---|---|
| GET /_connic/runs | ?agent, ?status, ?limit, ?cursor | {runs, next_cursor}, newest first | read |
| GET /_connic/runs/stats | ?agent, ?days (1 to 90, default 7) | {days, total, statuses, average_duration_ms, daily} | read |
| GET /_connic/runs/<id> | The run | read | |
| POST /_connic/runs/<id>/cancel | No body | The run | cancel |
{
"id": "3c7e9a10-2f4b-4d8e-b6a1-9e0c5d2f7a48",
"agent": "support",
"status": "completed",
"input": "{\"message\": \"Where is my order?\"}",
"output": "Your order shipped on Monday.",
"error": null,
"context": {"app": {"id": "…", "key": "portal"}, "end_user": {"id": "…", "signed_in": true}},
"queued_at": "2026-10-29T09:12:03.120000Z",
"started_at": "2026-10-29T09:12:03.410000Z",
"ended_at": "2026-10-29T09:12:05.260000Z",
"duration_ms": 1850
}- The list returns 25 runs by default and up to 100 with
limit; passnext_cursorascursorfor older runs.statusfilters by a run status such ascompleted. inputandoutputare the stored text. Parse them when they hold JSON.- Stats cover runs queued in the last
daysUTC days:statusescounts runs per status, anddailylists{day, runs}for each day.
Sessions
| Request | Body or query | Response | Rule |
|---|---|---|---|
| GET /_connic/sessions/<id> | {session, events} | read | |
| DELETE /_connic/sessions/<id> | No body | {"deleted": true} | delete |
A session is the conversation history of an agent with a persistent session. Its ID is session-<agent>-<key>, where key is the value of the agent's session.key, such as session-support-42 for conversation 42. session has id, agent, state, created_at, and updated_at; events lists the stored conversation events.
Approvals
| Request | Body or query | Response | Rule |
|---|---|---|---|
| GET /_connic/approvals | ?status: pending, approved, rejected, or timeout | {approvals}, pending first, up to 100 | read |
| POST /_connic/approvals/<id>/decide | {approved, message?, response?} | The approval | decide |
{
"id": "a8b1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"run_id": "3c7e9a10-2f4b-4d8e-b6a1-9e0c5d2f7a48",
"agent": "billing",
"tool": "issue_refund",
"params": {"order_id": "A-1042", "amount": 120},
"message": "Refund 120 EUR for order A-1042?",
"response_label": null,
"status": "pending",
"created_at": "2026-10-29T09:14:00.000000Z",
"resolved_at": null,
"reason": null
}approved is true or false, and message is an optional reason. Approvals that ask for input, shown by a response_label, also need response. connic records the visitor as the person who decided, by email, by name, or by the app's name on public apps. See Approvals.
Errors
| Status | Meaning |
|---|---|
| 400 | The body or query is invalid, with the field in the message, such as {"error": "limit: Input should be less than or equal to 100"}. Also unknown fields, a filter the database rejects, and an update of a set field. |
| 401 | {"error": "Sign-in required"}: the app requires sign-in and the visitor has no valid session. Send them to /_connic/login. |
| 403 | {"error": "Access denied"}: the rules don't allow the operation. {"error": "Only same-origin JSON requests are allowed"}: the request came from another site, or a request other than GET came without content-type: application/json. File transfers answer {"error": "Only same-origin requests are allowed"}. |
| 404 | The run, file, session, or approval doesn't exist or the rules don't allow it, such as {"error": "Run not found"}. {"error": "App not found"} when the app no longer serves. |
| 405 | Wrong method for the path. The body is {"detail": "Method Not Allowed"}. |
| 411 | A file transfer with a body but without Content-Length. |
| 413 | The body is larger than 1 MiB, or a file transfer request is larger than 17 MiB. |
| 429 | {"error": "Too many requests. Try again in a minute."} See Limits. |
| 500 | An access rule made a database filter that is too large for this visitor; see How rules apply. |
| 502 | connic couldn't complete the request. Retry. |
Limits
- Each visitor can send 300 reads, 60 writes, and 30 Retrieval searches per minute. Signed-in visitors count by account, public visitors by IP address.
- Each app can receive 6,000 reads, 1,200 writes, and 600 Retrieval searches per minute across all visitors.
- Lists return up to 100 entries per request, inserts take up to 50 documents, and run stats cover up to 90 days.
Writes appear in the project's audit log with the app and the visitor's ID. Each request counts in the app's stats as a data request, and a denied one also as a denied request.