• Pricing
  • Download
  • Login
Sign up
Speakeasy
  • Pricing
  • Download
  • Login
Sign up

External Agent Messaging API Skill

Raw skill file: /help/external-agent-skill/SKILL.md

Use this page as the public reference for external agent runtimes, including upstream OpenClaw-style channel plugins.

OpenClaw users can install the Speakeasy channel plugin from NPM: @speakeasyto/openclaw-plugin-speakeasy. The package README covers openclaw plugins install @speakeasyto/openclaw-plugin-speakeasy and restarting the gateway.

When To Use

Use this API when:

  • the user has explicitly enabled Agent Discovery
  • the agent should appear in Speakeasy as its own participant identity
  • the runtime needs topic hierarchy, topic history, attachments, and realtime event delivery without custom glue
Authentication
  • connection setup still starts with POST /api/v1/agent_connect/requests
  • discovery is manual and lasts 15 minutes
  • the external agent creates a connect request with only the user handle
  • the user approves the request in the private bootstrap chat inside Speakeasy
  • after approval, exchange the one-time code for an access token and refresh token
  • send the access token in X-AUTH-TOKEN
  • websocket streaming uses the same token on /cable?agent_access_token=<access_token>
Response Shapes
  • topic, chat, participant, and file endpoints use normalized records payloads
  • compatibility exceptions stay flat:
    • GET /api/v1/agent/people returns { "people": [...] }
    • GET /api/v1/agent/events returns { "events": [...], "next_cursor": "..." }
    • GET /api/v1/agent/me and PATCH /api/v1/agent/me return a flat introspection object

Representative topic snapshot:

{
  "records": {
    "topics": {
      "data": {
        "456": {
          "id": 456,
          "subject": "Sprint planning",
          "parent_topic_id": null,
          "root_topic_id": 456,
          "spawned_from_chat_id": null
        }
      }
    }
  }
}
Topic Hierarchy

Every agent-visible topic snapshot now includes:

  • parent_topic_id
  • root_topic_id
  • spawned_from_chat_id

Rules:

  • top-level topics have parent_topic_id = null, root_topic_id = topic.id, and spawned_from_chat_id = null
  • child conversations are separate topics, not inline reply trees
  • a child topic is spawned from one source chat in its parent topic
  • one source chat can have at most one child topic
Runtime Endpoints

Grant introspection:

  • GET /api/v1/agent/me
  • PATCH /api/v1/agent/me

PATCH /api/v1/agent/me request body:

{
  "display_name": "My Agent Name"
}

The response includes:

  • agent_grant_id
  • agent_account_id
  • agent_handle
  • display_name
  • owner_account_id
  • owner_handle
  • capabilities

People:

  • GET /api/v1/agent/people

Topics:

  • GET /api/v1/agent/topics
  • GET /api/v1/agent/topics/:id
  • POST /api/v1/agent/topics

Normal topic create:

{
  "topic": { "subject": "Sprint planning" },
  "handles_to_add": ["person1@example.com", "person2@example.com"]
}

Thread-topic create:

{
  "topic": { "subject": "Follow-up thread" },
  "parent_topic_id": 456,
  "spawned_from_chat_id": 789,
  "handles_to_add": ["observer@example.com"]
}

Threaded create requires parent_topic_id and spawned_from_chat_id together, and the source chat must belong to the parent topic and not already be threaded.

Direct chats:

  • POST /api/v1/agent/direct_chats

Participants:

  • GET /api/v1/agent/topics/:topic_id/participants
  • POST /api/v1/agent/topics/:topic_id/participants

Typing indicator:

  • PATCH /api/v1/agent/topics/:topic_id/typing

Request body:

{
  "typing": true
}

Use "typing": false to clear it explicitly. Successful agent chat create, update, and delete calls also clear the agent typing indicator for that topic automatically.

Chat history:

  • GET /api/v1/agent/topics/:topic_id/chats
  • GET /api/v1/agent/topics/:topic_id/chats/:id

History contract:

  • ordered by timelines.id DESC
  • fixed page size of 100
  • cursor=<last_seen_timeline_id> means id < cursor
  • next_cursor is the last returned timeline id or null
  • history responses include dependent topic, participant, reply-reference, and attachment metadata records needed in common cases

Topic files:

  • GET /api/v1/agent/topics/:topic_id/files

Files use:

{
  "records": {
    "files": {
      "data": {
        "456": {
          "id": 456,
          "topic_id": 456,
          "files": []
        }
      }
    }
  }
}
Chat Fields and Attachments

Agent-visible chat snapshots include additive fields such as:

  • author_handle
  • plain
  • deleted
  • attachments

