Reference / HTTP API
Reference

HTTP API

The SDKs for TypeScript, Go and Python wrap this API. Use it directly from any other language: other languages says what your server does. Every path below is under https://app.wakeshift.ai/api/apps/v1, takes and returns JSON, and answers errors as { "error": { "code", "message" } }. A request body field this page doesn't list is a 400.

Authentication

Your key is an Ed25519 private key. Don't send it. Instead, send a fresh signed assertion with each request:

HTTP
POST /api/apps/v1/events/emit
Authorization: Bearer <assertion>
Content-Type: application/json

The assertion is a compact JWS:

  • Header: {"alg": "EdDSA", "typ": "app+jwt"}.
  • Claims: iss and sub are your app's ID; aud is Wakeshift's URL without a trailing slash, plus /api/apps (https://app.wakeshift.ai/api/apps); iat is now, in whole seconds (an integer); exp is iat + 120; jti is 16 random bytes, base64url.
  • Signature: Ed25519 over base64url(header) + "." + base64url(claims). All base64url is without padding.

Wakeshift refuses an assertion that has expired, lives longer than 5 minutes, is issued more than a minute in the future, has no jti, or reuses one.

To read the key wst_test_<app>_<seed>: strip wst_, split on the first _ (the mode) and the next _ (the app ID). The rest is the 32-byte seed, base64url without padding. App IDs never contain _; seeds can.

The caller

Calls made for a conversation (subscribing it, unsubscribing it, annotating its results) carry a call object, built from the tool call:

JSON
{ "user": "u_123", "client": "Cursor", "meta": { }, "user_agent": "Cursor/1.0.0", "session": "<session key>", "harness": "grok-bot", "conversation": "bc-…" }
Field What it is
client The MCP client's name, clientInfo.name from initialize. It tells Wakeshift which agent this is: claude-code, codex-mcp-client, Cursor, copilot-cli, opencode, antigravity-client, and others. Send it whenever you have it: it wins over meta and user_agent. A stateless server doesn't have it: see stateless servers.
meta The tool call's _meta, as the client sent it. Codex sends its thread ID there. Without client, it tells Wakeshift Claude Code, Codex and Antigravity apart.
user_agent The HTTP request's User-Agent header. Without client, it tells Wakeshift opencode, Cursor and GitHub Copilot CLI apart.
session The session_key argument the agent passed back, if any. It's an opaque key: pass it on unchanged.
harness The harness argument: the program the agent says it runs in. A hint, not a credential: it only refines what client, meta and user_agent say (a Cursor call may be Grok Bot's or a Cursor cloud agent's), or names the agent when they say nothing. One that contradicts them is ignored. Pass it on unchanged.
conversation The conversation_id argument, if any. Cursor tells MCP servers nothing about the conversation, so its agents (and Grok Bot) pass their CURSOR_CONVERSATION_ID. Pass it on unchanged.
user Optional. Your own ID for the person (what it can contain).

client is at most 200 characters, session at most 100 and user at most 200. Longer is a 400.

Give a tool of yours that subscribes the caller (and any other tool whose results you annotate) three optional string arguments, described this way, and leave them out of required. The subscribe_events tool has them too:

JSON
"session_key": { "type": "string", "description": "The session key an earlier result gave you, if it did. It's a secret: keep it to this conversation; don't share or log it" },
"harness": { "type": "string", "description": "The program you're running in: codex, claude-code, cursor (local CLI or IDE), cursor-cloud (Cursor cloud agent), grok-bot, copilot-cli, opencode, antigravity, openclaw (even when it runs your turns in Codex), hermes, chatgpt, or your program's name if it isn't listed" },
"conversation_id": { "type": "string", "description": "Your own ID for this conversation, if your program gives you one. Cursor and Grok Bot: the output of `printenv CURSOR_CONVERSATION_ID`. Codex and Antigravity send it automatically; others leave it empty" }

Handling secrets

Some of what passes through your server is a credential for one conversation. Anyone who has it can act as that conversation: read its events, subscribe it to more, or end its subscriptions.

  • the session_key argument and the session a subscribe returns;
  • addition.text and addition.meta, which can hold what lets that conversation receive its events.

Don't log tool arguments or tool results that carry them, don't put them in error reports, and don't show them on a page. To name a conversation where people can see it, use subscription.id, or an HMAC of the session key under a secret of your own. Your key never leaves your server; publish URLs are credentials too.

Events

Events are the main way to offer subscriptions. Declare them in your manifest, let agents subscribe, and emit occurrences.

Endpoint Body Returns
PUT /manifest { description?, …, events: [{ name, description, title?, arguments, payload, topic, filter?, personal? }] } The manifest as Wakeshift keeps it. arguments and payload are JSON Schemas (2020-12).
POST /events/emit { name, arguments, payload, text, id?, to? } { matched, duplicate }
POST /event-subscriptions { call, name, arguments, once?, replay?, ttl? } { event_subscription: { id, event, arguments, created, expires_at }, session, addition: { text?, meta? }, agent }
POST /event-subscriptions/unsubscribe { call, name, arguments } { ended }
POST /event-subscriptions/list { user } { subscriptions: [{ id, event, arguments, destination, webhook_host?, created_at }] }
POST /event-subscriptions/end { user, id } { ended }
POST /events/subscribe { call, name, arguments, delivery, dialect, ttl_ms?, cursor?, max_age_ms? } { id, refresh_before, cursor, truncated, delivery_status? }
POST /events/unsubscribe { call, name, arguments, delivery, dialect } { ended }
  • Event types. topic is the template occurrences route by: ev/<name>/{arg}…, one segment per required string, number or boolean argument. filter maps a payload field to the optional argument it must equal. With personal: true, each subscription belongs to its subscriber and emit needs to. The SDKs build all of this from the event's definition.
  • Emit. payload must match the event's schema; text is at most 1000 bytes; the same id within a day emits once (duplicate: true). to names a personal event's subscriber, your call.user for them.
  • Bridged subscriptions (/event-subscriptions) subscribe the calling conversation, as subscribe_events does. Put addition in your tool's result, as for topics below. list and end act on one of your users' subscriptions, whatever their destination.
  • Native subscriptions (/events/subscribe) are what your server forwards when a client sends MCP Events' events/subscribe: pass the client's delivery object as it sent it, and dialect, "chatgpt" for ChatGPT and "draft" for every other client. Wakeshift checks the client's callback, then delivers occurrences to it, signed and in order.

Topics

Topics are what events are built on.

Endpoint Body Returns
POST /topics/subscribe { call, topic, filter?, once?, replay?, ttl? } { subscription: { id, topic, filter?, once, expires_at }, session, addition: { text?, meta? }, agent: { harness, via, claimed } }
POST /topics/unsubscribe { call, topic? } or { user, topic? } { unsubscribed: n, unknown_session? }
POST /topics/publish { topics: [...], event: { id?, text, data? } } { matched, duplicate }
POST /topics/annotate { call } { text?, meta?, agent: { harness, via, claimed } }
GET /topics/status?topic=… None { topic, subscriptions, connected, via: { stream, poll, hooks, webhook } }
POST /topics/close { topic } { closed: n }

After a subscribe, put addition.text after your own text in the tool result, and addition.meta in the result's _meta. The same goes for annotate, on the results you choose to annotate. A separate text content block after yours works, and so does your text, a blank line, then addition.text in one block (the SDK does the first for a result, the second for a string). In most MCP frameworks, setting _meta means returning a full CallToolResult.

agent is what Wakeshift recognised the caller as: harness (claude-code, codex, cursor, cursor-cloud, grok-bot, copilot, opencode, antigravity, …) and via ("client", "meta", "user_agent", or "claim" when only call.harness named it), both null when it recognised nothing; and claimed, what call.harness said, normalized ("Grok Bot" is grok-bot), or null. A claimed that differs from harness was ignored. When harness and claimed are both null, the agent can't be woken: log a warning.

unsubscribe takes call, to end the calling conversation's subscriptions, or user, to end every subscription of one of your users when they lose access. Passing both is a 400. Without topic, it ends them on all your app's topics. With call, unknown_session: true says Wakeshift doesn't know the session key, so nothing ended.

publish takes 1 to 20 topics. event.text is at most 1000 bytes, and event.data is a JSON object, within the 64 KB body limit. event.id (at most 200 characters) makes retries safe: the same id within a day publishes nothing and returns the first publish's matched with duplicate: true. After a day, the same id publishes again. matched counts the conversations the event reached.

filter is up to 20 top-level fields of the event's data, each name at most 64 characters, each equal to a string, number, boolean or null of at most 256 bytes as JSON.

ttl and replay are seconds as a number, or strings like "90s", "5m", "2h" and "30d" (days are whole numbers).

Subscriptions

Endpoint Body Returns
POST /subscriptions/{id}/send { event } { status: "delivered" | "queued" | "closed" }
GET /subscriptions/{id} None { id, topic, status: "active" | "closed", connected, via?, expires_at? }
POST /subscriptions/{id}/end {} { ended }: true if it ended the subscription, and its conversation is told; false if it had already ended or isn't your app's.

send and GET answer 404 unknown_subscription for an ID that isn't your app's.

Publish URLs

Endpoint Body Returns
POST /topics/publish-urls { topic } { id, topic, url }
POST /events/publish-urls { name, arguments, to? } { id, event, url }
DELETE /events/publish-urls/{id} None { revoked: true }

The URL itself takes no authentication. A topic's takes any body: see publish URLs. An event's takes the event's payload, a JSON object that matches its schema, and answers 400 bad_payload for anything else, or 410 unknown_event once your app no longer offers the event; to is required for a personal event, as for emit. DELETE revokes either kind (DELETE /topics/publish-urls/{id} still works for a topic's).

Users

Endpoint Body Returns
POST /users/forget { user } { forgotten }: true if there was something to delete. See forgetting a user.

Errors

Status Code When
400 bad_request Something in the request is invalid, including a body field this page doesn't list. The message says what.
401 unauthenticated The assertion was refused: expired, reused, signed with another key, or for another aud (a key for another Wakeshift). The message says which.
400 bad_arguments An event's arguments don't match its schema, or a routing value isn't a lowercase ID. The message says which.
403 private_topic Subscribing a caller to another user's u/<user>/… topic.
403 no_grant An app on Wakeshift accounts: the person's consent for your app, for that agent, is revoked or has expired.
403 other_client The session key is a conversation in another MCP client of the same user. Call again without it.
403 not_headless Topics need an app whose users are its own.
404 unknown_subscription The subscription ID isn't one of your app's.
404 unknown_publish_url The publish URL ID isn't one of your app's.
404 unknown_event Your app offers no event by that name.
413 too_large The request body is over 64 KB. The limit is on the whole body, so data must leave room for the rest of the request.