Gateway API

May 29, 2026 · View on GitHub

Default gateway endpoint: http://127.0.0.1:3000

Page Guide

Who this page is for

  • Operators wiring external systems into the local gateway
  • Integrators testing pairing, bearer-token auth, and webhook delivery
  • Reviewers checking what the HTTP surface exposes by default

Read this next

  • Open Security before exposing any gateway path beyond loopback or tunnel defaults
  • Open Configuration if you need the concrete gateway and channel keys behind these examples
  • Open Usage and Operations for runtime checks, restarts, and troubleshooting around gateway behavior

If you came from ...

  • Usage and Operations: this page provides the endpoint-level detail behind the gateway health and webhook checks
  • Security: come here when a security review needs the concrete HTTP auth and endpoint surface
  • Configuration: return here after editing gateway settings to validate the API-facing behavior

Endpoints

EndpointMethodAuthDescription
/healthGETNoneHealth check
/pairPOSTX-Pairing-CodeExchange one-time pairing code for bearer token (loopback-only when gateway is publicly bound)
/webhookPOSTAuthorization: Bearer <token>Send message payload: {"message":"..."}
/media/transcribePOSTAuthorization: Bearer <token>Transcribe base64 audio payloads through configured STT provider
/cronGETAuthorization: Bearer <token> on public binds or when pairing tokens existList live scheduler jobs from the running daemon
/cron/addPOSTAuthorization: Bearer <token> on public binds or when pairing tokens existAdd or schedule a live cron job
/cron/removePOSTAuthorization: Bearer <token> on public binds or when pairing tokens existRemove a live cron job by id
/cron/pausePOSTAuthorization: Bearer <token> on public binds or when pairing tokens existPause a live cron job by id
/cron/resumePOSTAuthorization: Bearer <token> on public binds or when pairing tokens existResume a live cron job by id
/cron/updatePOSTAuthorization: Bearer <token> on public binds or when pairing tokens existPartially update a live cron job
/telegramPOSTX-Telegram-Bot-Api-Secret-Token matching channels.telegram.accounts.<id>.webhook_secretTelegram inbound webhook
/whatsappGETQuery paramsMeta webhook verification
/whatsappPOSTMeta signatureWhatsApp inbound webhook
/maxPOSTX-Max-Bot-Api-Secret when configuredMax inbound webhook delivery
/api/messagesPOSTAuthorization: Bearer <Bot Framework JWT> and optional X-Webhook-SecretTeams Bot Framework inbound webhook
/.well-known/agent-card.jsonGETNoneA2A Agent Card discovery (public)
/a2aPOSTAuthorization: Bearer <token>A2A JSON-RPC 2.0 endpoint

Quick Examples

1) Health check

curl http://127.0.0.1:3000/health

2) Pair and get token

curl -X POST \
  -H "X-Pairing-Code: PAIRING_CODE" \
  http://127.0.0.1:3000/pair

Expected: bearer token response (exact JSON shape may vary by version).

3) Send webhook message

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message":"hello from webhook"}' \
  http://127.0.0.1:3000/webhook

4) List live cron jobs

curl -X GET \
  -H "Authorization: Bearer YOUR_TOKEN" \
  http://127.0.0.1:3000/cron

5) Add a live cron job

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expression":"*/15 * * * *","command":"echo hello"}' \
  http://127.0.0.1:3000/cron/add

/cron/add also accepts one-shot payloads such as {"delay":"10m","command":"echo later"} and agent payloads such as {"expression":"0 * * * *","prompt":"Summarize alerts","model":"openrouter/anthropic/claude-sonnet-4"}.

6) Max webhook delivery

Single-account example:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Max-Bot-Api-Secret: YOUR_MAX_SECRET" \
  -d '{"update_type":"bot_started","chat_id":100,"timestamp":1710000000000,"user":{"user_id":42,"first_name":"Igor"}}' \
  http://127.0.0.1:3000/max

Multi-account example:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Max-Bot-Api-Secret: YOUR_MAX_SECRET" \
  -d '{"update_type":"message_created","timestamp":1710000000000,"message":{"sender":{"user_id":42,"first_name":"Igor"},"recipient":{"chat_id":100,"chat_type":"dialog"},"body":{"mid":"m1","text":"ping"}}}' \
  "http://127.0.0.1:3000/max?account_id=main"

Max webhook notes:

  • nullclaw routes /max to the configured Max account by account_id query first, then by X-Max-Bot-Api-Secret.
  • If channels.max[].webhook_secret is configured, the header is required and must match exactly.
  • Use HTTPS in the configured Max-side webhook URL.

Teams webhook notes:

  • nullclaw validates the Bot Framework bearer token against Microsoft's OpenID metadata and signing keys before accepting the activity.
  • The token issuer must be https://api.botframework.com, the audience must match the configured Teams client_id, and the token serviceUrl must match the activity body.
  • Teams channelId endorsements are enforced from the published Bot Framework key metadata.
  • If channels.teams[].webhook_secret is configured, X-Webhook-Secret must also match exactly.

Media Transcription

POST /media/transcribe is intended for local orchestrators. It requires the same bearer-token auth as /webhook and /a2a, and uses the configured tools.media.audio STT model.

Request:

{
  "audio_base64": "BASE64_AUDIO_BYTES",
  "mime_type": "audio/webm;codecs=opus",
  "source": "mic",
  "language": "en"
}

Response:

{
  "text": "transcribed speech",
  "source": "mic",
  "language": "en",
  "mime_type": "audio/webm;codecs=opus"
}

