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>",
"transcript_path": "<string>",
"tool_name": "<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>",
"transcript_path": "<string>",
"tool_name": "<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>',
transcript_path: '<string>',
tool_name: '<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>',
'transcript_path' => '<string>',
'tool_name' => '<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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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>",
"transcript_path": "<string>",
"tool_name": "<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>",
"transcript_path": "<string>",
"tool_name": "<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>',
transcript_path: '<string>',
tool_name: '<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>',
'transcript_path' => '<string>',
'tool_name' => '<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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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 \"transcript_path\": \"<string>\",\n \"tool_name\": \"<string>\",\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. 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.
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?