---
name: mindset-server-api
description: Use when a backend, job or worker (no browser) should start a Mindset (mindset.ai) AI agent. Covers starting a conversation over HTTP with an org API key, sending follow-up messages, making retries safe with an idempotencyKey, handling 429 and other errors, having the agent deliver its result, and starting the agent from another product's webhook through a webhook source.
metadata:
  title: Call agents from your server
  blurb: Starts an agent from your backend, a job or a webhook, with no browser involved.
  group: In your app
  order: 4
  docs: start-it-from-your-server, runs, build-an-agent
  checked: d671041b6
  updated: 2026-10-19
---

# Start Mindset agents from your server

When you finish, the app's backend starts a Mindset agent working with no browser and no user present, sends it follow-up messages in the same conversation, and retries without ever starting the work twice. The agent delivers its result through a connection. To put an agent in front of a signed-in person instead, use the `mindset-embed` skill.

## Mindset in brief

Mindset is a hosted platform for building, running and governing AI agents. An **agent** is configuration, not code: a prompt, a model, and the resources it may use. Those resources are **scripts** (steps that must wait or happen in order), **functions** (fixed steps with one right answer), **widgets** and **connections** (the only way data enters or leaves). People manage all of it in the Studio, a web app, or through Mindset's MCP servers.

## Where you are building: host, org, environment

Every Mindset call names three things. Get all three from the person before you write code or call a tool. Never guess or invent them.

| Value | What it is | Where the person finds it |
|---|---|---|
| **Host** | The Mindset deployment their org lives on, for example `https://eu.mindset.ai` | The address bar when they're signed in to the Studio, or an agent's **Embed** tab |
| **Org slug** | Their company's account. The org is the isolation boundary: members, model keys, settings and audit sit here, and one org can never see into another | An agent's **Embed** tab, or the address under **Settings → Systems → Connect an AI client** |
| **Environment slug** | A named partition inside the org, such as Sandbox, Test or Production | An agent's **Embed** tab, or the same address. The **Environment** switcher in the Studio shows which one is current |

How environments behave:

- **Agents, scripts, functions, connections, triggers, schedules, conversations, runs and API keys each belong to exactly one environment** and can't see anything in another.
- **Every org starts with one environment, Sandbox, whose slug is `original`.** A slug never changes, even when the environment is renamed. Use the slug, never the display name.
- **Nothing copies between environments.** To promote a change from Test to Production, make the same change again in Production.
- **Don't create an environment to hold your work.** Environments can't be deleted, and they're for stages such as Test and Production, not for projects. Work in the environment the person names. Create one only if the person asks you to.
- **Members, model keys and org settings are shared by every environment.** Changing an org setting changes it everywhere.

## Credentials for code in an app: the org API key

Your app's backend authenticates to Mindset with an **org API key**.

- **An org admin creates it in the Studio under Settings → API keys.** It's shown once, when it's created. Ask the person to put it in their secret manager or environment. Never ask them to paste it to you.
- **A key works only in the environment it was created in.** Test code needs a Test key and production code needs a Production key.
- **It's admin-grade. It stays on the server.** Read it from an environment variable. Never put it in frontend code, a response body, a log line or a committed file.
- **Send it in the `x-api-key` header**, not in `Authorization`.

Keep one set of values per deployment stage, in the app's configuration:

```sh
MINDSET_HOST=https://eu.mindset.ai   # the person's host
MINDSET_ORG_SLUG=acme
MINDSET_ENV_SLUG=test                # the environment this deployment talks to
MINDSET_ORG_API_KEY=                 # secret: a key created in that same environment
```

A request with no `x-api-key` header gets `401`. A wrong org slug, a wrong environment slug, an agent that isn't in that environment, and a key without admin scope all return `404` `not_found`. The message says whether the org, the environment or the agent didn't resolve. An org or environment the key doesn't belong to reads exactly like one that doesn't exist, so nobody holding a key can probe for others. When you get a `404` during setup, check all four against the agent's **Embed** tab before anything else.

## Who creates what

Your code is only the caller. Everything it calls is set up in Mindset first, by a person in the Studio or by an agent on the `mindset-admin` MCP server.

