m4Mindset docs

Docs / SDK / Build / Call agents from your server

View as Markdown

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 instead.

What this is

Two HTTP calls, made with your org API key:

CallWhat it does
POST .../agents/{agent}/conversationsStarts a new conversation with the agent and hands it a task
POST .../agents/{agent}/conversations/{conversationId}/messagesSends 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.

If you need the reply back in the same call, connect through MCP instead. See 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.
  • 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.
  • 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

text
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".

FieldTypeWhat it is
taskstringThe brief for the agent, up to 8,000 characters. Leave it out when the agent's script drives the whole run
scriptParamsobjectStart values for the agent's script. Send every parameter the script declares. An agent with no script takes none
attributionobjectYour 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
idempotencyKeystringUp to 200 characters. See 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:

text
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

text
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.

FieldTypeWhat it is
taskstringThis message, up to 8,000 characters. The agent sees the earlier exchange too
scriptValuesobjectValues for keys the agent's running script declares it accepts from outside. Send only the keys you have
attributionobjectYour labels, as on the start call. If you leave it out, the labels from earlier in the conversation are used
idempotencyKeystringAs 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.

StatuscodeWhat happened
400validation_errorAn 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
401unauthorizedNo x-api-key header
403agent_not_available_hereOutside systems isn't ticked for this agent. The message says which box to tick
404The 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
422params_invalidscriptParams 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
422invalid_write_targetA scriptValues key the running script doesn't accept. The response names it
429rate_limitedOver 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.