---
title: "How do I make API requests from a Plugin?"
section: "Web Navigator"
route: /faqapirequestsinplugins
account: {accountId}
bruce_api: https://{accountId}.api.nextspace.host
guardian_api: https://guardian.nextspace.host
---
# How do I make API requests from a Plugin?

A Plugin never installs or imports anything. Navigator hands it the already-authenticated libraries through `pluginParams`, so a request from inside a Plugin is the same call you would write in a web app, minus the setup.

Two params matter here. `BModels` is the whole `bruce-models` namespace, which is where the per-record utilities live. `getters` is an `ApiGetters` instance already pointed at the current account, environment and session, and it is how you reach a raw API instance for anything the library has no utility for.

## Using the library namespaces

Prefer a namespace over a hand-rolled request. Every namespace resolves the API instance itself, caches consistently with the rest of Navigator, and returns typed records, so a Plugin reading an Entity sees exactly what the Info View sees.

Note that every namespace function takes an optional `api`. Omitting it is correct inside Navigator: the library falls back to the same instance the app is already using. Pass one only when you deliberately want a different account or environment.

**Entity**

```javascript
async function Run(params) {
    const { BModels, visualRegister } = params.pluginParams;
    const { Entity, EntityType } = BModels;

    const selectedIds = visualRegister.GetSelected();
    if (!selectedIds.length) {
        return () => {};
    }

    // One Entity, with its Type expanded in the same request.
    const { entity } = await Entity.Get({
        entityId: selectedIds[0],
        expandEntityType: true,
        expandAttachments: true
    });

    // Bruce holds the system attributes, Data holds the schema attributes.
    console.log("ID", entity.Bruce.ID);
    console.log("Type", entity.Bruce["EntityType.ID"]);

    // Reads a nested attribute without hand-walking the object.
    const status = Entity.GetValue({
        entity: entity,
        path: "Data.Condition.Status"
    });
    console.log("Status", status);

    // The display name Navigator itself would show.
    const { entityType } = await EntityType.Get({
        entityTypeId: entity.Bruce["EntityType.ID"]
    });
    console.log("Name", Entity.CalculateName({
        entity: entity,
        type: entityType,
        defaultToId: true
    }));

    // Writing back. Only the attributes you supply are changed
    // because override is false.
    Entity.SetValue({
        entity: entity,
        path: "Data.Condition.Status",
        value: "INSPECTED"
    });
    await Entity.Update({
        entity: entity,
        override: false
    });

    // Tells Navigator the record moved, so the scene and panels reread it.
    params.pluginParams.refreshData({
        entityIds: [entity.Bruce.ID]
    });

    return () => {};
}
```

**Entity list**

```javascript
async function Run(params) {
    const { Entity } = params.pluginParams.BModels;

    const ENTITY_TYPE_ID = params.plugin.Settings?.entityTypeId;

    // A count first, so a Plugin can refuse a selection that is too
    // large before it transfers any of it.
    const { totalCount } = await Entity.GetList({
        analysis: true,
        filter: {
            entityTypeId: ENTITY_TYPE_ID
        }
    });
    console.log(totalCount + " Entities of this Type");

    // Paged read. getNextPage is handed back on each response, so
    // you never construct the next request yourself.
    let page = await Entity.GetList({
        filter: {
            entityTypeId: ENTITY_TYPE_ID,
            pageSize: 500,
            pageIndex: 0,
            // Attribute conditions. Index the attribute for this to be fast.
            entityTypeConditions: {
                "Data.Condition.Status": "FAULTY"
            }
        }
    });

    const all = [];
    while (page) {
        all.push(...(page.entities ? page.entities : []));
        page = page.nextPage ? await page.getNextPage() : null;
    }
    console.log("Read " + all.length + " Entities");

    // Restricting to the ground area the camera currently covers.
    const area = await params.pluginParams.BEngine.ViewGroundArea.GetViewArea(
        params.pluginParams.viewer
    );
    const visible = await Entity.GetList({
        filter: {
            entityTypeId: ENTITY_TYPE_ID,
            bounds: area.bounds
        }
    });
    console.log("Visible", visible.entities?.length);

    return () => {};
}
```

**Entity Type**