Notes:

  • audio_base64 is required; mime_type defaults to audio/ogg.
  • Only audio/* MIME types are accepted.
  • Raise gateway.max_body_size_bytes, gateway.request_timeout_secs, and gateway.webhook_rate_limit_per_minute for live desktop audio chunking.
  • tools.media.audio.models[0] selects the STT provider/model/endpoint. If that provider has no key, NullClaw falls back to a keyed OpenAI/Groq/Telnyx provider when available.
  • Custom STT endpoints must be HTTPS for remote hosts; plain HTTP is accepted only for local/private endpoints and URLs with query or fragment parts are rejected.

A2A (Agent-to-Agent Protocol)

NullClaw implements Google's A2A protocol v0.3.0 over JSON-RPC 2.0, enabling interoperability with any A2A-compatible agent or client.

Configuration

Add to ~/.nullclaw/config.json:

{
  "a2a": {
    "enabled": true,
    "name": "My Agent",
    "description": "General-purpose AI assistant",
    "url": "https://your-public-url.example.com",
    "version": "0.3.0"
  }
}
FieldDefaultDescription
enabledfalseEnable A2A endpoints
name"NullClaw"Agent name in the Agent Card
description"AI assistant"Agent description
url""Public URL (used in Agent Card and supportedInterfaces)
version"1.0.0"Agent version string
multi_modalfalseAdvertise multi-modal capability in the Agent Card. Set to true when the configured model supports image inputs. The gateway probes the model at startup and sets this automatically; override manually if needed.

Multi-modal support

When multi_modal is true, the Agent Card includes "multi_modal": true in its capabilities object, signalling to A2A clients that the agent accepts image attachments. Incoming A2A messages may include inlineData parts (base64-encoded images) alongside text parts; the gateway forwards them to the model as [IMAGE: <mime_type>] markers.

To accept large image payloads, raise the gateway's HTTP body limit and socket read timeout in the gateway config block (see configuration.md gateway section):

{
  "gateway": {
    "max_body_size_bytes": 67108864,
    "request_timeout_secs": 120
  }
}

Agent Card Discovery

curl http://127.0.0.1:3000/.well-known/agent-card.json

Returns the Agent Card with capabilities, skills, security schemes, and supported interfaces. No authentication required.

JSON-RPC Methods

All methods are called via POST /a2a with a bearer token from /pair.

MethodDescription
message/sendSend a message, receive completed task
message/streamSend a message, receive SSE stream of events
tasks/getRetrieve task by ID (supports historyLength)
tasks/cancelCancel an active task
tasks/listList tasks with optional state/contextId filters
tasks/resubscribeResume SSE stream for an existing task

Task Lifecycle

submitted → working → completed
                    → failed
                    → canceled
                    → input-required
                    → auth-required
                    → rejected

Terminal states: completed, failed, canceled, rejected.

Examples

Send a message:

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "messageId": "msg-1",
        "role": "user",
        "parts": [{"kind": "text", "text": "What is nullclaw?"}]
      }
    }
  }' \
  http://127.0.0.1:3000/a2a

Stream a response (SSE):

curl -N -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/stream",
    "params": {
      "message": {
        "messageId": "msg-2",
        "role": "user",
        "parts": [{"kind": "text", "text": "Explain A2A protocol"}]
      }
    }
  }' \
  http://127.0.0.1:3000/a2a

Get a task:

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"id":"task-1"}}' \
  http://127.0.0.1:3000/a2a

Cancel a task:

curl -X POST \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tasks/cancel","params":{"id":"task-1"}}' \
  http://127.0.0.1:3000/a2a

Multi-turn Conversations

Include contextId in the message to group tasks into a conversation. All messages with the same contextId share session state and conversation history:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "message/send",
  "params": {
    "message": {
      "messageId": "msg-3",
      "contextId": "my-conversation",
      "role": "user",
      "parts": [{"kind": "text", "text": "Follow-up question"}]
    }
  }
}

Error Codes

CodeNameDescription
-32700JSONParseErrorInvalid JSON payload
-32600InvalidRequestErrorRequest validation error
-32601MethodNotFoundErrorUnknown method
-32602InvalidParamsErrorMissing or invalid parameters
-32603InternalErrorServer-side error
-32001TaskNotFoundErrorTask ID not found
-32002TaskNotCancelableErrorTask already in terminal state
-32003PushNotificationNotSupportedErrorPush notifications not supported
-32005ContentTypeNotSupportedErrorIncompatible content types
-32007AuthenticatedExtendedCardNotConfiguredErrorExtended card not available

Security Guidance

  1. Keep gateway.require_pairing = true.
  2. Keep gateway on loopback (127.0.0.1) and expose externally through tunnel/proxy.
  3. If you intentionally use a non-loopback bind, generic endpoints (/webhook, /cron/*, /a2a, /media/transcribe) still require a stored bearer token even when interactive pairing is disabled; preconfigure gateway.paired_tokens if you are not using /pair.
  4. On non-loopback binds, /pair only accepts loopback clients. Do initial pairing locally or preconfigure gateway.paired_tokens before exposing the port.
  5. Treat bearer tokens as secrets; do not commit or log them.
  6. Treat Max webhook secrets the same way: randomize them per account and do not reuse one secret across multiple bots.

Next Steps

  • Review Security before changing public exposure, pairing, or token-handling assumptions
  • Check Configuration for the settings that back the examples on this page
  • Use Usage and Operations for gateway startup, health checks, and post-change validation flow