| Piece | Who creates it, and where | What your code does with it |
|---|---|---|
| The agent, with **Outside systems** ticked | An org admin, in the Studio under the agent's **Versions & Availability** tab | Names its handle in the URL |
| The org API key | An org admin, in the Studio under **Settings → API keys**. No MCP tool creates one, and a key can't create keys | Reads it from the environment and sends it in `x-api-key` |
| The connection the agent delivers its result through | An org admin, in the Studio or through `mindset-admin` | Nothing. If the result goes to your own HTTP API, you build that endpoint |
| A webhook source and its binding to the agent | An org admin, in the Studio on the agent's **Triggering** tab, or through `mindset-admin` | Nothing. The vendor's product posts to it |

There's no "API trigger" to register for your own software. Your backend posts straight at the agent.

## Ask the person for

- The host, org slug and environment slug for each deployment stage, and which environment each one talks to.
- The **agent handle**, for example `billing-help`. The agent's **Triggering** tab has a **How to call using the API** link that shows both calls with the host, slugs and handle filled in.
- Confirmation that the agent is switched on, has an active version, and has **Outside systems** ticked under **Where this agent can be used**. New agents start switched off. On the admin MCP server, `get_agent` shows that box as `availability.api`, and `available: true` means the agent can be reached. Its `status` gates nothing unless it reads `archived`.
- That an org API key exists for each environment and is in the app's secret store.
- Where the result should end up (your own API, Slack, a ticket) and that the agent has a connection to it. On the admin MCP server, `agent_config_admin` verb `describe_resources` lists what it holds. An agent that holds nothing can only answer in its own conversation, and every call still returns `202`.
- Whether the agent runs a script, and if so which start parameters it declares.

## Step 1: start a conversation

```
POST <host>/api/v1/orgs/<org-slug>/envs/<environment-slug>/agents/<agent>/conversations
x-api-key: <org API key>
content-type: application/json
```

`<agent>` is the handle or the agent's ID. The ID keeps working if someone renames the handle. Every body field is optional. No body, or an empty one, means "run this agent now".

| Field | What it is |
|---|---|
| `task` | The brief, up to 8,000 characters. Leave it out when the agent's script drives the whole run. Say what to do with the result, for example "post what you find to the billing channel" |
| `scriptParams` | Start values for the agent's script. Send every parameter the script declares. An agent with no script takes none |
| `attribution` | Your own string labels, such as a ticket number: at least one key, up to 16, keys up to 64 characters, values up to 256 |
| `idempotencyKey` | Up to 200 characters. See step 3 |

The body is strict: any other field is a `400`. A body that isn't valid JSON, or is JSON but not an object, is also a `400`, never treated as empty.

The answer is `202 Accepted` with `{ "conversationId": "<uuid>" }` as soon as Mindset has stored the request. Stored means it runs even if a Mindset server restarts. Store `conversationId` if you'll send more messages.

## Step 2: send a follow-up

```
POST <host>/api/v1/orgs/<org-slug>/envs/<environment-slug>/agents/<agent>/conversations/<conversationId>/messages
```

Same headers. `conversationId` must be the UUID the start call returned.

| Field | What it is |
|---|---|
| `task` | This message, up to 8,000 characters. The agent sees the earlier exchange too |
| `scriptValues` | Values for keys the running script declares it accepts from outside. Send only the keys you have |
| `attribution` | As on the start call. Leave it out and the conversation's earlier labels are used |
| `idempotencyKey` | As on the start call, scoped to this conversation |

`scriptParams` here is a `400`: start values belong to the start call only. The answer is `202` with the same `conversationId`. There's no "busy" refusal: a follow-up sent while the agent is still answering is accepted, and the agent answers it too.

## Step 3: make retries safe

A network failure can happen after Mindset has already accepted the request. Always send an `idempotencyKey`, built from something your system already has, such as `billing:${ticket}:start` or `billing:${ticket}:card-updated`, so a retry reuses it.

- The first call with a key runs once. Every later call with the same key gets the first call's answer and runs nothing, even if the rest of the body differs. Calls without a key run every time.
- **A key never expires.** It's remembered for as long as the org exists. Build it from what stays the same when a crashed job re-runs and changes for new work: an invoice and its billing date, never the time the job ran. Test with a different prefix from the live job's, or the test uses up the live keys.
- On the start call a key belongs to one agent, shared by every system that calls it, so begin your keys with your job's name. On the follow-up call it belongs to one conversation.
- Most refusals don't use up the key: `401`, `400`, `404`, `409`, `422`, `429` and `403 agent_not_available_here`. Fix the request and retry with the same key. A follow-up refused with `403 forbidden` does use it up. After a `5xx` or a dropped connection, retry with the same key: whether or not the first call landed, the work runs once.
- A replayed start call doesn't count against the rate limit. `Retry-After` is always whole seconds.