```javascript
async function Run(params) {
    const { EntityType } = params.pluginParams.BModels;

    // Every Type in the account. Settings, which carry the data schema,
    // are stripped by default to keep the payload small, so ask for them
    // only when you intend to read the schema.
    const { entityTypes } = await EntityType.GetList({
        expandSettings: true
    });

    entityTypes.forEach((type) => {
        console.log(type.ID, type.Name);
    });

    // Types under one parent, which is how you walk an ontology branch.
    const { entityTypes: children } = await EntityType.GetList({
        parentTypeId: entityTypes[0].ID
    });
    console.log(children.length + " child Types");

    // A single Type, and how many Entities it holds.
    const { entityType } = await EntityType.Get({
        entityTypeId: entityTypes[0].ID
    });
    const count = await EntityType.Count({
        entityTypeId: entityType.ID
    });
    console.log(entityType.Name + " holds " + count);

    return () => {};
}
```

## Custom requests

Not every endpoint has a namespace utility, and a new endpoint always lands in the API before it lands in the library. For those, take the API instance directly and call the verb yourself. This is the same instance every namespace uses underneath, so the account header, session header, base URL and cache are already correct.

`getters.GetBruceApi()` returns the Bruce API for the current account. Paths are appended to the API's base URL and take no leading slash. The base URL already carries the version root, so most paths are bare, for example `entitytypes` or `entity/{id}`. Some newer endpoints are addressed under an explicit `v3/` prefix; the Swagger reference is the place to confirm which.

**Raw requests**

```javascript
async function Run(params) {
    const { getters } = params.pluginParams;

    // The Bruce API for the account Navigator is currently viewing.
    const api = getters.GetBruceApi();

    // GET. The second argument is the request params object, where
    // urlParams becomes the query string.
    const types = await api.GET("entitytypes", {
        urlParams: {
            "expandSettings": "yes"
        }
    });
    console.log(types);

    // A path that has no namespace utility yet.
    const links = await api.GET("entity/SOME_ENTITY_ID/links");
    console.log(links);

    // POST with a JSON body.
    await api.POST("entity/SOME_ENTITY_ID", {
        Data: {
            Note: "Written by a Plugin"
        }
    });

    // PUT and DELETE take the same shape.
    // await api.PUT("some.endpoint", body);
    // await api.DELETE("some.endpoint");

    return () => {};
}
```

**Non-library requests**

```javascript
async function Run(params) {
    const { getters } = params.pluginParams;
    const api = getters.GetBruceApi();
    await api.Loading;

    // Where you need fetch itself, for a streamed response, a binary
    // body, or an endpoint that does not answer JSON, build the URL and
    // the session header off the API instead of assembling either
    // by hand. ConstructUrl carries the account, which the base URL
    // may hold as a query param rather than a path.
    const url = api.ConstructUrl({
        url: "entities/ExportCSV",
        urlParams: {
            "entityTypeId": "SOME_ENTITY_TYPE_ID"
        }
    });

    const headers = {};
    headers[api.GetSessionHeader()] = api.GetSessionId();

    const res = await fetch(url, {
        headers: headers
    });
    const csv = await res.text();
    console.log(csv.length + " bytes of CSV");

    return () => {};
}
```

**Other APIs**

```javascript
async function Run(params) {
    const { getters, session, account } = params.pluginParams;

    // Guardian, for users, sessions and permissions.
    const guardian = getters.GetGuardianApi();

    // Bruce Global, for cross-account surfaces.
    const global = getters.GetGlobalApi();

    // Another account, where a Plugin deliberately reads across one.
    // The session must have access to it.
    const other = getters.GetBruceApi({
        accountId: "SOME_OTHER_ACCOUNT_ID"
    });

    // The current context, without a request.
    console.log("Account", account?.Name);
    console.log("Session user", session?.["User.ID"]);
    console.log("Account ID", getters.GetAccountId());

    return () => {};
}
```

## Caching

Namespace reads are cached, which is what keeps Navigator responsive when several surfaces ask for the same record. A Plugin that has just written a record, or that is polling something that changes underneath it, needs to say so.

```javascript
const { BModels, getters } = params.pluginParams;

// Bypasses the cache for this one read.
const { entity } = await BModels.Entity.Get({
    entityId: "SOME_ENTITY_ID",
    req: {
        noCache: true
    }
});

// Drops a record from the cache, so every later reader refetches it.
const api = getters.GetBruceApi();
api.Cache.Remove(BModels.Entity.GetCacheKey({
    entityId: "SOME_ENTITY_ID"
}));
```

Namespaces that write, such as `Entity.Update`, already clear what they invalidate. Clear the cache yourself only after a raw `POST` that the library does not know changed anything.

---

Urls on this page are resolved for account `{accountId}`.
Site index: https://docs.nextspace.host/llms.txt · whole site in one file: https://docs.nextspace.host/llms-full.txt
Human-readable version of this page: https://docs.nextspace.host/faqapirequestsinplugins
