curl --request POST \
--url https://api.opal.dev/v1/agent-activity \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{
"hook_event_name": "<string>",
"tool_input": {},
"session_id": "<string>",
"prompt_id": "<string>",
"agent_id": "<string>",
"agent_type": "<string>",
"permission_mode": "<string>",
"actor_email": "<string>",
"opal_reported": {
"configured_email": "<string>",
"claude_account_email": "<string>",
"git_email": "<string>",
"os_user": "<string>",
"hostname": "<string>"
},
"duration_ms": "<unknown>",
"transcript_path": "<string>",
"tool_name": "<string>",
"mcp_server": {
"name": "<string>",
"source": "<string>"
},
"tool_use_id": "<string>",
"cwd": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
]
}
'import requests
url = "https://api.opal.dev/v1/agent-activity"
payload = { "events": [
{
"hook_event_name": "<string>",
"tool_input": {},
"session_id": "<string>",
"prompt_id": "<string>",
"agent_id": "<string>",
"agent_type": "<string>",
"permission_mode": "<string>",
"actor_email": "<string>",
"opal_reported": {
"configured_email": "<string>",
"claude_account_email": "<string>",
"git_email": "<string>",
"os_user": "<string>",
"hostname": "<string>"
},
"duration_ms": "<unknown>",
"transcript_path": "<string>",
"tool_name": "<string>",
"mcp_server": {
"name": "<string>",
"source": "<string>"
},
"tool_use_id": "<string>",
"cwd": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
] }
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({
events: [
{
hook_event_name: '<string>',
tool_input: {},
session_id: '<string>',
prompt_id: '<string>',
agent_id: '<string>',
agent_type: '<string>',
permission_mode: '<string>',
actor_email: '<string>',
opal_reported: {
configured_email: '<string>',
claude_account_email: '<string>',
git_email: '<string>',
os_user: '<string>',
hostname: '<string>'
},
duration_ms: '<unknown>',
transcript_path: '<string>',
tool_name: '<string>',
mcp_server: {name: '<string>', source: '<string>'},
tool_use_id: '<string>',
cwd: '<string>',
timestamp: '2023-11-07T05:31:56Z'
}
]
})
};
fetch('https://api.opal.dev/v1/agent-activity', 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.opal.dev/v1/agent-activity",
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([
'events' => [
[
'hook_event_name' => '<string>',
'tool_input' => [
],
'session_id' => '<string>',
'prompt_id' => '<string>',
'agent_id' => '<string>',
'agent_type' => '<string>',
'permission_mode' => '<string>',
'actor_email' => '<string>',
'opal_reported' => [
'configured_email' => '<string>',
'claude_account_email' => '<string>',
'git_email' => '<string>',
'os_user' => '<string>',
'hostname' => '<string>'
],
'duration_ms' => '<unknown>',
'transcript_path' => '<string>',
'tool_name' => '<string>',
'mcp_server' => [
'name' => '<string>',
'source' => '<string>'
],
'tool_use_id' => '<string>',
'cwd' => '<string>',
'timestamp' => '2023-11-07T05:31:56Z'
]
]
]),
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.opal.dev/v1/agent-activity"
payload := strings.NewReader("{\n \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\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.opal.dev/v1/agent-activity")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/agent-activity")
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 \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"received": 123,
"stored": 123,
"attributed": 123,
"unattributed": 123
}Report agent activity
Prototype (AG-285). Reports activity observed by an agent runtime on a developer’s machine - today, Claude Code hook events.
This is telemetry from cooperating machines, not attestation. Opal can only see what a configured, consenting client chooses to send, cannot verify that a fleet is configured at all, and has no way to detect a machine that is reporting nothing. Absence of activity for an agent therefore means “nothing was reported”, never “the agent did nothing”.
The request body is either a batch envelope ({"events": [...]}), a bare JSON array of events, or a single raw Claude Code hook payload - the last so a hook can POST here directly with no relay in between.
The bearer token establishes the organization and nothing finer. No field in the payload is treated as an identity claim Opal trusts: the actor headers below are used for best-effort attribution and display only. Events that cannot be attributed to an agent are retained and surfaced as unattributed activity rather than dropped.
curl --request POST \
--url https://api.opal.dev/v1/agent-activity \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{
"hook_event_name": "<string>",
"tool_input": {},
"session_id": "<string>",
"prompt_id": "<string>",
"agent_id": "<string>",
"agent_type": "<string>",
"permission_mode": "<string>",
"actor_email": "<string>",
"opal_reported": {
"configured_email": "<string>",
"claude_account_email": "<string>",
"git_email": "<string>",
"os_user": "<string>",
"hostname": "<string>"
},
"duration_ms": "<unknown>",
"transcript_path": "<string>",
"tool_name": "<string>",
"mcp_server": {
"name": "<string>",
"source": "<string>"
},
"tool_use_id": "<string>",
"cwd": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
]
}
'import requests
url = "https://api.opal.dev/v1/agent-activity"
payload = { "events": [
{
"hook_event_name": "<string>",
"tool_input": {},
"session_id": "<string>",
"prompt_id": "<string>",
"agent_id": "<string>",
"agent_type": "<string>",
"permission_mode": "<string>",
"actor_email": "<string>",
"opal_reported": {
"configured_email": "<string>",
"claude_account_email": "<string>",
"git_email": "<string>",
"os_user": "<string>",
"hostname": "<string>"
},
"duration_ms": "<unknown>",
"transcript_path": "<string>",
"tool_name": "<string>",
"mcp_server": {
"name": "<string>",
"source": "<string>"
},
"tool_use_id": "<string>",
"cwd": "<string>",
"timestamp": "2023-11-07T05:31:56Z"
}
] }
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({
events: [
{
hook_event_name: '<string>',
tool_input: {},
session_id: '<string>',
prompt_id: '<string>',
agent_id: '<string>',
agent_type: '<string>',
permission_mode: '<string>',
actor_email: '<string>',
opal_reported: {
configured_email: '<string>',
claude_account_email: '<string>',
git_email: '<string>',
os_user: '<string>',
hostname: '<string>'
},
duration_ms: '<unknown>',
transcript_path: '<string>',
tool_name: '<string>',
mcp_server: {name: '<string>', source: '<string>'},
tool_use_id: '<string>',
cwd: '<string>',
timestamp: '2023-11-07T05:31:56Z'
}
]
})
};
fetch('https://api.opal.dev/v1/agent-activity', 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.opal.dev/v1/agent-activity",
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([
'events' => [
[
'hook_event_name' => '<string>',
'tool_input' => [
],
'session_id' => '<string>',
'prompt_id' => '<string>',
'agent_id' => '<string>',
'agent_type' => '<string>',
'permission_mode' => '<string>',
'actor_email' => '<string>',
'opal_reported' => [
'configured_email' => '<string>',
'claude_account_email' => '<string>',
'git_email' => '<string>',
'os_user' => '<string>',
'hostname' => '<string>'
],
'duration_ms' => '<unknown>',
'transcript_path' => '<string>',
'tool_name' => '<string>',
'mcp_server' => [
'name' => '<string>',
'source' => '<string>'
],
'tool_use_id' => '<string>',
'cwd' => '<string>',
'timestamp' => '2023-11-07T05:31:56Z'
]
]
]),
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.opal.dev/v1/agent-activity"
payload := strings.NewReader("{\n \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\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.opal.dev/v1/agent-activity")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/agent-activity")
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 \"events\": [\n {\n \"hook_event_name\": \"<string>\",\n \"tool_input\": {},\n \"session_id\": \"<string>\",\n \"prompt_id\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"agent_type\": \"<string>\",\n \"permission_mode\": \"<string>\",\n \"actor_email\": \"<string>\",\n \"opal_reported\": {\n \"configured_email\": \"<string>\",\n \"claude_account_email\": \"<string>\",\n \"git_email\": \"<string>\",\n \"os_user\": \"<string>\",\n \"hostname\": \"<string>\"\n },\n \"duration_ms\": \"<unknown>\",\n \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\n \"mcp_server\": {\n \"name\": \"<string>\",\n \"source\": \"<string>\"\n },\n \"tool_use_id\": \"<string>\",\n \"cwd\": \"<string>\",\n \"timestamp\": \"2023-11-07T05:31:56Z\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"received": 123,
"stored": 123,
"attributed": 123,
"unattributed": 123
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
The identity the reporting client claims this activity belongs to - an email or an Anthropic user id. Unverified: a Claude Code hook payload contains no user identity, so this has to be injected by whoever deploys the hook (via the http handler's headers map and allowedEnvVars interpolation), and whoever controls the machine controls the header. Used only when the API key itself does not identify a developer. When the email belongs to a person in the organization, that person is recorded as who the activity was reported for (always as a claim), and the activity is attributed to the one Claude agent they own, if they own exactly one. The API key's own email is never treated this way: a key belongs to whoever created it, which with a shared key is not who is at the keyboard. Omitting it is valid. When an event's opal_reported object carries other emails, Opal also tries them, in the order configured_email, claude_account_email, git_email, after this header and skipping any that repeat it. The first one that resolves to an agent wins.
The name of the machine that sent this request, as that machine reports it (hostname -s). Unverified: two machines can share a name, and whoever controls the machine controls the header. Stored with each event, so a reader can tell which machine reported it. When an event's own opal_reported.hostname names a different machine, this header wins and the body's value is kept beside it. Omitting it is valid.
Body
AgentActivityIngestRequest Object
Description
A batch of agent activity events.
Three body shapes are accepted: this envelope ({"events": [...]}), a bare JSON
array of AgentActivityEvent, and a single AgentActivityEvent object. The last
exists so a Claude Code hook can POST here directly, since a hook sends one bare
payload per event and cannot wrap it.
Show child attributes
Show child attributes
Response
A per-batch accounting of what was accepted, stored, and attributed. An organization that has agent activity turned off gets a 200 with an empty body for every body shape, so a sender does not keep the events and try again.
AgentActivityIngestResult Object
Description
What one ingest request actually did.
received and stored differ when a delivery was retried and deduplicated.
attributed and unattributed sum to received rather than to stored, because
attribution happens before deduplication. Unattributed events are kept, not dropped:
activity Opal cannot tie to a governed agent is itself a governance finding.
Events parsed out of the request body.
Rows actually written. Lower than received when a retried delivery was deduplicated on tool_use_id.
Events that resolved to an agent in this organization.
Events that resolved to no agent, and are therefore visible only in the unattributed activity list. A persistently high value means the reporting clients are not sending an identity Opal can resolve.
Was this page helpful?