A Node 18 helper that retries network failures, `5xx` and `429`, and throws on any other refusal:

```javascript
const base = `${process.env.MINDSET_HOST}/api/v1/orgs/${process.env.MINDSET_ORG_SLUG}/envs/${process.env.MINDSET_ENV_SLUG}/agents/billing-help`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function post(url, body) {
  for (let attempt = 1; attempt <= 5; attempt++) {
    let r;
    try {
      r = await fetch(url, {
        method: "POST",
        headers: { "x-api-key": process.env.MINDSET_ORG_API_KEY, "content-type": "application/json" },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30_000),
      });
    } catch {
      await sleep(1000 * attempt); // a timeout or a dropped connection: safe to retry, the body carries an idempotencyKey
      continue;
    }
    if (r.status === 202) return r.json();
    if (r.status === 429) { await sleep(Number(r.headers.get("retry-after") ?? "5") * 1000); continue; }
    if (r.status >= 500) { await sleep(1000 * attempt); continue; }
    const e = await r.json().catch(() => ({}));
    throw new Error(`Mindset refused: ${r.status} ${e.code ?? ""} ${e.message ?? ""}`);
  }
  throw new Error("Mindset did not accept the call after 5 attempts");
}

const ticket = "BILL-1421";
const { conversationId } = await post(`${base}/conversations`, {
  task: "Invoice INV-4471 failed to charge. Check the account and post what you find to the billing channel.",
  attribution: { ticket },
  idempotencyKey: `billing:${ticket}:start`,
});
await post(`${base}/conversations/${conversationId}/messages`, {
  task: "The customer has updated their card. Retry the charge.",
  idempotencyKey: `billing:${ticket}:card-updated`,
});
```

## Step 4: get the result

**Neither call returns the agent's reply, and no HTTP endpoint reads a conversation's reply or transcript with an org API key.** Don't look for one, and don't poll.

- **Have the agent deliver it.** Give the agent a connection to wherever the result belongs (your own HTTP API, Slack, a ticketing system) and say in `task` what to do with what it finds. If it calls your own API, build that endpoint and pass your reference (the ticket number) in the task so the agent sends it back.
- **People read the run in the Studio** under **Log**. Your `attribution` labels are recorded on it. Runs started through the API don't appear in a schedule's **Recent runs** table.
- **An org admin's AI client reads it on the `mindset-admin` MCP server**: `conversation_fetch` with noun `message` and the `conversationId` in `filters`, or `get_run` with the conversation ID for the run's trace.
- **If the caller must wait for the answer**, this API is the wrong tool. A person or an AI client gets replies through the `mindset` MCP server's `call_agent` (see the `mindset-mcp` skill).

## Step 5 (optional): start the agent from another product's webhook

Use this when a system that can't hold your API key (a CRM, a ticketing tool, a payments product) should start the agent. Your own software should use steps 1 to 3 instead.

An org admin sets it up, in the Studio (**Triggering** tab, **Let another product start this agent**, **Set up an inbound source**) or through `mindset-admin` (a connection with the `webhook_source` adapter, then a binding added with `update_agent`). In order:

1. Create the source with the vendor's signing secret only. The secret goes in through a capture link, never through you or the conversation.
2. Give the agent a script that declares a `payload` parameter, unless the source names another. The vendor's event body arrives there. A webhook binding on an agent with no script is refused, and the binding must supply every other parameter the script declares.
3. Bind the agent to the source and activate the version that holds the binding. A binding staged in a saved version doesn't run.
4. Tick **Triggers** on the agent. That box, not **Outside systems**, admits webhook deliveries.
5. Only then copy the delivery address from the source and paste it into the vendor's console:
   `<host>/api/v1/orgs/<org-slug>/envs/<environment-slug>/webhooks/<agent-handle>/<token>`. Copy it whole. Never assemble it by hand.

How deliveries behave:

- **The first delivery teaches** Mindset how the vendor signs and starts nothing. Every later delivery is checked against the secret and starts a run. Don't type in a signing scheme up front.
- **Every refusal before the signature check is a bare `401` with no body.** The vendor's dashboard shows no reason. The reason is in the source's delivery list in the Studio, or `list_webhook_deliveries` on `mindset-admin`.
- **An accepted delivery answers `202`**, even a duplicate. A delivery from the verified vendor that can't be used still gets a `2xx`, so the vendor doesn't switch the subscription off.
- **A vendor that verifies the address before saving it** must be able to reach the host. A local or undeployed environment can't complete that check.

