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/chatwithout aconversation_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 }outcomeis"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 return202with the run's details; the run resumes right away.remember: trueonly goes with"approved": further calls to that same tool of the same connector in this conversation are not asked again. Any other combination (rememberwith"denied", or over an API key) returns422 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
selectedor non-emptyothertext.
- Who may call it: the token of the end-user who owns the conversation, or
your organization's API key (
X-API-Keyfor the server-to-server channel). - When valid, returns
202 Acceptedwith the run details; the system automatically records arole=usermessage 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 Acceptedwith 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-Keyreturns 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.