The Remote API is for agents that live entirely outside Orvoq — a LangGraph graph, a CrewAI crew, or any custom framework you run on your own infrastructure — and call into Orvoq to report what they are doing. Unlike a native or SDK-connected agent, Orvoq never needs a URL to reach your agent; your agent reaches Orvoq instead, which is why this works from inside a private network, a CI runner, or a sandbox with no inbound access at all.
Every call is plain REST, so it works from any language. The examples below use Python (via requests) since that is the most common runtime for LangGraph/CrewAI-style agents, but the same calls work from curl, Node, Go, or anything else that can send an HTTP request.
REMOTE_API_KEYS environment variable on the agent-service instead of a per-user API key — useful for a service account shared across many agents. If neither a database API key nor REMOTE_API_KEYS is configured, the Remote API refuses every request rather than silently allowing them through.Every request needs your key in one of two headers, against the /remote/v1 base path:
Authorization: Bearer YOUR_API_KEY
# — or —
X-Remote-Key: YOUR_API_KEYSpawning creates the run your agent's work will be tracked under. Two modes: start it immediately, or leave it queued until you're ready.
import requests
NEXUS_URL = "https://your-workspace.orvoq.ai/remote/v1"
HEADERS = {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
}
resp = requests.post(f"{NEXUS_URL}/executions", headers=HEADERS, json={
"organizationId": ORG_ID,
"workspaceId": WORKSPACE_ID,
"agentId": AGENT_ID,
"label": "Customer feedback analysis",
"initialTask": "Analyze Q3 feedback and identify top 5 issues",
"autoStart": True, # False leaves it "Queued" until you resume it
"memoryScope": "execution", # 'execution' | 'session' | 'workspace'
})
execution = resp.json()
EXECUTION_ID = execution["_id"]
SESSION_ID = execution["sessionId"] # a headless session, auto-created for this runThis creates a standalone execution with its own headless session — it shows up on the Runs page immediately. If your agent is already a participant in a real session, spawn into that session instead with POST /sessions/:sessionId/executions.
There are two distinct ways to invoke a tool, depending on who should be in control:
| Mode | Endpoint | Use for |
|---|---|---|
| Report-only | /tools/report | Your agent already ran the tool itself — you just want it recorded for audit and monitoring. |
| Gate-then-execute | /tools/invoke | You want Orvoq's permission and approval system to decide before anything runs — for anything dangerous (email, delete, deploy, write to a CRM). |
requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/report", headers=HEADERS, json={
"toolName": "web_search",
"input": {"query": "customer benchmarks 2026"},
"output": search_results,
"durationMs": 1200,
"status": "success", # or "failure", with an "error" field
})resp = requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/invoke", headers=HEADERS, json={
"toolName": "send_email",
"input": {"to": "team@company.com", "subject": "Q3 analysis", "body": "..."},
"isSideEffecting": True, # triggers the approval check
"framework": "langgraph",
})
outcome = resp.json()
if outcome["status"] == "executed":
print(outcome["result"]) # ran immediately, no approval needed
elif outcome["status"] == "blocked":
print(outcome["reason"]) # tool not granted to this agent
elif outcome["status"] == "awaiting_approval":
approval_id = outcome["approvalRequestId"] # see Step 3A framework that raises its own human-in-the-loop pause — a LangGraph interrupt(), for instance — without going through a specific tool call can open one directly with POST /executions/:id/request-approval, giving it a summary and the proposedchange. This blocks the execution (moving it to “Waiting on approval”) and surfaces it in Command Center exactly like a gated tool call.
When a call comes back awaiting_approval, the execution is paused until a human acts in the Orvoq UI. Since your agent is outside Orvoq, it polls for the resolution rather than waiting on a webhook:
import time
def wait_for_approval(approval_id, timeout_seconds=300):
start = time.time()
while time.time() - start < timeout_seconds:
status = requests.get(
f"{NEXUS_URL}/approvals/{approval_id}/status", headers=HEADERS
).json()
if status["status"] != "pending":
return status["status"] # 'approved' | 'rejected' | 'modified' | 'overridden'
time.sleep(2)
return "timeout"
resolution = wait_for_approval(approval_id)
if resolution == "approved":
# Re-invoke with the resolved approval id so the gate isn't re-triggered
requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/tools/invoke", headers=HEADERS, json={
"toolName": "send_email",
"input": {"to": "team@company.com", "subject": "Q3 analysis", "body": "..."},
"isSideEffecting": True,
"framework": "langgraph",
"resolvedApprovalId": approval_id,
})
elif resolution == "modified":
pass # the reviewer changed the instructions — re-plan around them
elif resolution == "rejected":
pass # skip the actionIn the Orvoq UI, the run shows a “Waiting on approval” badge the moment this happens, and the reviewer can Approve, Reject with a reason, Modify the instructions, or Override the output directly — the same four actions available for a native agent's approval requests.
Poll the execution's current state, or subscribe to a live stream of its step count and token usage:
# Point-in-time status
status = requests.get(f"{NEXUS_URL}/executions/{EXECUTION_ID}", headers=HEADERS).json()
print(status["status"]) # Queued, Running, WaitingOnApproval, Completed, Failed, Killed, Retrying
# Real-time, via Server-Sent Events (updates roughly every 500ms)
# GET /executions/{EXECUTION_ID}/monitor → event stream of {status, totalSteps, totalTokens}Lifecycle actions all take the same small body — who is acting, and why:
| Endpoint | What it does |
|---|---|
/executions/:id/suspend | Soft-pause — keeps state, can be resumed later |
/executions/:id/resume | Resume a suspended or queued execution |
/executions/:id/retry | Retry after a failure |
/executions/:id/clone | Restart fresh from the same configuration |
/executions/:id/complete | Mark it successfully finished |
/executions/:id/kill | Hard stop — use when the work should end, successful or not |
requests.post(f"{NEXUS_URL}/executions/{EXECUTION_ID}/complete", headers=HEADERS, json={
"actorId": "my-orchestrator",
"actorType": "system",
"reason": "Task finished successfully",
})All paths below are relative to /remote/v1.
| Method | Path | Description |
|---|---|---|
| POST | /executions | Spawn a standalone execution (auto-creates a headless session) |
| POST | /sessions/:sessionId/executions | Spawn an execution inside an existing session |
| GET | /executions/:id | Get execution details and status |
| POST | /executions/:id/tools/report | Record a tool call the agent already ran |
| POST | /executions/:id/tools/invoke | Gate a tool through permission/approval before it runs |
| POST | /executions/:id/request-approval | Raise a human review pause directly, without a specific tool call |
| GET | /approvals/:id/status | Poll an approval's resolution |
| POST | /approvals/:id/approve | Approve programmatically |
| POST | /approvals/:id/reject | Reject programmatically, with a reason |
| POST | /approvals/:id/modify | Redirect the agent with modified instructions |
| POST | /approvals/:id/override | Substitute your own output for the proposed one |
| GET | /sessions/:id/approvals | List pending approvals for a session |
| SSE | /executions/:id/monitor | Live execution monitoring stream |
| GET | /sessions/:id/monitor | Snapshot of every execution in a session |
| POST | /executions/:id/suspend | Soft-pause |
| POST | /executions/:id/resume | Resume |
| POST | /executions/:id/retry | Retry after failure |
| POST | /executions/:id/clone | Restart fresh |
| POST | /executions/:id/complete | Mark completed |
| POST | /executions/:id/kill | Hard stop |
| POST | /sessions/:sessionId/:action | Session-level pause / resume / stop / restart / archive |
| Error | Cause / fix |
|---|---|
| 401 — Missing API key | No Authorization or X-Remote-Key header was sent. |
| 401 — Remote API is not enabled | No API key exists for this organization and REMOTE_API_KEYS is unset on the agent-service — create one in Settings → API Keys. |
| 401 — Invalid API key | The key doesn't match any active database key or configured static key — check for typos, revocation, or expiry. |
| Tool returns blocked | The tool isn't in the agent's granted list — update its permissions or registration. |
| Approval never resolves | No one is watching the run — approvals escalate after the configured timeout, but check the Runs page. |
Was this article helpful?