Documentation menu

Webhooks

anymrkt sends signed events to an HTTPS endpoint you choose — the same model as Stripe's event destinations. Create destinations in Integration Studio → Event destinations, or with the studio_create_event_destination MCP tool.

Destinations are scoped

Each destination receives events from exactly one place, chosen when it is created and never widened afterwards:

  • One brand — orders, refunds, disputes, publishes for that brand only.
  • One board — card, comment, sprint, epic and connector changes on that Kanban board.
  • Your account — agents connecting, refused or rate-limited calls, usage thresholds, keys, tokens, plan changes. Only you can create these, signed in; an agent token cannot.

Events are named one by one — there are no wildcards — so a new event type never starts arriving at your endpoint until you add it. An agent token may create a brand destination only for a brand it was given, and only with thewebhooks permission.

Payload: thin or snapshot

Thin (the default) says what happened and where to look. Snapshot adds a data object with the record as it was, secrets redacted. Every payload carries contextnaming the scope it came from, and the destination's pinned api_version.

{
  "id": "evt_9f2c…",
  "object": "event",
  "type": "order.paid",
  "api_version": "2026-10-03",
  "created": 1759536000,
  "livemode": true,
  "context": { "brand_id": "b1c2…" },
  "related_object": { "id": "ord_…", "type": "order", "url": "/sell/orders/ord_…" }
}

Verifying the signature

Every request has Anymrkt-Signature: t=<unix>,v1=<hex>, where the hex is HMAC-SHA256 of "<t>.<raw body>" with your signing secret. Reject requests older than five minutes. After you roll a secret, the old one keeps signing for 24 hours alongside the new one, so there are two v1 values.

import crypto from 'node:crypto'

// Use the RAW request body — re-serialised JSON will not match the signature.
export function verifyAnymrkt(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=')).filter(([k]) => k === 't').map(([k, v]) => [k, v]),
  )
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  // During a secret roll there are two v1 values; either may match.
  return header.split(',').filter((p) => p.startsWith('v1=')).some((p) => {
    const got = Buffer.from(p.slice(3), 'hex'), want = Buffer.from(expected, 'hex')
    return got.length === want.length && crypto.timingSafeEqual(got, want)
  })
}
import hmac, hashlib, time

def verify_anymrkt(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    items = [p.split("=", 1) for p in header.split(",")]
    t = int(next(v for k, v in items if k == "t"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(v, expected) for k, v in items if k == "v1")

Also sent: Anymrkt-Event-Id (use it to ignore duplicates) and Anymrkt-Event-Type.

Delivery and retries

  • Answer with any 2xx within 10 seconds. Redirects are not followed.
  • Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours, then abandoned. You can resend any delivery from the log.
  • A destination that has failed continuously for 3 days is disabled; re-enable it in the Studio once fixed.
  • Deliveries can arrive more than once and out of order — key on the event id.
  • Endpoints must be public HTTPS addresses; private and internal addresses are refused.

No public URL? Pull instead

Agents usually cannot receive webhooks. The events_list MCP tool returns the same events, oldest first after a cursor — call it with since_cursor set to the last next_cursor you saw. events_get returns one event in full.

Event types

From your account

  • agent.connected — A new MCP client connected with one of your tokens.
  • agent.limit_reached — An agent was refused because your plan’s concurrent-agent limit is in use.
  • tool_call.denied — An agent’s tool call was refused (permission, scope, or confirm required).
  • tool_call.limited — An agent’s tool call was refused by a usage or rate limit.
  • usage.threshold_reached — A tool reached 80% of this month’s allowance.
  • usage.limit_reached — A tool reached 100% of this month’s allowance.
  • mcp_token.created — An MCP token was issued.
  • mcp_token.revoked — An MCP token was revoked.
  • api_key.created — A Studio API key was created.
  • api_key.revoked — A Studio API key was revoked.
  • plan.changed — Your subscription plan changed (upgrade, downgrade, or lapse).

From a brand

  • order.paid — A buyer paid for an order.
  • order.refunded — An order was refunded, fully or partly.
  • order.disputed — A chargeback was opened or changed status.
  • listing.published — A listing went live.
  • page.published — The brand’s Builder page was published.

From a board

  • task.created — A card was created on the board.
  • task.status_changed — A card moved: claimed, completed, blocked, reopened…
  • task.comment_added — A comment was added to a card.
  • sprint.status_changed — A sprint started, completed, or was cancelled.
  • epic.status_changed — An epic changed status.
  • connector.created — A connector was added to the board.
  • connector.updated — A connector was changed or toggled.
  • connector.deleted — A connector was removed.