Docs/cURL
Examples

cURL

The whole loop from a shell, with jq.

Set the key and host once:

Set your keyShell
export SAI_API_KEY="sapi_..."
export SAI_API_URL="https://api.simular.ai"

Start a task

Start a taskShell
SESSION=$(curl -s "$SAI_API_URL/v1/agents/message" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message":"Open Notepad, type hello, save it to the desktop as hello.txt","wait":false}' \
  | jq -r .sessionId)
echo "$SESSION"

Poll until done

Poll until doneShell
CURSOR=""
while :; do
  RESP=$(curl -s "$SAI_API_URL/v1/agents/events?sessionId=$SESSION&since=$CURSOR&wait=30" \
    -H "Authorization: Bearer $SAI_API_KEY")
  CURSOR=$(echo "$RESP" | jq -r .cursor)
  STATUS=$(echo "$RESP" | jq -r .status)
  echo "$RESP" | jq -c '.events[] | select(.type=="data-progress") | .data.text'
  case "$STATUS" in
    running) ;;
    needs_approval)
      echo "$RESP" | jq '.events[] | select(.type=="data-approval-request") | .data'
      break ;;
    idle)
      echo "$RESP" | jq -r .text
      break ;;
    error)
      echo "$RESP" | jq '.events[] | select(.type=="error")'
      break ;;
  esac
done

An empty since on the first call is the same as omitting it.

Answer an approval

When the loop prints an approval with "isLinkOnly": false:

Answer an approvalcURL
curl -s "$SAI_API_URL/v1/agents/approve" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "approvalId": "ap_7Qw", "response": "yes" }'

When it prints "isLinkOnly": true, open the approvalUrl in a browser. Then run the poll loop again with the same SESSION and CURSOR.

The poll response also carries the blocking approval as a top-level approval field, so jq .approval works when the event has already scrolled past.

Attach a file

Upload and attach a fileShell
ATTACHMENT=$(curl -s "$SAI_API_URL/v1/agents/upload" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "x-filename: report.pdf" \
  --data-binary @./report.pdf)

curl -s "$SAI_API_URL/v1/agents/message" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --argjson a "$ATTACHMENT" \
    '{message:"Summarise the attached PDF in five bullets", wait:false, attachments:[$a]}')"

Stop a task

Stop the taskcURL
curl -s "$SAI_API_URL/v1/agents/abort" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"sessionId\":\"$SESSION\"}"

Get typed output

Add options to ask for JSON, and keep the turnId:

Start a task with typed outputShell
TURN=$(curl -s "$SAI_API_URL/v1/agents/message" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Open https://news.ycombinator.com and read the title of the top story.",
    "options": {
      "output_schema": {
        "type": "object",
        "properties": { "title": { "type": "string" } },
        "required": ["title"]
      }
    },
    "wait": false
  }' | jq -r .turnId)
echo "$TURN"

Poll the turn until it ends:

Poll the turnShell
while :; do
  R=$(curl -s "$SAI_API_URL/v1/agents/turns/$TURN" -H "Authorization: Bearer $SAI_API_KEY")
  case $(echo "$R" | jq -r .status) in
    succeeded) echo "$R" | jq .output; break ;;
    failed) echo "$R" | jq '{reason_code, raw_answer, output_errors}'; break ;;
    *) sleep 3 ;;
  esac
done

status is needs_input while an approval is pending. Read it from GET /events with the turn's sessionId, and answer it as above.

Stream instead

Leave wait out and read the SSE frames as they arrive:

Stream a taskcURL
curl -sN "$SAI_API_URL/v1/agents/message" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "What is on the screen right now?" }'
© 2026 Simular Inc. All rights reserved.