Skip to main content
connic
Apps

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.

Last updated

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 Origin and Sec-Fetch-Site headers browsers send.
  • Every request other than GET, including reads such as find, must send the app's Origin and content-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>.
connic.js
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.

GET /_connic/me
{
  "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

RequestBody or queryResponseRule
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 50201 {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.
  • find returns 25 documents by default and up to 100 with limit. sort maps fields to 1 or -1, skip pages through results, and fields lists the fields to return. count is the number of documents returned, total the number of allowed documents that match.
  • update and delete need a non-empty filter. update sets top-level fields and removes those set to null, like db_update.
  • Inserts get the set fields of the rules. inserted lists the stored documents with their system fields.

Files

RequestBody or queryResponseRule
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 filewrite
POST /_connic/files/update{path, new_path?, metadata?}The fileupdate
POST /_connic/files/delete{path}{deleted}delete
  • list shows 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; pass next_cursor as cursor for the next page. Filters take the database operators on _path, _folder, _name, _size, _mime_type, _created_at, _updated_at, and metadata keys.
  • update moves or renames a file with new_path and merges metadata into the file's metadata; null removes 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.

  1. POST /_connic/files/upload with the path and size, and optionally mime_type and metadata. Without mime_type, connic guesses it from the file name.

  2. Send each part with PUT <upload_url>/parts/<n>?token=<token>, numbered from 1. Every part except the last is exactly part_bytes long.

  3. Finish with POST <upload_url>/complete?token=<token>. The response is the stored file. To give up instead, send DELETE <upload_url>?token=<token>.

POST /_connic/files/upload (201)
{
  "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"
}
upload.js
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

RequestBody or queryResponseRule
POST /_connic/retrieval/search{namespace, query, limit?, min_score?}{results}, best match firstsearch
search.js
const { results } = await connic("retrieval/search", {
  namespace: "help",
  query: "How do I change my invoice address?",
  limit: 5,
});
POST /_connic/retrieval/search
{
  "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"}
    }
  ]
}
  • namespace must be a namespace listed in access.retrieval or one below it. The search covers that namespace and every namespace below it, such as help.billing when searching help.
  • query is natural language of up to 1,000 characters. limit is 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: content is its text, score its relevance from 0 to 1, and metadata the 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

RequestBody or queryResponseRule
GET /_connic/runs?agent, ?status, ?limit, ?cursor{runs, next_cursor}, newest firstread
GET /_connic/runs/stats?agent, ?days (1 to 90, default 7){days, total, statuses, average_duration_ms, daily}read
GET /_connic/runs/<id>The runread
POST /_connic/runs/<id>/cancelNo bodyThe runcancel
run
{
  "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; pass next_cursor as cursor for older runs. status filters by a run status such as completed.
  • input and output are the stored text. Parse them when they hold JSON.
  • Stats cover runs queued in the last days UTC days: statuses counts runs per status, and daily lists {day, runs} for each day.

Sessions

RequestBody or queryResponseRule
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

RequestBody or queryResponseRule
GET /_connic/approvals?status: pending, approved, rejected, or timeout{approvals}, pending first, up to 100read
POST /_connic/approvals/<id>/decide{approved, message?, response?}The approvaldecide
approval
{
  "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

StatusMeaning
400The 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"}.
404The 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.
405Wrong method for the path. The body is {"detail": "Method Not Allowed"}.
411A file transfer with a body but without Content-Length.
413The 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.
500An access rule made a database filter that is too large for this visitor; see How rules apply.
502connic 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.