# Call agents from your server

> Start a conversation with a Mindset agent from your own backend, send it follow-up messages, and make retries safe.

After this page your backend can start an agent working, with no browser and no user present, and send it follow-up messages in the same conversation. Retries won't start the work twice.

Use this when something in your system should set an agent going: a nightly job, a new support ticket, a payment that failed. To put an agent in front of a person in your web app, use [the browser SDK](https://docs4.mindset.ai/docs/sdk/how-embedding-works) instead.

## What this is

Two HTTP calls, made with your org API key:

| Call | What it does |
|---|---|
| `POST .../agents/{agent}/conversations` | Starts a new conversation with the agent and hands it a task |
| `POST .../agents/{agent}/conversations/{conversationId}/messages` | Sends another message into a conversation that already exists |

Both answer **202 Accepted** with the conversation's ID as soon as Mindset has stored your request. Neither returns the agent's reply. An agent run can take minutes, so Mindset accepts the work and runs it. Stored means it runs even if a Mindset server restarts.

To get the result somewhere, have the agent deliver it: give it a connection to your own HTTP API, to Slack, or to wherever the result belongs, and tell it in the task what to do with what it finds. The run is recorded in Mindset like any other run. See [Check what happened](https://docs4.mindset.ai/docs/ams/check-what-happened).

If you need the reply back in the same call, connect through MCP instead. See [Use your agents from Claude and other AI clients](https://docs4.mindset.ai/docs/ams/use-your-agents-from-claude-and-other-ai-clients).

## Before you start

- **Tick Outside systems on the agent.** In AMS, open the agent's availability and tick **Outside systems**, with the agent switched on. See [Where your agent can be reached](https://docs4.mindset.ai/docs/ams/where-your-agent-can-be-reached#choose-where-it-can-be-used).
- **An org API key for the right environment.** A key works only in the environment it was created in, and it can start every live agent in that environment. See [Members and API keys](https://docs4.mindset.ai/docs/ams/environments#members-and-api-keys).
- **The agent's handle, and your org and environment slugs.** The agent's **Triggering** tab has a **How to call using the API** link that shows both calls with your host, slugs and handle filled in.

The examples use the host `https://eu.mindset.ai`, the org `acme`, the environment `production` and an agent with the handle `billing-help`. Replace them with yours.

## Start a conversation

```
POST https://YOUR-MINDSET-HOST/api/v1/orgs/{orgSlug}/envs/{envSlug}/agents/{agent}/conversations
```

`{agent}` is the agent's handle, or its ID. The ID keeps working if someone renames the handle.

Send the key in the `x-api-key` header and the body as JSON. Every body field is optional, and an empty body (or no body) means "run this agent now".

| Field | Type | What it is |
|---|---|---|
| `task` | string | The brief for the agent, up to 8,000 characters. Leave it out when the agent's script drives the whole run |
| `scriptParams` | object | Start values for the agent's script. Send every parameter the script declares. An agent with no script takes none |
| `attribution` | object | Your own labels for this call, such as a ticket number. String values only, at least one key, up to 16 keys, keys up to 64 characters, values up to 256 characters |
| `idempotencyKey` | string | Up to 200 characters. See [make retries safe](#make-retries-safe) |

The body is strict. Any other field is a 400.

```bash
curl -sS -X POST \
  "https://eu.mindset.ai/api/v1/orgs/acme/envs/production/agents/billing-help/conversations" \
  -H "x-api-key: $MINDSET_ORG_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "task": "Invoice INV-4471 failed to charge. Check the customer account and post what you find to the billing channel.",
    "attribution": { "ticket": "BILL-1421" },
    "idempotencyKey": "bill-1421-start"
  }'
```

The answer:

```
HTTP/1.1 202 Accepted

{ "conversationId": "3f0c2b1e-8a5d-4c1e-9b7a-2d6f4e8c1a90" }
```

Store `conversationId` if you'll send the agent more messages.

## Send a follow-up message

```
POST https://YOUR-MINDSET-HOST/api/v1/orgs/{orgSlug}/envs/{envSlug}/agents/{agent}/conversations/{conversationId}/messages
```

`{conversationId}` is the ID the start call returned. It must be a UUID.

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

`scriptParams` belongs to the start call only. Sending it here is a 400.

```bash
curl -sS -X POST \
  "https://eu.mindset.ai/api/v1/orgs/acme/envs/production/agents/billing-help/conversations/3f0c2b1e-8a5d-4c1e-9b7a-2d6f4e8c1a90/messages" \
  -H "x-api-key: $MINDSET_ORG_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "task": "The customer has updated their card. Retry the charge.",
    "idempotencyKey": "bill-1421-card-updated"
  }'
```

It also answers 202 with `{ "conversationId": ... }`, the same ID.

## Make retries safe

Networks fail after a request has arrived. Without protection, a retry starts the agent a second time.

Send an `idempotencyKey`. Mindset runs the first call with a given key once. Every later call with the same key gets the original call's answer, the same `conversationId`, and runs nothing, even if the rest of the body is different. Calls without a key run every time.

- On the start call, a key belongs to one agent. The same key sent to two different agents starts two conversations.
- On the follow-up call, a key belongs to one conversation. The same key in two conversations is two messages.
- A call that's refused (a 400, 404 or 422) doesn't use up its key, so you can fix the request and retry with the same key.
- A replayed start call doesn't count against the rate limit.

Build the key from something your system already has, such as the ticket number and the step, so a retry naturally reuses it.

## Node example

A complete script for Node 18 or later. Save it as `start-agent.mjs` and run it with `MINDSET_ORG_API_KEY=... node start-agent.mjs`.

```javascript
const MINDSET_HOST = "https://eu.mindset.ai";
const ORG_SLUG = "acme";
const ENV_SLUG = "production";
const AGENT = "billing-help";
const API_KEY = process.env.MINDSET_ORG_API_KEY;

const base = `${MINDSET_HOST}/api/v1/orgs/${ORG_SLUG}/envs/${ENV_SLUG}/agents/${AGENT}`;

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": API_KEY, "content-type": "application/json" },
        body: JSON.stringify(body),
      });
    } catch (err) {
      // Network failure: safe to retry, because the body carries an idempotencyKey.
      console.warn(`Attempt ${attempt} failed: ${err.message}`);
      await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
      continue;
    }

    if (r.status === 202) return r.json();

    const error = await r.json().catch(() => ({}));
    if (r.status === 429) {
      const wait = Number(r.headers.get("retry-after") ?? "5");
      console.warn(`Rate limited. Waiting ${wait}s.`);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    if (r.status >= 500) {
      await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
      continue;
    }
    // 400, 401, 403, 404, 422: retrying the same request won't help.
    throw new Error(`Mindset refused the call: ${r.status} ${error.code ?? ""} ${error.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 customer account and post what you find to the billing channel.",
  attribution: { ticket },
  idempotencyKey: `${ticket}-start`,
});
console.log("Started conversation", conversationId);

await post(`${base}/conversations/${conversationId}/messages`, {
  task: "The customer has updated their card. Retry the charge.",
  idempotencyKey: `${ticket}-card-updated`,
});
console.log("Sent follow-up");
```

## Limits

- **60 new conversations a minute** per org. Over that, the start call answers 429 with a `Retry-After` header in seconds. Nothing was started, so retrying after that wait is a clean first attempt.
- **Follow-up messages aren't rate limited.** Messages to one conversation queue up and run in turn.
- **`task` is up to 8,000 characters** on both calls.

## What can go wrong

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

| Status | `code` | What happened |
|---|---|---|
| 400 | `validation_error` | An unknown body field, a body that isn't a JSON object, `attribution` that breaks its limits, a `conversationId` that isn't a UUID, or `scriptParams` on a follow-up |
| 401 | `unauthorized` | No `x-api-key` header |
| 403 | `agent_not_available_here` | **Outside systems** isn't ticked for this agent. The message says which box to tick |
| 404 | | The agent doesn't exist in this org and environment, is switched off, or is someone's personal agent; the conversation isn't reachable with this key; the org or environment slug doesn't match the key; or the key lacks admin scope. They all look the same on purpose |
| 422 | `params_invalid` | `scriptParams` don't match what the agent's script declares. The response names the keys that are missing or not declared. An agent with no script refuses any `scriptParams` |
| 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. Wait for `Retry-After` |

A 202 means the work was accepted. If the run then fails, that shows in the conversation in AMS, not in your HTTP response.

## Related

- [Where your agent can be reached](https://docs4.mindset.ai/docs/ams/where-your-agent-can-be-reached) for the **Outside systems** box.
- [Write a script](https://docs4.mindset.ai/docs/ams/write-a-script) for script parameters and values a script accepts from outside.
- [Create a session for your users](https://docs4.mindset.ai/docs/sdk/create-a-session-for-your-users) for the browser SDK's server call.
