Skip to content
GET /toolbox

Toolboxes the caller owns plus every toolbox shared with them, each with the caller's `access_level`, PLUS the two virtual dynamic rows (`global:{user_id}`, one `connection:{connection_id}` per active usable connection) appended to the LAST page only (the one whose `next_cursor` is null), always `access_level: "owner"` — so the order across the whole listing is stored toolboxes first, automatic ones after. There is no org-wide oversight listing for toolboxes and no `?visibility=` parameter: no organization permission — not even `toolbox:view`/`toolbox:manage` — widens this beyond what the caller owns or was granted; a private toolbox never appears here for anyone but its owner and its grantees. `?q=` matches the name as a substring. `?type=stored` drops the dynamic rows entirely — for a caller, such as a toolbox picker, that only wants ACL-backed rows and would otherwise have to filter the dynamic ones out itself. `?connector_slug=` narrows to the toolboxes holding a tool from any of the named connectors; take the slugs worth passing from `GET /toolbox/connector`, not from the whole catalog. The three filters combine by AND. Cursor-paginated.

Query Parameters

qstring

Substring match (case-insensitive) on the toolbox name. LIKE wildcards are matched literally. Max 200 characters.

typestring

stored restricts the listing to ACL-backed toolboxes, omitting the dynamic global:…/connection:… rows; automatic is the mirror, paging the dynamic rows alone. Unset returns both, stored first. Any other value is a 400.

Possible values:
storedautomatic
connector_slugstring[]

Filter to the toolboxes holding at least one tool from ANY of these connectors — a union, never an intersection. Repeat the parameter or send one comma-separated value; both are the same filter, spelled exactly as GET /connection?connector_slug= is. At most 25 distinct values per request; more is a 400, never a silent truncation. A dynamic row is matched on the connectors it really spans: a connection:{id} row on its own connector, the global:{user_id} row when any of the caller's active usable connections matches. GET /toolbox/connector is where the values come from.

Response Body

next_cursorstring,null
prev_cursorstring,null
resultobject[]
access_levelstring

The caller's access: owner, or a granted view/use/edit level. Always owner for a dynamic row. Never oversight — toolboxes have no org-wide oversight fallback.

access_summaryobject

Present on a GET /toolbox stored row exactly when that row's can_see_shares is true — the caller owns it, holds edit, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a view/use grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read can_see_shares to distinguish "not permitted" from "not carried"; never infer it from this field's absence.

5 properties
org_levelstring,null

Level of the org-wide grant, or null when there is none.

Possible values:
viewuseeditnull
previewobject[]

At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.

4 properties
grantee_idstring,null
grantee_typestring
Possible values:
userteamorg
levelstring
Possible values:
viewuseedit
namestring,null

Display name; null for org grants and for grantees no longer in the org.

team_countinteger
totalinteger

Every grant, the org-wide one included.

user_countinteger
access_viastring

How the CALLER reaches this toolbox — not who else can. owner — they own it. direct — a grant naming them personally. team — a grant to a team they belong to (or, per §6.2, one they administer), named in access_via_team. org — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else org, else team, else direct. So a caller granted edit personally AND view org-wide reads org — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on can_see_shares, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a view/use grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means "no source to name", never "not permitted", and never an implied org.

Possible values:
ownerdirectteamorg
access_via_teamobject

Present exactly when access_via is team, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.

2 properties
idstring

Team id (team_…).

namestring,null

Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries.

can_manageboolean

Whether the caller may edit this toolbox's own settings — owner, or edit access. No toolbox:manage fallback. Mirrors PATCH /toolbox/{id}.

can_revoke_shareboolean

can_share, verbatim — a THIRD formula, distinct from can_manage: an edit grantee whose role omits toolbox:share can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors DELETE /toolbox/{id}/share/{aclId}.

can_shareboolean

Whether the caller may share this toolbox — owner, edit access, or toolbox:share. No toolbox:manage fallback. Mirrors POST /toolbox/{id}/share's own check.

can_transferboolean

Whether the caller may transfer or delete this toolbox — ownership, full stop. No toolbox:manage fallback. Mirrors POST /toolbox/{id}/transfer and DELETE /toolbox/{id}.

can_useboolean

Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a use-gated action of their own.

connected_app_summaryobject

The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on GET /toolbox/{id} for a stored toolbox, and only when the caller may see the ACL (the same gate access_summary uses). Never the full set: page GET /toolbox/{id}/connected-app for that.

2 properties
countinteger
previewobject[]

At most 5 apps, for a hover preview.

