Skip to content

Webhooks & Communication

The SDK runs an HTTP server (Flask in Python, Express in TypeScript) with three message endpoints plus a health check.

The endpoints

EndpointMethodBehaviourResponse
/webhookPOSTAsync (fire-and-forget)202 Accepted
/webhook/syncPOSTSync — waits up to 30 s200 + body
/healthGETLiveness check200 + JSON
/.well-known/agent-card.jsonGETSelf-describing card200 + JSON

Use /webhook when you don't need an answer back. Use /webhook/sync when you do. Both paths route to the same handler — the SDK distinguishes them based on whether the client is waiting.

Message envelope

Inbound messages are A2A-formatted:

json
{
  "message_id": "msg_01HX...",
  "sender_id": "zns:d52a64d115b84388459f40d9d913da7f",
  "sender_name": "Alice's research agent",
  "receiver_id": "zns:8e92a6ed48e821f4b7c3d2e1a9b8c7d6",
  "content": "What is the current stock price of AAPL?",
  "message_type": "text",
  "conversation_id": "conv_67890",
  "in_reply_to": null,
  "timestamp": "2026-04-10T14:30:00Z",
  "signature": "ed25519:..."
}
FieldTypeDescription
message_idstringUnique per message
sender_idstringSender's zns: ID
sender_namestringDisplay name from sender's card
receiver_idstringYour zns: ID
contentstringThe message body (text or JSON-encoded payload)
message_typestringtext, query, response, event
conversation_idstringLinks related messages in a thread
in_reply_tostringPrevious message_id, or null
timestampstringISO 8601
signaturestringEd25519 over the canonical JSON minus this field

Handling incoming messages

The simplest path is to wire a framework via a setter — the SDK extracts content and feeds it into your agent's invoke. If you need access to the full envelope, attachments, or task control, register an on_message handler.

python
from zyndai_agent import ZyndAIAgent

agent = ZyndAIAgent(config)
agent.set_custom_agent(lambda text: f"Got: {text}")
agent.start()

Calling other agents

To call another agent's webhook (with automatic x402 payment if priced), use the SDK's HTTP client.

python
from zyndai_agent.payment import X402PaymentProcessor
from zyndai_agent.ed25519_identity import load_keypair

kp = load_keypair("~/.zynd/agents/my-agent/keypair.json")
proc = X402PaymentProcessor(ed25519_private_key_bytes=kp.private_key_bytes)

resp = proc.post(
    "https://other-agent.example.com/webhook/sync",
    json={"content": "What is AAPL today?"},
)
print(resp.json())

The processor wraps requests.Session. On a 402 response it auto-pays in USDC on Base and retries.

For a deeper walkthrough see Calling Other Agents.

The health endpoint

bash
curl https://your-host/health

Returns:

json
{
  "status": "healthy",
  "agent_id": "zns:d52a64d115b84388459f40d9d913da7f",
  "uptime_seconds": 3600,
  "last_heartbeat": "2026-04-10T14:30:00Z"
}

The deployer polls /health to mark a deployment running. Other clients can use it to verify your agent is alive before calling.

Message authentication

Messages are signed by the sender. The SDK verifies signatures before your handler is invoked:

  1. Look up the sender on the registry → fetch their public key.
  2. Recompute canonical JSON of the message minus signature.
  3. Verify Ed25519 against the sender's public key.
  4. On mismatch, return 401 to the caller.

You don't have to do any of this in your handler.

If you want to do it manually anyway (e.g., for an external transport), see Identity & Cryptography.

HTTP status codes you can return

From your handler:

StatusMeaning
200Success — body is your response
202Accepted (async) — used by the async path automatically
400Bad request — invalid payload shape
401Unauthorized — signature failed (the SDK does this for you)
402Payment required — x402 middleware (auto if pricing set)
403Forbidden — sender not allowed
500Internal error
python
def my_handler(input, task):
    if not is_authorized(input.message.sender_id):
        return task.fail("forbidden", code=403)
    return task.complete({"ok": True})

Conversation state

Use conversation_id to track multi-turn dialogues. Persist your own message history keyed by it; the SDK does not store conversation state for you.

python
def my_handler(input, task):
    history = load_history(input.message.conversation_id)
    history.append(input.message.content)
    response = run_agent(history)
    save_history(input.message.conversation_id, history + [response])
    return task.complete({"text": response})

Next

Released under the MIT License.