API and keys
Every Apps feature is an HTTP API on the same origin as inference: records, actions, files and deploys, each scoped to one app and environment.
Who can call
| Credential | Reaches | Get it |
|---|---|---|
| A key linked to the app | That app's files (pull and push), records and actions, in both environments. Not deploys. | Link an existing project key on the app's Settings → API keys. |
| A key with Apps Permissions | Every app of the team: create apps, list them, sync, deploy, records and actions. | Tick Apps Permissions when creating a key on a project's API Keys page. |
| A CLI sign-in | The same as Apps Permissions, as you, while you're a member of the team. | ib-apps login, or an MCP client's OAuth sign-in. |
| An environment token | One environment's records, actions and Shopify calls. Not files, not deploys. | Set in the app's code as IB_ENVIRONMENT_TOKEN. |
Send it as Authorization: Bearer …. A key that isn't linked to an app gets the same 404 as for an app that doesn't exist, so app names can't be probed. Changes are recorded under the key (key:<id>), the person (user:<id>) or the environment (env:<id>).
Production's records are the live app's data, and a linked key reads and writes them too. Give CI a key linked only to the apps it syncs.
Environment tokens
Each environment has a token (ibenv_…), derived from a server secret rather than stored. Inference Bureau puts it in the environment's code as IB_ENVIRONMENT_TOKEN, with IB_API_URL, IB_APP and IB_ENVIRONMENT, which is all createClient() needs. It also signs the calls Inference Bureau makes to the app: action runs (/_ib/actions) and app proxy requests. Production gets it at its next build. The dashboard shows it among the system variables, hidden until revealed.
Embeddings for writes made with it are billed to the team's Apps key (see Vector fields).
Records and actions
Under /v1/apps/{app}/environments/{env}, where {env} is development or production:
| Route | Does |
|---|---|
GET /models | Every model with its fields, and problems for schemas that can't be read. |
GET /models/{model} | One model's fields and its record count. |
GET /models/{model}/records | A page of records. filter, sort and a vector search are JSON in the query string, and a text search is plain words; first, after or skip (records to pass over, up to 10,000), vectors=full to include vector fields, and select (JSON) for chosen fields and related records. |
POST /models/{model}/records/query | The same, with the options as a JSON body. |
POST /models/{model}/records/aggregate | Counts, sums, averages, least and most over the records, in groups: { groupBy?, aggregates, filter?, search?, sort?, first?, timeZone? }. Answers { groups, hasMore }. See Aggregates. |
POST /models/{model}/records | Create from field values. Runs the model's create action when it has one. 201 with the record. |
GET /models/{model}/records/{id} | One record, with vectors when vectors=full; select as for a page. |
PATCH /models/{model}/records/{id} | Change the given fields. Runs update when it exists. |
DELETE /models/{model}/records/{id} | Delete. Runs delete when it exists. 204. |
POST /models/{model}/records/upsert | Update the record whose on field (or fields) hold the values' values, or create one: { on, values }. Answers { record, created }, 201 when created. Runs update or create when they exist. |
POST /models/{model}/bulk | Up to 250 records in one call: { op: "create", records: [values] }, { op: "update", records: [{ id, values }] }, { op: "delete", ids } or { op: "upsert", on, records: [values] }. Each goes the way a single write goes; direct writes share one transaction, each rolled back alone when refused. Answers 200 with { records, failed, counts } (deleted for a delete): failed has each refused record's index, id and error. See Bulk writes and upserts. |
GET /models/{model}/export.csv | Every record as CSV, in id order, or those matching filter (JSON) by sort. See Import and export CSV. |
POST /models/{model}/import | A CSV into the model: a text/csv body with the options in the query string, or { csv, on?, actions?, apply?, fileName? }. on is id (the default when the file has an id column), a unique field, or null to create every row. Without apply it answers the plan: { columns, rows, creates, updates, problemRows, problems, matchable, on }. With it, 202 with the import run; actions queues the model's create and update actions for each row. Not for a call scoped to a store or role. |
GET /models/{model}/runs/{id} | An import run (or a cleanup or backfill) with its state, counts (created, updated, failed) and the first failures by row. |
POST /models/{model}/records/{id}/check-password | Whether { password, field? } matches the record's password field (password unless named): { valid }. A missing record answers false. |
POST /models/{model}/uploads | A direct upload for a file field: { field, fileName?, mimeType?, size? }. 201 with { url, method, headers, token, expiresAt }: PUT the file to url with headers within the hour, then write { directUploadToken: token } to the field within a day. |
POST /models/{model}/actions/{action} | Run a model action: { id?, params?, shop? }. Answers { record, result?, event }. |
POST /changes | Which models had records created, updated or deleted since a cursor, for live queries: { models, cursor?, wait? }. Waits up to wait seconds (25 at most) for a change, then answers { cursor, models }, models empty when none came; ask again with the new cursor. Without a cursor it answers at once with one. Held to the call's store and role as reads are. The SDK's api.$changes. |
POST /actions/{action} | Run a global action: { params?, shop? }. Answers { result?, event }. |
POST /jobs | Queue an action as a background job: { action, id?, params?, shop?, delay?, runAt?, retries?, priority?, concurrencyKey?, concurrency? }, action a global action's name or model.action. 201 with the job. The SDK's api.enqueue. |
POST /jobs/bulk | Queue up to 250 jobs for one action: { action, jobs: [{ id?, params? }], ...options }, the options shared. Every job is checked before any is queued. 201 with { jobs } in order. The SDK's api.enqueueMany. |
GET /jobs | The queue, newest first, with counts by state: state (queued, running, succeeded, failed or canceled) and limit (100 by default, up to 500). |
GET /jobs/{id} | One job. |
POST /jobs/{id}/cancel | Cancel a queued job. |
POST /jobs/{id}/retry | Run a failed or canceled job once more, or a queued one now. |
Keys, environment tokens and owners' or admins' sign-ins queue, cancel and retry jobs; a member's sign-in only reads the queue.
curl -X POST https://api.inferencebureau.com/v1/apps/reviews/environments/production/models/review/records/query \
-H "Authorization: Bearer $IB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter": { "status": { "equals": "pending" } },
"sort": { "createdAt": "Ascending" },
"first": 50
}'Bodies are JSON, at most 4 MiB (MCP messages, 512 KiB). Filters, sorting, pages and search work as in the client, which wraps these routes.
Your app's reference
Every environment has an API reference worked out from its files, the same one the console's API Reference page shows and the MCP server's get_reference returns: each model with its fields, their types, the filter operators each takes, whether it sorts and joins text search, and its description; who may read it; each action with its params (as written and as JSON Schema), whether it works on a record, who may call it, and the webhook or schedule that queues it.
| Route | Answers |
|---|---|
GET /v1/apps/{app}/environments/{env}/reference | The reference as JSON. |
GET /v1/apps/{app}/environments/{env}/reference.md | The reference as Markdown, for a README or an agent's context. |
GET /v1/apps/{app}/environments/{env}/openapi.json | The environment's API as OpenAPI 3.1: the records routes per model, every action, the record and input schemas, and the headers above. Import it into Zapier, n8n, ChatGPT actions or openapi-typescript. |
A description on a field, a schema, a param or an action's options is what the reference shows for it (see Field options and Action options). Without one, the reference still describes types, filters and grants.
Apps, files and deploys
These are what ib-apps uses. Environment tokens can't call them.
| Route | Does |
|---|---|
GET /v1/apps | The team's apps (only the linked ones for a linked key). |
POST /v1/apps | Create an app: { name, platform: "shopify" | "web", distribution?, polaris?, starter?, recipe? }, where distribution is custom (the default) or public for a public Shopify app, polaris: true starts a Shopify app's admin pages in Polaris web components, starter is what a web app begins as, members (the default) or site (see the starter files), and recipe starts the app from one of the example apps by its id, such as reviews; a recipe decides the distribution, Polaris and starter itself. Needs Apps Permissions or a sign-in. |
PATCH /v1/apps/{app} | Change a Shopify app's distribution, until production is first installed on a store. Needs Apps Permissions or a sign-in. |
GET /v1/apps/{app}/environments/{env}/files?since=N | Files changed since version N (0 for all), with the current version. |
POST /v1/apps/{app}/environments/{env}/files | Push changes: { changes: [{ op: "put" | "delete", path, base, content?, encoding? }] }. base is the hash you last saw; a mismatch comes back as a conflict, not an overwrite. Development only. |
POST /v1/apps/{app}/deploy | Deploy development to production; the build runs after the answer (202). Needs Apps Permissions or a sign-in. |
GET /v1/apps/{app}/deploy | Where production stands: its version, the version deployed, and the newest build's state. |
Shopify
| Route | Does |
|---|---|
POST /v1/apps/{app}/environments/{env}/shopify/graphql | An Admin GraphQL call for an installed store: { shop, query, variables?, priority?, maxRetries? }. Throttled calls are tried again as Shopify says to (maxRetries, default 5); priority: "background" also waits for half the store's bucket first. Shopify's answer comes back as it came. The SDK's shopify client. |
POST /v1/apps/{app}/environments/{env}/shopify/session | Check an admin session token: { token, refresh? } (refresh reads the store's grant again, after the merchant granted scopes). Answers { shop, userId, sessionId, preview?, scopes } or 401 invalid_session. A store Shopify installed that isn't recorded yet is recorded from the token. The SDK's verifyShopifySession. |
Bulk metafield edits
A metafield run changes many metafields on one installed store, up to 250,000 of them, and can be undone. Give it a CSV (owner,metafield,value[,type]), a JSON list of changes, or a filter with values; it plans first (current and new values, each checked against the store's metafield definitions), then applies in batches of 25, recording each change so undo can restore the previous values. Routes: GET and POST /v1/apps/{app}/environments/{env}/metafield-runs, GET …/{id}, GET …/{id}/changes, POST …/{id}/apply and POST …/{id}/undo. Only keys with Apps Permissions and CLI sign-ins can use them.
- Big runs. A filter, a CSV by URL (
csvUrl, an https link to up to 100 MB, for a CSV bigger than the 4.5 MB a request can carry) or more than 2,500 changes is planned after the answer:POSTanswers202with the runplanning, andGET …/{id}follows it toplanned(orfailed, witherror). - More than 2,500 changes are applied and undone with Shopify's bulk mutations, outside the store's rate limit, and current values for more than 1,000 owners of a type are read with a bulk query. While Shopify works on it, the run reads
applying(orundoing) withbulknaming the step. It carries on by itself until it's done, and each change still records whether it landed. - A run answers with at most 5,000 of its changes;
totalsays how many it has.GET …/{id}/changes?offset=&limit=&state=pages through them (at most 5,000 at a time), or only thefailedones, say.
Errors
Errors use the API's usual shape: { error: { type, code, message, request_id } }, plus problems for a refused write and event and logs for a failed action. See the status codes.