AgenTrux API Reference
API reference for endpoints exposed via the A2A (Agent-to-Agent) protocol.
Base URL:
https://api.agentrux.com
Table of Contents
- Authentication Flow
- Credentials
- Discovery — /.well-known + /a2a
- Auth — /auth
- Data Plane — /topics
- Console — /console
- Error Responses
- Webhook Notifications
Authentication Flow
1. Redeem an Activation Code (once)
POST /auth/redeem-activation-code { "code": "act_..." }
→ { client_id: "crd_...", client_secret: "aks_...", script_id, issued_at }
2. Get an access token (per script run)
POST /oauth/token (form: grant_type=client_credentials&client_id=crd_...&client_secret=aks_...)
→ { access_token: "aat_..." (JWT), expires_in: 600, ... }
3. Send / Receive Events
POST /topics/{id}/events (send — Authorization: Bearer aat_...)
GET /topics/{id}/events (receive / poll)
GET /topics/{id}/events/stream (SSE)
4. Refresh (authorization_code / device_code grants only)
POST /oauth/token (form: grant_type=refresh_token&refresh_token=art_...)
(client_credentials does NOT issue a refresh token — just call /oauth/token again.)
5. Cross-Account share (Console-only, Phase Z 2026-05-22)
Topic owner: POST /console/aliases/{ali_id}/share-trust {grantee_email}
Grantee: POST /console/grants {topic_id, script_id, action}
(Anonymous invite tokens were removed; receiver must be a registered user.)
Credentials
| Name | Prefix | Lifetime | Description |
|---|---|---|---|
| Activation Code | act_ |
One-time | Redeem for a Script credential via POST /auth/redeem-activation-code. Do not store |
| Script Credential ID (client_id) | crd_ |
Permanent | Your client_id. Store it (sent to POST /oauth/token) |
| Client Secret | aks_ |
Permanent | Your password. Shown once at redeem; store securely |
| Access Token (JWT) | aat_ (RS256 JWT) |
Short-lived (~10 min) | Used in Authorization: Bearer header |
| Refresh Token | art_ |
One-time (rotation) | Issued only by authorization_code / device_code grants (not client_credentials) |
Note (Phase Z, 2026-05-22): inv_ anonymous invite tokens are no longer issued. Cross-account access is established via Console (admin direct picker by grantee email). See Cross-Account Share.
Discovery
GET /a2a
Health check. No authentication required.
Response 200
{
"status": "ok",
"version": "0.1.0",
"timestamp": "2026-03-11T12:00:00+00:00"
}
GET /.well-known/agent-card.json
A2A Agent Card (JSON). No authentication required. An agent can autonomously use AgenTrux by reading this card alone.
Auth
Authentication and token endpoints. None require a JWT (credentials are in the body).
POST /auth/redeem-activation-code
Redeem a one-time Activation Code (act_) for a permanent Script credential.
Request
{
"code": "act_Abc123..."
}
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | The act_ activation code (single-use) |
Response 200
{
"client_id": "crd_550e8400-e29b-41d4-a716-446655440000",
"client_secret": "aks_xxxxxxxx",
"script_id": "scr_...",
"issued_at": "2026-03-11T12:00:00+00:00"
}
client_secret (aks_) is returned exactly once.
| Error | HTTP | Condition |
|---|---|---|
| NOT_FOUND | 404 | Code does not exist or has expired |
| CONFLICT | 409 | Code already consumed |
Important: Store
client_id(crd_) andclient_secret(aks_) permanently. Redeem is one-time; issue a freshact_in the Console if lost.
POST /oauth/token
Obtain a Bearer access token. Form-encoded (application/x-www-form-urlencoded), OAuth 2.1.
Request (grant_type=client_credentials)
grant_type=client_credentials&client_id=crd_...&client_secret=aks_...
| Field | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | client_credentials |
| client_id | string | Yes | Script credential ID (crd_) |
| client_secret | string | Yes | Script credential secret (aks_) |
Response 200
{
"access_token": "aat_eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 600,
"scope": "topic:top_<uuid>:read topic:top_<uuid>:write"
}
grant_type=client_credentials does not return a refresh_token; call /oauth/token again when the aat_ expires (~10 min).
JWT Claims
The issued JWT contains the following claims:
| Claim | Description |
|---|---|
sub |
Script ID (UUID) |
account_id |
Account ID (billing principal, value acc_…) |
alias_id |
Alias ID (application-layer principal that owns the script, value ali_…) |
scope |
Granted topic scopes |
| Error | HTTP | Condition |
|---|---|---|
| invalid_client | 401 | Invalid client_id (crd_) or client_secret (aks_) |
| SUSPENDED | 403 | Script, Alias, or account is suspended |
POST /oauth/token (grant_type=refresh_token)
Rotate a refresh token for a new access token. Refresh tokens are issued only by the authorization_code and device_code grants — client_credentials has no refresh leg (just call /oauth/token again). The old refresh token is immediately invalidated.
Request (form-encoded)
grant_type=refresh_token&refresh_token=art_xxxxxxxx&client_id=...
Response 200
{
"access_token": "aat_eyJhbGciOi...",
"refresh_token": "art_yyyyyyyy",
"token_type": "Bearer",
"expires_in": 600
}
| Error | HTTP | Condition |
|---|---|---|
| UNAUTHORIZED | 401 | Invalid or already-used refresh token |
Important: Always save the new
refresh_token. The old one is immediately invalidated.
Cross-Account Share (Console-only, Phase Z 2026-05-22)
The legacy anonymous inv_ tokens were removed. For inviting a registered user into a group, use POST /console/group-invites/redeem (the invitee redeems in their own Console session). Cross-account topic/script access is established through two Console actions, both requiring a signed-in session (no anonymous-receiver flow):
- Topic owner calls
POST /console/aliases/{ali_id}/share-trustwith{ "grantee_email": "..." }. The grantee must already be a registered AgenTrux user. - Grantee then calls
POST /console/grantswith{ topic_id, script_id, action }in their own Console session.
For Script lending (delegated execution), use POST /console/scripts/{scr_id}/share with { grantee_email } directly — the script_delegations row is created server-side.
Data Plane
Event send/receive on topics. All endpoints require JWT authentication.
Authorization: Bearer <access_token>
JWT scope: topic:{topic_id}:read or topic:{topic_id}:write
GET /topics
List every topic this script can access, with human-readable names. Used by workflow plugins (MCP, Agent SDK, etc.) to populate a topic selector without going through the Console (Kratos) API.
Auth: any valid script JWT. The response is built from the JWT
scope claim — no extra DB authorization step.
Request
GET /topics
Authorization: Bearer <access_token>
Response 200
{
"items": [
{
"topic_id": "top_019d5ec8-dc41-7723-9761-6a40eacc858a",
"name": "oc-command",
"display_name": "OpenClaw Command",
"retention_seconds": 86400,
"actions": ["read", "write"]
}
]
}
| Field | Type | Description |
|---|---|---|
| topic_id | UUID | Topic identifier |
| name | string | Topic name |
| display_name | string | Human-readable name (currently always mirrors name) |
| retention_seconds | int | Retention in seconds |
| actions | string[] | Granted actions (subset of ["read", "write"]) |
Results are sorted by (name, topic_id) for stable positional ordering.
A script granted both read and write on the same topic appears as one
entry with actions: ["read", "write"]. Soft-deleted topics are dropped
silently so a stale grant does not break the dropdown.
GET /console/grants
List the grants visible in the current Console session, joined with topic
metadata. Intended for admin / diagnostic UIs that need to answer "what
permissions do I have, who gave them to me, and what are the per-grant
limits" — workflow plugins should prefer GET /topics for dropdowns.
Auth: Console session (Kratos). This is a Console-plane endpoint, not a script-JWT data-plane one.
Request
GET /console/grants
Cookie: <console session>
Response 200
{
"items": [
{
"grant_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"topic_id": "top_019d5ec8-dc41-7723-9761-6a40eacc858a",
"topic_name": "oc-command",
"topic_display_name": "OpenClaw Command",
"action": "read",
"grantor_alias_id": "...",
"description": "OpenClaw command bus",
"rate_limit_per_min": 60,
"daily_limit": 10000,
"created_at": "2026-04-08T17:52:48+00:00"
}
]
}
| Field | Type | Description |
|---|---|---|
| grant_id | UUID | Grant identifier |
| topic_id | UUID | Topic the grant covers |
| topic_name | string | Topic name (joined from topic_repo) |
| topic_display_name | string | Topic display name |
| action | string | "read" or "write" |
| grantor_alias_id | UUID | Alias that issued the grant |
| description | string | Free-form description |
| rate_limit_per_min | int | null | Grant-level per-minute cap (null = unlimited) |
| daily_limit | int | null | Grant-level per-day cap (null = unlimited) |
| created_at | ISO 8601 | When the grant was created |
One entry per active (topic, action) pair. Wildcard (topic_id = "*")
grant rows are not returned — build_scope_with_grants already explodes
them per-topic at JWT issuance, and the wildcard row itself is not
directly callable. Soft-deleted grants, soft-deleted topics, and grants
outside the JWT scope are dropped. Sorted by (topic_name, action, grant_id).
POST /topics/{topic_id}/events
Send an event to a topic.
Auth: topic:{topic_id}:write
Request
{
"event_type": "sensor.reading",
"payload": {"temperature": 23.5, "unit": "celsius"},
"metadata": {"source": "edge-01"}
}
Provide either payload (inline path) or payload_object_id (object-ref path), not both.
| Field | Type | Required | Description |
|---|---|---|---|
| event_type | string | No | Event type label (max 128 chars) |
| payload | any JSON value | Conditional | Inline JSON payload (required for the inline path) |
| payload_object_id | string (pob_) |
Conditional | Reference to a pre-uploaded payload object (required for the object-ref path) |
| metadata | object | No | Free-form metadata object |
Response 200
{
"event_id": "evt_...",
"topic_id": "top_...",
"stored_at": "2026-03-11T12:00:00+00:00",
"ttl_expires_at": "2026-03-12T12:00:00+00:00",
"payload_kind": "inline",
"inline_size_bytes": 48,
"next_read_cursor": "..."
}
For the object-ref path, payload_kind is "object_ref" and the response includes payload_object_id (pob_...) and size_bytes instead of inline_size_bytes.
| Error | HTTP | Condition |
|---|---|---|
| UNAUTHORIZED | 401 | Invalid or expired JWT |
| FORBIDDEN | 403 | No write scope |
| NOT_FOUND | 404 | Topic does not exist |
| PAYLOAD_TOO_LARGE | 413 | Payload exceeds size limit |
| RATE_LIMITED | 429 | Publish rate limit exceeded |
GET /topics/{topic_id}/events
Retrieve events with cursor-based pagination.
Auth: topic:{topic_id}:read
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| after | cursor | null | Fetch events strictly after this cursor |
| before | cursor | null | Fetch events strictly before this cursor |
| limit | int (>0, capped at 1000) | 100 | Number of events to fetch |
| order | string | asc | Sort order: asc or desc |
| since | RFC3339 | null | Only events at/after this timestamp |
| until | RFC3339 | null | Only events before this timestamp |
Response 200
{
"events": [
{
"event_id": "evt_...",
"event_type": "sensor.reading",
"stored_at": "2026-03-11T12:00:00+00:00",
"ttl_expires_at": "2026-03-12T12:00:00+00:00",
"payload_kind": "inline",
"payload": {"temperature": 23.5},
"producer_script_id": "scr_...",
"cursor": "..."
}
],
"next": {
"after": "<opaque-cursor>",
"before": null,
"has_more": true,
"url": "/topics/top_.../events?after=...&limit=100",
"frontier": "<opaque-cursor>"
},
"topic": {
"topic_id": "top_...",
"oldest_available_evt_id": "evt_..."
}
}
Events are returned under the top-level events array. Pagination uses the
opaque cursor token in next.after (not the evt_id string): pass it as the
after query param, or follow next.url directly, to fetch the next page.
The next object also carries before (opaque cursor for the reverse
direction, or null), has_more (boolean), and frontier (the
current watermark cursor, for tailing new events). The topic object reports the
topic_id and oldest_available_evt_id (or null if the topic is
empty). Each event also carries its own cursor.
| Error | HTTP | Condition |
|---|---|---|
| UNAUTHORIZED | 401 | Invalid JWT |
| FORBIDDEN | 403 | No read scope |
| NOT_FOUND | 404 | Topic does not exist |
| PAYMENT_REQUIRED | 402 | Monthly egress quota exhausted (details.metric = "egress_bytes") |
| RATE_LIMITED | 429 | Rate limit exceeded |
Reads (this endpoint, GET /events/{event_id}, and
GET /payloads/{object_id}) return 402 PAYMENT_REQUIRED with
details.metric = "egress_bytes" once the monthly egress quota is exhausted.
GET /topics/{topic_id}/events/stream
Receive hint notifications for new events via SSE (Server-Sent Events).
Auth: topic:{topic_id}:read
Query Parameters / Headers
| Parameter | Type | Description |
|---|---|---|
| Last-Event-ID | string | Resume position: the opaque cursor from a prior id: field (sent as the Last-Event-ID request header) |
Response 200 text/event-stream
event: hint
id: <opaque-cursor>
data: {"topic_id":"top_...","event_id":"evt_...","ts":"2026-01-01T00:00:00Z","producer_script_id":"scr_..."}
: keepalive
- Hint only: SSE notifies that events exist — it does not include the payload. Use
GET /topics/{topic_id}/eventsto fetch actual events. - Keepalive: A comment line (
: keepalive) is sent every 30 seconds. - Reconnect: Send the last received
id:value (an opaque cursor) as theLast-Event-IDheader when reconnecting; the server decodes it to resume. Ordering is not guaranteed — dedupe byevent_id.
GET /topics/{topic_id}/events/{event_id}
Get a single event by ID.
Auth: topic:{topic_id}:read
Response 200
{
"event_id": "evt_...",
"event_type": "sensor.reading",
"stored_at": "2026-03-11T12:00:00+00:00",
"ttl_expires_at": "2026-03-12T12:00:00+00:00",
"payload_kind": "inline",
"payload": {"temperature": 23.5},
"producer_script_id": "scr_...",
"cursor": "..."
}
Historical backfill
There is no separate by-sequence endpoint. To backfill missed events
after a network interruption, page through
GET /topics/{topic_id}/events?after=<cursor> using the cursor-based
read, resuming from the last cursor you successfully processed.
POST /topics/{topic_id}/payloads
Create external payload metadata and get a presigned upload URL.
Auth: topic:{topic_id}:write
Request
{
"content_type": "application/octet-stream",
"size_bytes": 1048576,
"checksum_sha256": "abc123..."
}
| Field | Type | Required | Description |
|---|---|---|---|
| size_bytes | int (>0) | Yes | Size in bytes |
| content_type | string | No | MIME type |
| checksum_sha256 | string | Yes | SHA-256 checksum of the object |
Response 201
{
"object_id": "...",
"upload_url": "https://...",
"expiration": "2026-03-12T12:00:00+00:00"
}
Upload the file via a PUT request to
upload_url.
| Error | HTTP | Condition |
|---|---|---|
| RATE_LIMITED | 429 | Presigned URL issuance is rate-limited per tier per hour (Free 10/h; Starter/Pro/Business 100/h; Enterprise unlimited). Returns a Retry-After header. |
GET /topics/{topic_id}/payloads/{object_id}
Get payload metadata and a presigned download URL.
Auth: topic:{topic_id}:read
Response 200
{
"object_id": "...",
"content_type": "application/octet-stream",
"size": 1048576,
"hash": "sha256:abc123...",
"download_url": "https://...",
"expiration": "2026-03-12T12:00:00+00:00"
}
Console
Console endpoints are used by the web UI. They require session-based authentication (Kratos).
GET /console/connections/sse
SSE endpoint for the Composer. Streams real-time event notifications for topics that the current user's Alias owns or has access to. Used by the console Composer for live event updates. To read the actual events, use GET /console/topics/{topic_id}/events.
Auth: Session cookie (Kratos) — the user must have read access to the topic via their active Alias.
Query Parameters / Headers
| Parameter | Type | Description |
|---|---|---|
| Last-Event-ID | string | Resume position: the opaque cursor from a prior id: field (sent as the Last-Event-ID request header) |
Response 200 text/event-stream
event: hint
id: <opaque-cursor>
data: {"topic_id":"top_...","event_id":"evt_...","ts":"2026-01-01T00:00:00Z","producer_script_id":"scr_..."}
: keepalive
- Same SSE format as
GET /topics/{topic_id}/events/streambut authenticated via session instead of JWT. - Last-Event-ID: On reconnect, the browser automatically sends the
Last-Event-IDheader with the last receivedid:value, allowing seamless resume. - Keepalive: A comment line (
: keepalive) is sent every 30 seconds to keep the connection alive.
AccountResponse
The AccountResponse schema (returned by account-related console endpoints) includes the following fields:
| Field | Type | Description |
|---|---|---|
| account_id | UUID | Account ID |
| name | string | Account slug (unique identifier) |
| display_name | string | null | Display name |
| string | null | Contact email | |
| phone | string | null | Phone number |
| real_name | string | null | Legal / real name |
| address | string | null | Postal address |
| date_of_birth | string | null | Date of birth (ISO date string) |
| gender | string | null | Gender |
| created_at | datetime | Account creation timestamp |
These profile fields can be updated via PATCH /console/me/profile, sending a JSON body containing the profile fields to change.
Error Responses
All endpoints use a unified error format.
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid credentials"
}
}
| Code | HTTP | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Invalid or expired JWT / credentials |
| FORBIDDEN | 403 | Valid JWT but insufficient scope |
| SUSPENDED | 403 | Account, Alias, or script is suspended |
| NOT_FOUND | 404 | Resource does not exist |
| CONFLICT | 409 | Token already used / grant already exists |
| INVALID | 422 | Validation error |
| RATE_LIMITED | 429 | Rate limit exceeded |
| PAYLOAD_TOO_LARGE | 413 | Payload size exceeded |
| SERVICE_UNAVAILABLE | 503 | Backend dependency is down |
| INTERNAL | 500 | Unexpected server error |
Webhook Notifications
When an event is published to a topic, registered webhook URLs receive a notification (configured via the management console).
Payload
{
"topic_id": "top_...",
"event_id": "evt_...",
"timestamp": 1710151200,
"delivery": "webhook"
}
Signature Verification
The X-AgenTrux-Signature header carries the timestamp and an HMAC-SHA256 signature (Stripe-compatible format).
X-AgenTrux-Signature: t={timestamp},v1={hex_digest}
Verification steps:
- Parse
t(unix timestamp) andv1(hex signature) from the header - Compute
HMAC-SHA256(secret, "{t}." + raw_request_body)using the subscription's webhook secret as the key - Compare with
v1using a constant-time comparison