curl --request POST \
--url https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"message": "Also bump the timeout to 30s while you're in there.",
"client_message_id": "follow-up-018f6f3a"
}
EOFimport requests
url = "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages"
payload = {
"message": "Also bump the timeout to 30s while you're in there.",
"client_message_id": "follow-up-018f6f3a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Also bump the timeout to 30s while you\'re in there.',
client_message_id: 'follow-up-018f6f3a'
})
};
fetch('https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Also bump the timeout to 30s while you\'re in there.',
'client_message_id' => 'follow-up-018f6f3a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages"
payload := strings.NewReader("{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}"
response = http.request(request)
puts response.read_body{
"ok": true,
"reason_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"session_id": "<string>",
"conversation_created": true,
"delivery": "<string>",
"continuation_session_id": "<string>",
"message_id": "<string>",
"client_message_id": "<string>",
"held_reason": "<string>"
}{
"error": "missing_bearer",
"message": "Authorization required"
}{
"error": "session_not_found"
}{
"error": {
"type": "rate_limited",
"message": "This API key exceeded its request-rate limit"
}
}Message a Reason
Messaging a Reason is the primary way to give it work. The message goes into the Reason’s long-running conversation, which this call opens the first time (as openReasonConversation does); the Reason then decides how to do the work: it answers, starts Sessions, or sets up Loops. Sessions it starts are filed under it.
The message is delivered exactly as sendSessionMessage delivers a follow-up to the conversation Session: the same body, admission, usage and safeguards. A message sent while the Reason is working steers its running turn (or waits for the next one with delivery queue). The call returns once the message is accepted, not when the Reason answers: poll listReasonMessages with the last end_cursor for its reply, or follow the Session (session_id) with getSession and listSessionEvents. Send a client_message_id to make retries safe: resending with it returns the accepted message instead of adding another. Anyone the Reason is shared with may message it; each turn acts as the person who sent it.
runcurl --request POST \
--url https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"message": "Also bump the timeout to 30s while you're in there.",
"client_message_id": "follow-up-018f6f3a"
}
EOFimport requests
url = "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages"
payload = {
"message": "Also bump the timeout to 30s while you're in there.",
"client_message_id": "follow-up-018f6f3a"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Also bump the timeout to 30s while you\'re in there.',
client_message_id: 'follow-up-018f6f3a'
})
};
fetch('https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Also bump the timeout to 30s while you\'re in there.',
'client_message_id' => 'follow-up-018f6f3a'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages"
payload := strings.NewReader("{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reasonmachines.com/v3/organizations/{orgId}/reasons/{reasonId}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Also bump the timeout to 30s while you're in there.\",\n \"client_message_id\": \"follow-up-018f6f3a\"\n}"
response = http.request(request)
puts response.read_body{
"ok": true,
"reason_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"session_id": "<string>",
"conversation_created": true,
"delivery": "<string>",
"continuation_session_id": "<string>",
"message_id": "<string>",
"client_message_id": "<string>",
"held_reason": "<string>"
}{
"error": "missing_bearer",
"message": "Authorization required"
}{
"error": "session_not_found"
}{
"error": {
"type": "rate_limited",
"message": "This API key exceeded its request-rate limit"
}
}Authorizations
Your Reason API key from Settings › API keys. New keys use reason_; legacy ara_ keys remain accepted. Keys are capability-scoped: run, mcp:read, mcp:write, secrets:read, secrets:write, sessions:read, sessions:debug, knowledge:read, memory:read, memory:write, skills:read, skills:write, repos:read, repos:write, reviews:read, reviews:write, deployment:read, analytics:read, org:read, org:write, attachments:read, attachments:write, guardrails:read, guardrails:write, network:read, network:write, automations:read, automations:write, agent_auth:read. network:read and network:write are opt-in, excluded from default keys. network:read reads the saved workspace network policy; network:write replaces custom allowed hosts and requires a current organization owner/admin. Legacy project PATCH requests containing network_allowed_hosts also require network:write plus their existing run scope and project management permission; saved project hosts do not affect current session egress. Only owners/admins can mint or manage keys with network:write; keys cannot grant themselves more authority. See /docs/network-policies for replacement semantics and runtime applicability. mcp:write manages MCP server configuration only; it does not authorize remote MCP-tool execution. sessions:read reads sessions, including the assistant, reasoning and tool activity in their events. sessions:debug is privileged: only for organization owners/admins, it expands session events to the full diagnostic projection (diagnostic event kinds, status text and raw event metadata).
Headers
Optional self-reported caller agent name. If absent, the server uses the User-Agent for external callers.
80Path Parameters
Organization id or slug. Resolve it with GET /v3/self.
The Reason id, returned as id by listReasons and createReason.
Body
The follow-up instruction for the agent. It reaches the running turn or starts the next one; the response's delivery says which.
Up to 10 attachments; maximum 64 MiB per file and 80 MiB total decoded bytes. The endpoint's request-body limit also applies to the encoded payload.
- Option 1
- Option 2
Show child attributes
Show child attributes
Caller-generated idempotency key. Reusing it for the same Session returns the original accepted turn.
1 - 160Optional model id from GET /agent-auth/models to retry this session on. A model override always starts a new continuation run, because a run's model is fixed at its first execution. Omit or use auto to keep the session's current model.
How much reasoning the model should use for the follow-up turn. Higher effort trades time and token usage for more deliberation; supported levels depend on the selected model. Omit to retain the Session's effort setting.
minimal, low, medium, high, xhigh, max Same as the create field, for this and later turns: the follow-up goes on with the provider's other accounts, then Reason, when the Session's subscription or key runs out. A Session that already rolls over keeps doing so.
While a turn runs: steer (the default) hands the message to the running turn; queue holds it until the turn ends and then runs it as the next one. A queued message can be sent at once or withdrawn with its message_id (POST .../messages/{messageId}/steer, DELETE .../messages/{messageId}/queued). Not combinable with target.
steer, queue Moves the Session: this message's turn, and every later one, runs on this target instead of the one the Session was on. Same shape and admission as the create target (cloud, a Device and root, or a named Machine). Only the conversation moves; files on the previous machine do not, unless they were pushed to the Session branch. A move starts a new continuation run, so the Session must have finished its current turn. Any target other than cloud needs the devices:use scope.
- Option 1
- Option 2
- Option 3
- Option 4
Show child attributes
Show child attributes
Response
Message accepted and delivered or queued.
True: the message was accepted.
The Reason the message went to.
The Reason's conversation Session the message was added to. getSession and listSessionEvents read it; listReasonMessages reads the replies.
True when this message opened the Reason's conversation for the first time.
How the message was delivered: steered_live (into the running turn), queued_for_next_turn or held_in_queue (waits for the turn to end), started_continuation_run or reattached_warm_session (started a new turn), applied_to_queued_run, saved_for_follow_up (saved; see held_reason), queue_cancelled or continuation_blocked.
The stable logical Session id after an accepted follow-up. Continue polling this id; Reason may run the turn in a hidden execution Attempt.
Set for a message sent with delivery queue: the id to send it at once or withdraw it while it waits.
The message's idempotency key: the client_message_id sent, or the one the server gave it. Resending the message with it returns this accepted turn instead of adding another.
Present when delivery is saved_for_follow_up and the message is waiting instead of starting. session_machine_transition_pending: the Session's machine is changing; the message is delivered when the change settles, so do not send it again. If the change has not settled after 10 minutes the message is marked not delivered instead.

