Skip to content

Endpoint stats & status page ​

Emithook already counts everything that happens to your webhooks: what arrived, what was delivered, what failed and how long delivery took. Two calls let you put those numbers in front of your own users, so you don't have to keep a second set of counters.

  • Stats JSON: GET /v1/endpoints/{id}/stats returns one endpoint's numbers, for you to render however you like.
  • Status page: GET /v1/endpoints/{id}/stats/embed returns a short-lived signed link to a ready-made HTML page. Anyone can open it or put it in an <iframe> without an API key.

Both need a read-scoped key.

Get the numbers ​

bash
curl -s "https://api.emithook.com/v1/endpoints/<endpoint-id>/stats?window=24h" \
  -H "Authorization: Bearer $EMITHOOK_API_KEY"
json
{
  "endpoint": { "id": "ep_01JX9", "name": "Shopify orders", "url": "https://wh.emhk.in/acme/shopify/orders", "status": "active" },
  "window": "24h",
  "from": "2026-09-18T10:25:00.000Z",
  "to": "2026-09-19T10:25:00.000Z",
  "bucket_seconds": 3600,
  "generated_at": "2026-09-19T10:25:00.000Z",
  "rollup_fresh_at": "2026-09-19T10:23:00.000Z",
  "last_received_at": "2026-09-19T10:24:51.000Z",
  "totals": {
    "received": 13714, "delivered": 13542, "failed": 52, "dlq": 0, "retrying": 3, "unrouted": 0,
    "success_rate": 0.9962,
    "verification": { "passed": 13714, "failed": 0, "none": 0 }
  },
  "latency_ms": { "p50": 91, "p95": 244, "p99": 480 },
  "series": [{ "start": "2026-09-18T10:25:00.000Z", "received": 571, "delivered": 566, "failed": 0, "dlq": 0 }],
  "destinations": [
    { "id": "dst_01JX9SQS", "name": "Orders queue", "type": "sqs", "attempted": 13594, "delivered": 13542,
      "failed": 52, "retrying": 0, "success_rate": 0.9962, "latency_ms": { "p50": 60, "p95": 180, "p99": 390 } }
  ]
}
FieldMeaning
window24h (the default, 24 one-hour buckets), 7d or 30d (one-day buckets), measured back from now. These are the same numbers the console shows for the endpoint and window.
totals.receivedEvery event the endpoint accepted in the window.
totals.delivered / failed / dlqEvents that ended delivered, failed, or dead-lettered. retrying are still being retried. unrouted had no route to forward them to.
totals.success_ratedelivered / (delivered + failed + dlq). null when nothing has settled yet, never a made-up 0.
latency_msDelivery round-trip to your destinations. Over more than one hour it is a volume-weighted mean of the hourly percentiles, not an exact window percentile.
destinationsThis endpoint's traffic per destination, even when a destination is shared with other endpoints. attempted counts events actually sent to it: a route filter's skips and mirror copies are left out. Destinations the endpoint routes to now but that saw nothing in the window are listed with zeros.
rollup_fresh_atThe numbers come from rollups that update every few minutes; this says when they last moved.
last_received_atLive: the newest request that reached this endpoint's URL, including requests held while it was paused.

With the SDK:

ts
const stats = await emithook.getEndpointStats('ep_01JX9', { window: '7d' })
console.log(`${(stats.totals.success_rate ?? 1) * 100}% delivered`)

Show a status page ​

Mint a link on your server, then give only the URL to the browser. The API key never leaves your server.

bash
curl -s "https://api.emithook.com/v1/endpoints/<endpoint-id>/stats/embed?ttl=3600&theme=auto" \
  -H "Authorization: Bearer $EMITHOOK_API_KEY"
# → { "url": "https://api.emithook.com/embed/stats/v1.eyJ…", "expires_at": "2026-09-19T11:24:00.000Z", "window": "24h", "theme": "auto" }
ts
// e.g. in the handler that renders your customer's integrations page
const { url } = await emithook.getEndpointStatsEmbed(endpointId, { ttl: 3600 })
res.render('integration', { statsUrl: url })
html
<iframe src="{{ statsUrl }}" title="Webhook status" style="width:100%;height:720px;border:0"></iframe>
ParameterDefaultMeaning
window24hThe tab the page opens on. Viewers can switch between 24h, 7d and 30d.
themeautoauto follows the viewer's light/dark setting, or force light / dark.
ttl3600Link lifetime in seconds, from 60 up to 604800 (7 days).

What the page shows, and what it never shows:

  • Shows: the endpoint's name, a health badge, received / delivered / success rate, latency (p50, p95, p99), when it last received a request, a per-hour or per-day chart, and each destination by name and type.
  • Never shows: the endpoint's receive URL (anyone who knows it can post to it), destination URLs or queue ARNs, API keys, or any payload.
  • Runs no script and loads nothing from anywhere else. It is a single HTML document with inline styles, and it refreshes itself every few minutes, so it stays current on a wall display until the link expires (7 days at most, so a permanent display needs a fresh link at least weekly).
  • The health badge reads the window's success rate, and the last ~2 hours override it once at least 5 deliveries settled or are retrying there. Recent retries count as not yet delivered, so a destination outage shows while it is happening; a single failure on a quiet endpoint doesn't turn it red.
  • Anyone holding the URL can view the page until expires_at. Nothing is stored server-side, so a single link can't be revoked. Keep ttl short and mint a fresh link each time you render the page.
  • A link keeps working after the API key that minted it is revoked, until it expires.
  • The token is readable, not secret in itself: it carries your org and endpoint ids (it can't be altered). Like any URL, it can also land in proxy and access logs.
  • Error pages carry no data: "This link has expired" (410), "Link not recognised" or "Endpoint unavailable" (404, a malformed link or a deleted endpoint), "Too many requests" (429, beyond 300 views of one endpoint's page per minute — the limit is per endpoint, so anyone holding the link can use it up for a minute; another reason to keep links short-lived), "Stats unavailable" (503, a temporary read failure).
  • Self-hosting: links are signed with EMBED_SIGNING_SECRET (at least 32 characters; when unset, a key derived from CONSOLE_JWT_SECRET). Changing it revokes every outstanding link, for every org, at once; there is no per-org revocation. Set API_BASE_URL to your public API origin: without it, links minted from inside the server (the hosted MCP server) are refused with 501 rather than pointing at an internal host.

From the CLI or an agent ​

bash
emithook endpoints stats ep_01JX9 --window 7d
emithook endpoints stats-embed ep_01JX9 --ttl 86400 --theme dark

Over MCP: get_endpoint_stats { endpoint, window? } and get_endpoint_stats_embed { endpoint, window?, theme?, ttl? }. Over MCP a link lasts at most 1 day.

  • The raw inbound requests, with filters: Requests & replay (GET /v1/requests?endpoint=&outcome=&since=&until=).
  • The events behind these numbers: GET /v1/events?endpoint=&status=&since=&until=.
  • Hourly rollup rows by id: GET /v1/metrics?key=<endpoint-or-destination-id>.

Emithook · a Finnoto product