5 properties
client_idstring
client_namestring,null

Null when the OAuth client row is gone.

grant_idstring

OAuth grant id (ogrt_…) — what DELETE /oauth/grant/{id} takes, scoped to the grant's own user.

userobject
3 properties
emailstring · email
idstring

User id (usr_…).

namestring

Omitted along with email when the id no longer resolves to an org member.

viastring

toolbox: this toolbox is named explicitly on the grant. all_tools: an "All my tools" authorization by a user who can currently use this toolbox — computed live, not a stored fact.

Possible values:
toolboxall_tools
connection_idstring

Present only for type: "connection" dynamic rows.

connector_slugstring

Present only for type: "connection" dynamic rows.

connectorsobject

Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every GET /toolbox row: a stored row summarises its own entries, a connection:{id} row is that connection's one connector, and a global:{user_id} row summarises the connections it spans. Present on every command response too (POST /toolbox, GET /toolbox/{id}, PATCH /toolbox/{id}, POST /toolbox/{id}/transfer), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. total: 0 means no connector-backed tools (empty, or synthetic-only), never "not computed".

2 properties
previewobject[]

At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up GET /connector/{slug} to make, and a page of rows costs no per-row catalog lookup.

3 properties
logostring,null

The connector's square icon when it has one, else its wordmark logo, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from name when it is null.

namestring

Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw.

slugstring

Connector slug — connectors are keyed by slug, not by id.

totalinteger

Distinct connector slugs across the toolbox.

created_atstring,null · date-time

Null for dynamic rows, which are computed, never stored.

descriptionstring,null
entry_countinteger

Omitted for dynamic rows, which have no stored entries of their own.

idstring

Toolbox id — a stored row (tbx_…), or a dynamic id (global:{user_id} / connection:{connection_id}).

namestring
needs_connection_countinteger

Cheap per-page SQL aggregate over PROXY entries with connection_id IS NULL — drives the console's Status column ("Ready" / "N need connection"). Excludes synthetic entries, which pin no connection_id of their own by design and have no connection picker to fix. Omitted for dynamic rows (nothing to bind). Broken delegation (delegation_ok: false) only surfaces in detail, inside entries[].

ownerobject,null

Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.

3 properties
emailstring · email
idstring

User id (usr_…) — same value as owner_user_id.

namestring,null
owner_user_idstring

Creator (usr_…). For a dynamic row, the caller.

readonlyboolean

True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable.

sharesobject[]

A bounded preview of the ACL (same cap as access_summary), present only on GET /toolbox/{id} when the caller may see the ACL. Never on a list row (GET /toolbox) — page GET /toolbox/{id}/share for the full, cursor-paginated grant list.

8 properties
created_atstring · date-time
grantee_idstring,null

User id (usr_…) or team id (team_…). Null for a grantee_type: "org" grant.

grantee_typestring

user and team grants target one grantee_id; org applies to every member of the organization and takes no id.

Possible values:
userteamorg
idstring

ACL entry id — the :aclId a DELETE .../share/{aclId} call takes.

levelstring

Same ladder for every shareable resource, low to high. view — see it exists, read its metadata/config; cannot exercise it. use — view + exercise it, resource-specific: run tools through a connection; execute a toolbox's tools through its bound connections (this DELEGATES — the caller runs through each entry's pinning editor's own authority, not necessarily their own); stamp a new toolbox by copying a template's entries; create connections from a connector. edit — use + change its settings, entries and its own grants.

Possible values:
viewuseedit
resource_idstring

The shared resource's id (a connector's slug, for that type).

resource_typestring
Possible values:
connectiontoolboxtemplateconnector
updated_atstring · date-time
template_idstring,null

Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted.

typestring

stored = a real ACL-backed row. global/connection = computed per request, always readonly: true.

Possible values:
storedglobalconnection
updated_atstring,null · date-time
curl -X GET 'https://api.elaichi.ai/toolbox' \
  -H 'Authorization: Bearer $ELAICHI_API_TOKEN' \
  -H 'Content-Type: application/json'
const response = await fetch('https://api.elaichi.ai/toolbox', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer ' + process.env.ELAICHI_API_TOKEN,
    'Content-Type': 'application/json',
  },
});

const data = await response.json();
console.log(data);
import os
import requests

url = "https://api.elaichi.ai/toolbox"
headers = {
    "Authorization": f"Bearer {os.environ['ELAICHI_API_TOKEN']}",
    "Content-Type": "application/json",
}

response = requests.get(url, headers=headers)
print(response.json())