Address

Guides

Wake on mail

Subscribe a webhook to an address's events, prove the endpoint, verify each signature, and fetch what arrived.

Markdown

A webhook wakes your agent when mail arrives or is held, so it need not poll. Events carry ids only, never message text: your agent then reads with its key, as it would anyway.

Subscribe

curl
curl -X POST https://address.seleya.ai/api/v1/subscriptions \
  -H "Authorization: Bearer $SELEYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address": "desk@acme.seleya.email", "url": "https://agent.acme.com/hooks/mail"}'

Before it saves the subscription, the service sends your URL a challenge:

JSON
{"type": "subscription.challenge", "timestamp": "2026-10-16T08:00:00Z", "data": {"subscription_id": "…", "challenge": "ch_…"}}

Answer it with any 2xx and the same challenge, as JSON: {"challenge": "ch_…"}. Then the call answers 201 with the subscription and its secret (whsec_…). The secret is shown once: keep it to verify every event.

Events

EventWhen
message.receivedMail was delivered to the address
message.heldMail was held for a person
message.sentA send went out
message.bouncedA send bounced
approval.requestedThe agent asked for something only a person may do
approval.approved, approval.rejected, approval.failedThe person decided, or carrying it out failed
binding.paused, tenant.pausedA key, or the owner’s sending, was paused

Each body is {"type", "timestamp", "data": {"event_id", "address_id", "object_id"}}. For a message, object_id is the delivery id: fetch it with GET /v1/deliveries/{id}, or read the stream from your cursor.

Verify the signature

Events are signed with Standard Webhooks. The headers are webhook-id, webhook-timestamp and webhook-signature, which holds v1, and a base64 HMAC-SHA256. The key is your secret after whsec_, base64-decoded; the signed text is {webhook-id}.{webhook-timestamp}.{body}, with the body exactly as received.

import base64, hashlib, hmac, time

def verify(secret: str, headers, body: bytes) -> bool:
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    want = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(want, sig.split(",", 1)[1]) for sig in headers["webhook-signature"].split() if sig.startswith("v1,"))

Delivery

  • At least once: the same event may come twice, so drop repeats by webhook-id.
  • A failed delivery is retried with backoff. After five failures in a row the subscription pauses.
  • Answer quickly with any 2xx, then do the work.

MCP Events

Apps that support MCP Events, such as ChatGPT, subscribe through the MCP server instead, to message.received and message.held. Deliveries use the same signatures, plus an X-MCP-Subscription-Id header, and carry {"eventId", "name", "timestamp", "data": {"address", "delivery_id"}}.

Claim or select an address before subscribing. An empty address selection is an error. A subscription covers the current selected addresses; repeat subscribe after claiming another address or changing access. Repeating it reconciles the scope and callback secret. Disconnecting or losing authority removes the affected callbacks. Delivery depends on the host supporting events and running the agent; authorization alone does not establish background execution.

Docs