## Rules

- The org API key stays on the server. Never put it in a browser, a mobile app, a vendor's console, a log or a committed file.
- A key reaches every live agent with **Outside systems** ticked in its environment. Use a Test key against Test and a Production key against Production.
- Always send an `idempotencyKey` on both calls.
- Never retry a `400`, `401`, `403`, `404`, `409` or `422` unchanged. Retry network failures, `5xx`, and `429` after `Retry-After` seconds.
- A `202` means accepted, not done. A run that fails later shows in the Studio, not in your HTTP response.
- An org runs at most 5 API or webhook runs at once (the default). Extra calls still get `202`; their runs start later, which can take several minutes.
- A run that follows a script carries on by itself: Mindset gives it another turn after it stops at a limit, and resumes it when a person answers or a wait runs out. An agent with no script takes one turn per message. Post another message to the same conversation to give it another.
- Put each item of work in its own conversation (one per ticket or invoice) rather than one huge run.

## Check it works

1. Start a conversation with the request shape above and a test `task`. Expect `202` and a `conversationId`.
2. Send the same request again with the same `idempotencyKey`. Expect `202` and the same `conversationId`, and only one run in **Log**.
3. Send a follow-up to that `conversationId`. Expect `202` and the same ID.
4. Confirm the result arrived where the agent was told to deliver it.
5. For a webhook source: trigger one event in the vendor (it teaches), trigger a second (it runs), and read the source's delivery list.

## When it fails

Every error body is JSON with `code`, `message` and sometimes `context`.

| What you see | Cause and fix |
|---|---|
| `400` `validation_error` | An unknown body field, a body that isn't a JSON object, `attribution` over its limits, a `conversationId` that isn't a UUID, or `scriptParams` on a follow-up. `context` says which |
| `401` `unauthorized` | No org API key on the request. Send it in `x-api-key` |
| `401` `invalid_api_key` | The key is revoked, expired or unknown. An admin issues a new one in the same environment |
| `403` `agent_not_available_here` | **Outside systems** isn't ticked for the agent. The message names the box |
| `403` `forbidden` | On a follow-up: the `conversationId` belongs to another environment or to a person's own chat. Send follow-ups only to IDs your start call returned. This refusal uses up the `idempotencyKey` |
| `404` `not_found` | Wrong org or environment slug for the key, an agent that doesn't exist there or is switched off or personal, or a key without admin scope. The message says whether the org, the environment or the agent didn't resolve |
| `409` `resource_not_published` | The agent's script names a resource with no published version. The message names the phase and the resource. Publish it, then retry |
| `422` `resource_not_granted` | The agent's script names a tool or resource the agent doesn't hold. The message names the phase and the resource. An admin grants it to the agent or changes the script |
| `422` `params_invalid` | `scriptParams` don't match the script's declared parameters. The response names missing and undeclared keys |
| `422` `invalid_write_target` | A `scriptValues` key the running script doesn't accept. The response names it |
| `429` `rate_limited` | Over 60 new conversations a minute in the org (the default). Schedule and webhook starts count too. Nothing started. Wait `Retry-After` seconds and retry the same request. Follow-ups aren't rate limited |
| `202` but nothing arrives | The run failed, is waiting, or is queued behind the org's 5 running calls. Look in the Studio under **Log**. Check the agent has an active version, then check its delivery connection |
| Webhook: vendor shows a failure with no reason | Read the source's delivery list. Each refused row names the reason, for example no signing secret captured, no agent bound, a stale address or the source switched off |
| Webhook: deliveries arrive but no run starts | Check the version holding the binding is active, **Triggers** is ticked on the agent, and the agent's script declares the `payload` parameter. Remember the first delivery only teaches |
| Webhook: nothing in the delivery list at all | The address is wrong or names another environment, or the vendor never sent. Recopy the address, then check the vendor's own settings |

## Read more

Fetch these as Markdown when you need more than this skill gives you. If one disagrees with this skill, the page is newer: follow it.

- https://docs.mindset.ai/docs/start-it-from-your-server.md: read for the request, the limits and error codes, and setting up a schedule or a webhook source.
- https://docs.mindset.ai/docs/runs.md: read before sizing a job, and for what happens when a run stops part way.
- https://docs.mindset.ai/docs/build-an-agent.md: read when the agent refuses calls because of its availability boxes.
- https://docs.mindset.ai/docs/check-what-happened.md: read to find a run's record in the Studio.
