curl --request POST \
--url https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Build host",
"host": "host.example.com",
"username": "ubuntu",
"authentication": {
"type": "private_key",
"private_key": "<load from your secret manager>"
},
"consent_host_access": true,
"idempotency_key": "save-build-host-001"
}
'import requests
url = "https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections"
payload = {
"name": "Build host",
"host": "host.example.com",
"username": "ubuntu",
"authentication": {
"type": "private_key",
"private_key": "<load from your secret manager>"
},
"consent_host_access": True,
"idempotency_key": "save-build-host-001"
}
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({
name: 'Build host',
host: 'host.example.com',
username: 'ubuntu',
authentication: {type: 'private_key', private_key: '<load from your secret manager>'},
consent_host_access: true,
idempotency_key: 'save-build-host-001'
})
};
fetch('https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections', 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}/ssh-connections",
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([
'name' => 'Build host',
'host' => 'host.example.com',
'username' => 'ubuntu',
'authentication' => [
'type' => 'private_key',
'private_key' => '<load from your secret manager>'
],
'consent_host_access' => true,
'idempotency_key' => 'save-build-host-001'
]),
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}/ssh-connections"
payload := strings.NewReader("{\n \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\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}/ssh-connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections")
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 \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\n}"
response = http.request(request)
puts response.read_body{
"connection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"host": "<string>",
"port": 123,
"username": "<string>",
"workspace_path": "<string>",
"auth_type": "password",
"host_key_fingerprint": "<string>",
"pending_host_key_fingerprint": "<string>",
"device_id": "<string>",
"status": "saved",
"error_code": "<string>",
"created_at": "<string>",
"updated_at": "<string>"
},
"replayed": true
}{
"connection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"host": "<string>",
"port": 123,
"username": "<string>",
"workspace_path": "<string>",
"auth_type": "password",
"host_key_fingerprint": "<string>",
"pending_host_key_fingerprint": "<string>",
"device_id": "<string>",
"status": "saved",
"error_code": "<string>",
"created_at": "<string>",
"updated_at": "<string>"
},
"replayed": true
}Add an SSH connection
Save encrypted password or private-key authentication for a Linux or macOS host. Does not connect yet. Explicit host-access consent and a non-secret idempotency_key are required. Same key/body replays for 24 hours, including after credential rotation. Changed bodies, expired keys and deleted connections conflict. Keep credentials in a caller-side secret manager, never prompts or command history. REST only. Requires an API key with ssh:write, which is never granted by default: select it explicitly when creating the key.
Server setup requirements
Reason’s hosted connector initiates SSH from Reason’s cloud, not from your browser or computer. Configure the server before saving a connection:
- Use a Linux server or a Mac (macOS on Apple silicon or Intel) with an x64 or arm64 CPU. Windows is not supported by this automatic installation flow. SSH devices run headlessly; they do not provide desktop streaming or interactive desktop control.
- Provide a public hostname or IP address and an SSH port reachable from Reason’s cloud. Private LAN, localhost, and Tailscale-only addresses are not supported, so a Mac or server behind a home router needs a public address or port forward. A successful SSH connection from your own computer does not prove cloud reachability. Reason may connect from more than one address. Configure the host’s firewall and any upstream network rules according to your organization’s security policy; do not disable the firewall.
- Enable SSH and authorize the supplied SSH user with a password or private key. Prefer a dedicated, least-privileged account and key. The account needs a private, writable home directory, disk space for the worker, and permission to read, write, and execute within its workspace. Root or administrator access is not required.
- Allow outbound HTTPS for the CLI download and the worker’s connection to Reason. On Linux, the host needs bash, curl, getent, stat, readlink, and timeout. With systemd, a non-root user needs lingering enabled by the administrator. Without systemd, use Reconnect after the host restarts or the worker stops. On macOS, the stock system tools are sufficient and the account does not need to sign in to the desktop. The worker runs as a per-user launchd agent, so use Reconnect after the Mac restarts, and keep the Mac from sleeping while it should accept work.
- Save the connection, then connect to probe the SSH host key. Compare the fingerprint with a trusted source before choosing Trust & install, for example by running
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubon the host through a console you already trust. Reason installs and enrolls the worker only after that verification.
For example, an administrator can prepare a dedicated user on a systemd host, replacing the public key:
sudo useradd -m -s /bin/bash reason
sudo loginctl enable-linger reason
sudo install -d -m 700 -o reason -g reason /home/reason/.ssh
echo 'ssh-ed25519 AAAA... reason' | sudo tee /home/reason/.ssh/authorized_keys
sudo chown reason:reason /home/reason/.ssh/authorized_keys
sudo chmod 600 /home/reason/.ssh/authorized_keys
On a Mac, turn on Remote Login in System Settings > General > Sharing. Then an administrator can prepare the account from Terminal, replacing the public key. The dseditgroup line is needed only when Remote Login allows only specific users:
sudo sysadminctl -addUser reason -fullName "Reason worker" -password "$(openssl rand -base64 24)"
sudo createhomedir -c -u reason
sudo dseditgroup -o edit -a reason -t user com.apple.access_ssh
sudo -u reason install -d -m 700 /Users/reason/.ssh
echo 'ssh-ed25519 AAAA... reason' | sudo -u reason tee /Users/reason/.ssh/authorized_keys
sudo -u reason chmod 600 /Users/reason/.ssh/authorized_keys
sudo pmset -a sleep 0
Run a Session on the host
Once the connection is connected, its device_id is the enrolled Device (a different ID from the connection). Read GET /v3/organizations/{orgId}/devices/{device_id} and use a root’s id as root_id, then create a Session with devices:use:
POST /v3/organizations/{orgId}/sessions
{
"prompt": "Write fizzbuzz.py in the workspace, run it with python3, and report its output and df -h .",
"idempotency_key": "<uuid>",
"target": { "kind": "headless", "device_id": "<device_id>", "root_id": "<root_id>" }
}
GET the Session to read back its target. Instructions are content-screened: host reconnaissance such as uname -a or reading /etc/os-release returns 400 instruction_blocked with the matched categories.
Remove the host software
Deleting a connection deletes its credentials and revokes its Device; the worker files stay on the host. To remove them, run systemctl --user disable --now reason-ssh-<connection_id>.service; rm -f ~/.config/systemd/user/reason-ssh-<connection_id>.service; systemctl --user daemon-reload; rm -rf ~/.reason-ssh/<connection_id> ~/reason-workspaces/<connection_id> as the SSH user. For root, use systemctl without --user and the unit in /etc/systemd/system. A custom workspace_path is left in place.
If setup fails, inspect the connection’s error_code: ssh_host_blocked means the address is not permitted; ssh_dns_failed means name resolution failed; ssh_authentication_failed means the credentials were rejected; ssh_platform_unsupported means the operating system or architecture is unsupported. A connected setup receipt is not proof that the Device remains online; check its current Device status.
Scope:ssh:writecurl --request POST \
--url https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Build host",
"host": "host.example.com",
"username": "ubuntu",
"authentication": {
"type": "private_key",
"private_key": "<load from your secret manager>"
},
"consent_host_access": true,
"idempotency_key": "save-build-host-001"
}
'import requests
url = "https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections"
payload = {
"name": "Build host",
"host": "host.example.com",
"username": "ubuntu",
"authentication": {
"type": "private_key",
"private_key": "<load from your secret manager>"
},
"consent_host_access": True,
"idempotency_key": "save-build-host-001"
}
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({
name: 'Build host',
host: 'host.example.com',
username: 'ubuntu',
authentication: {type: 'private_key', private_key: '<load from your secret manager>'},
consent_host_access: true,
idempotency_key: 'save-build-host-001'
})
};
fetch('https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections', 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}/ssh-connections",
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([
'name' => 'Build host',
'host' => 'host.example.com',
'username' => 'ubuntu',
'authentication' => [
'type' => 'private_key',
'private_key' => '<load from your secret manager>'
],
'consent_host_access' => true,
'idempotency_key' => 'save-build-host-001'
]),
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}/ssh-connections"
payload := strings.NewReader("{\n \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\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}/ssh-connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reasonmachines.com/v3/organizations/{orgId}/ssh-connections")
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 \"name\": \"Build host\",\n \"host\": \"host.example.com\",\n \"username\": \"ubuntu\",\n \"authentication\": {\n \"type\": \"private_key\",\n \"private_key\": \"<load from your secret manager>\"\n },\n \"consent_host_access\": true,\n \"idempotency_key\": \"save-build-host-001\"\n}"
response = http.request(request)
puts response.read_body{
"connection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"host": "<string>",
"port": 123,
"username": "<string>",
"workspace_path": "<string>",
"auth_type": "password",
"host_key_fingerprint": "<string>",
"pending_host_key_fingerprint": "<string>",
"device_id": "<string>",
"status": "saved",
"error_code": "<string>",
"created_at": "<string>",
"updated_at": "<string>"
},
"replayed": true
}{
"connection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"host": "<string>",
"port": 123,
"username": "<string>",
"workspace_path": "<string>",
"auth_type": "password",
"host_key_fingerprint": "<string>",
"pending_host_key_fingerprint": "<string>",
"device_id": "<string>",
"status": "saved",
"error_code": "<string>",
"created_at": "<string>",
"updated_at": "<string>"
},
"replayed": true
}Authorizations
Your Reason API key from Settings > API. 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, automations:read, automations:write, agent_auth:read. mcp:write manages MCP server configuration only; it does not authorize remote MCP-tool execution. sessions:debug is privileged: it expands diagnostic session events only for organization owners/admins.
Path Parameters
Organization id or slug. Resolve it with GET /v3/self.
Body
1 - 1601 - 253^[a-zA-Z0-9.:_-]+$SSH username, up to 64 characters, or Railway's sbx:: routing selector.
1 - 77- Option 1
- Option 2
Show child attributes
Show child attributes
Persist one non-secret key per logical request. Retry the identical body/key for up to 24 hours. Expired or deleted request keys return 409; reconnect intentionally with a fresh key.
1 - 160^[A-Za-z0-9._:-]+$1 <= x <= 65535Optional absolute remote workspace path. Omit to use the installer's private default workspace.
1 - 1024
