Harnix
Integrate

Idempotency & cancel

Idempotency-Key, cancelling a run, confirming an action, answering questions, resume semantics.

Idempotency-Key

Send this on every message-sending call (POST /v2/chat, POST /v2/conversations/{id}/messages):

Idempotency-Key: 2f6a9c31-...

255 characters max, generated by you (a UUID is a reasonable choice). Safe to retry after a network timeout, or whenever you're not sure the previous request landed:

  • Same key, same conversation, same end-user → returns the message already created on the first call, doesn't create a second one, doesn't re-run the run.
  • Same key but a different end-user (e.g. one end-user accidentally reuses a key you already used) → 422 validation_error — it never returns someone else's message.
  • For POST /v2/chat without a conversation_id: retrying with the same key lands back on the same conversation it auto-created the first time, instead of creating a new one on every retry.

Skipping Idempotency-Key still works fine — you just lose retry safety (a retry with no key can create two messages).

Cancelling a run

POST https://api.harnix.vn/v2/runs/{run_id}/cancel
Authorization: Bearer <token>

202 Accepted — { "status": "cancelling" }. This is a request, not an instant event: the run stops at the next safe checkpoint (e.g. once a currently-running tool call finishes), not arbitrarily mid-step. Follow the run's stream to know exactly when it ends — the final frame will be error with message "cancelled".

Cancelling a run that's already finished (succeeded, failed, or already cancelled) returns run_not_cancellable (409). A run waiting for an action to be confirmed or waiting for questions to be answered (see below) can be cancelled like a running one.

Confirming an action

When the agent wants to call a connector tool that needs confirmation, the run pauses and you receive an approval frame on the stream (or pending_approval when reading the conversation back). Answer with:

POST https://api.harnix.vn/v2/runs/{run_id}/approval
Authorization: Bearer <token>
Content-Type: application/json

{ "outcome": "approved", "remember": false }
  • outcome is "approved" (run the tool with exactly the arguments shown) or "denied" (don't run it; the tool gets an error result and the assistant tells the user the action was not carried out). Both return 202 with the run's details; the run resumes right away.
  • remember: true only goes with "approved": further calls to that same tool of the same connector in this conversation are not asked again. Any other combination (remember with "denied", or over an API key) returns 422 validation_error.
  • Who may call it: the token of the end-user who owns the conversation, or your organization's API key (X-API-Key, for the server-to-server channel — which decides for itself and has no "remember").
  • A run that isn't waiting (already decided, finished, or never paused) returns run_not_waiting (409) — including a second call for the same run, so a double click does no harm. A run that doesn't exist or isn't yours: run_not_found (404).

Not answering within the waiting window (5 minutes by default, see Limits) counts as declining. Every decision — who, when, outcome — shows on the run's trace in the console.

Answering questions (ask_user)

When the agent calls the built-in ask_user tool to ask the user questions, the run pauses in waiting_input and you receive a question frame on the stream (or pending_question when reading the conversation back).

You have two ways to answer:

Option 1: Call the /answer endpoint

Submit structured answers matching the questions:

POST https://api.harnix.vn/v2/runs/{run_id}/answer
Authorization: Bearer <token>
Content-Type: application/json

{
  "answers": [
    {
      "selected": ["District 3"],
      "other": null
    }
  ]
}
  • answers: an array of answer items with length matching the number of questions asked (1–4). Each item has:
    • selected: a list of selected option labels (single-choice allows at most 1 label; multi-choice allows 0–4 unique labels matching the question's options).
    • other: optional free text string when the user chooses "Other" or writes custom text (up to 2,000 characters after whitespace trimming).
    • Each item must have at least one selection in selected or non-empty other text.
  • Who may call it: the token of the end-user who owns the conversation, or your organization's API key (X-API-Key for the server-to-server channel).
  • When valid, returns 202 Accepted with the run details; the system automatically records a role=user message in the conversation formatted with the answer content, and the run resumes immediately.
  • A run not waiting for input (already answered, finished, or waiting for approval) returns run_not_waiting (409) — a double call does no harm. A run that doesn't exist or isn't yours: run_not_found (404). Answers that fail validation against question options: validation_error (422).

Option 2: Send a follow-up message into the conversation

While a question is waiting, the user can type directly into the chat (POST /v2/conversations/{id}/messages or POST /v2/chat). The system treats this message as the user's free-text reply to the question:

  • Returns 202 Accepted with the same waiting run (no new run created, not blocked by active run limits).
  • Appends the message to the conversation and resumes the run.
  • Retrying with the same Idempotency-Key returns the same run without writing a duplicate message.

Question expiry

If not answered within the waiting window (10 minutes by default, configured via ASK_USER_TIMEOUT_SECONDS, see Limits): the question expires. The run resumes, the model is notified that the question expired so it can proceed with available context, and the run succeeds normally — no extra user message is written on expiry.

Resume semantics (when a run is retried)

A run interrupted mid-flight (a worker restart, a transient infrastructure error) can be automatically retried by the engine from where it left off, rather than starting over. This matters for how you think about a side-effecting tool call:

A tool can be called again, at most once

A tool that already has a durable result is never called again on resume. A tool that had no result yet when the interruption happened (still running, or its result didn't get written in time) may be called again — accept this as a known cost, not a promise that the tool is itself idempotent. If you're building an MCP server with side effects (sending an email, creating an order...), add your own idempotency key on your server's side for that action.

An action the user confirmed also runs once: the decision is recorded before the tool runs, so when a run resumes after an interruption Harnix neither asks again nor re-runs a tool that already has a result.

This doesn't change anything about the public endpoints above — it's internal engine behavior, mentioned here because it determines how safe it is for a tool you configure through an MCP server.

Next

  • Streaming — watch a run change state live, including when it's cancelled.
  • Tools — configuring MCP servers.
  • Runs & traces — inspect a run step by step (including a resumed one); this content is removed per your organization's trace retention setting.

On this page