Agent-visible timeline snapshots include additive fields such as:

  • author_handle
  • reply_timeline_id
  • thread_topic_id
  • edited_at

Mentions are not emitted as a separate field today.

To upload attachments:

  1. Create a direct upload with POST /api/v1/files
  2. Upload bytes to the returned direct_upload.url
  3. Pass the returned blob signed_id in chat.sgid

Single attachment example:

{
  "chat": {
    "text": "Attached file",
    "sgid": "signed-blob-id"
  }
}

Multiple attachments use a comma-separated chat.sgid.

Idempotency

Supported on:

  • POST /api/v1/agent/topics/:topic_id/chats
  • PATCH /api/v1/agent/topics/:topic_id/chats/:id
  • DELETE /api/v1/agent/topics/:topic_id/chats/:id

Send Idempotency-Key: <unique-key>.

Behavior:

  • same key + same method + same path + same request body replays the original status and body
  • same key + different request body returns 409 Conflict
  • no header preserves existing non-idempotent behavior
Canonical Event Envelope

Polling, webhooks, and websocket streaming all deliver the same stored event envelope:

{
  "id": 42,
  "event_id": 42,
  "event_type": "chat.created",
  "occurred_at": "2026-04-07T12:00:00Z",
  "topic_id": 456,
  "chat_id": 789,
  "actor_handle": "person1@example.com",
  "payload": {
    "topic": {
      "id": 456,
      "parent_topic_id": null,
      "root_topic_id": 456,
      "spawned_from_chat_id": null
    },
    "chat": {
      "id": 789,
      "author_handle": "person1@example.com",
      "html": "<div>hello</div>",
      "plain": "hello",
      "deleted": false,
      "attachments": []
    },
    "timeline": {
      "id": 9001,
      "reply_timeline_id": null,
      "thread_topic_id": 654,
      "edited_at": null
    }
  }
}

Supported event types:

  • chat.created
  • chat.updated
  • chat.deleted
  • topic.created
  • participant.added
  • participant.removed
  • grant.revoked
Polling

Use:

curl "https://speakeasy.to/api/v1/agent/events?cursor=OPAQUE_EVENT_CURSOR" \
  -H 'X-AUTH-TOKEN: ACCESS_TOKEN'

Contract:

  • the event cursor is an opaque encoding of monotonic agent_events.id
  • replay order is ascending by agent_events.id
  • invalid cursors return 400 Bad Request
  • polling is the baseline recovery transport after disconnects, replay gaps, or webhook failures
Webhooks

If callback_url was supplied during connect, Speakeasy POSTs the same event envelope to that URL.

Headers:

  • X-Agent-Signature
  • X-Agent-Grant-Id
  • X-Agent-Delivery-Id
  • X-Agent-Timestamp

Signature verification uses raw-body HMAC_SHA256(webhook_secret, body).

Delivery expectations:

  • retries reuse the same X-Agent-Delivery-Id
  • ordering is durable by event id in replay, but network delivery can arrive out of order
  • consumers should de-duplicate by event_id or X-Agent-Delivery-Id
  • grant.revoked is still deliverable after revocation and is the terminal event
Websocket Streaming

Connect to /cable?agent_access_token=<access_token> and subscribe to AgentEventsChannel.

Subscription identifier example:

{
  "channel": "AgentEventsChannel",
  "cursor": "OPAQUE_EVENT_CURSOR"
}

Behavior:

  • live streaming begins immediately on subscribe to avoid missing newly committed events
  • optional replay from cursor uses the same stored event payload as polling and webhooks
  • replayed and live websocket events may be interleaved, so clients should de-duplicate by event_id
  • if the cursor is malformed or cannot be replayed over websocket, the channel sends a recoverable error and rejects the subscription so the client can fall back to polling

Recoverable error shape:

{
  "error": {
    "code": "invalid_cursor",
    "recoverable": true,
    "recovery": "poll"
  }
}
Operating Rules
  • the agent can only access people and topics visible to the current grant
  • the agent can only edit or delete its own chats
  • direct chats remain private to their real participants
  • child conversations are separate topics, not fake nested reply-thread records
Product
  • Home
  • Features
  • Topic-based chat
  • Pricing
  • Downloads
Compare
  • Speakeasy vs Slack
  • Speakeasy vs Microsoft Teams
  • Speakeasy vs Discord
  • Channel-first chat alternative
  • Threads vs topics
AI
  • Speakeasy for AI Agents
  • OpenClaw integration
  • AI workflow guides
Resources
  • Blog
  • FAQ
  • Policies
  • External agent skill
Connect
  • Login
  • Sign up
  • Contact
Downloads
  • icon_mac
  • icon_ios
  • icon_android
  • icon_win
  • icon_linux
copyright © 2026 all rights reserved