Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/uptimemonitoring-com:uptimemonitoring-com-webhook-deliveries-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: UptimeMonitoring.com Webhook Deliveries API
version: 0.1.0
description: 'API-first uptime monitoring for developers, deploy pipelines, and AI agents.
## Authentication
All `/api/v1` endpoints require a Bearer token — either an API key (`umk_live_*`)
or a session token obtained via GitHub OAuth. The `/mcp` endpoint additionally
accepts OAuth 2.1 JWT access tokens (`eyJ...`) issued via the self-hosted
authorization server documented under the `OAuth` tag.
```
Authorization: Bearer umk_live_abc123...
```
## Rate limits
200 requests/minute per account. Exceeding the limit returns `429` with a
`Retry-After` header.
Responses that pass through the per-account rate-limiter carry the
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`
headers (decimal integers; `Reset` is a Unix epoch second at which
the bucket would be full again). In practice this is every
authenticated response under `/api/v1/*` whose request reached the
handler — earlier failures from the auth middleware (e.g. 401 missing
credentials, 403 suspended account) return before the limiter runs
and do NOT carry the trio. Routes mounted outside `/api/v1`
(notably `GET /api/v1/events/stream`, which has its own SSE
middleware stack) are not subject to this limiter and likewise do
not emit these headers. Successful responses on
`POST /api/v1/monitors` reflect the **global** 200/min bucket; the
per-route 20/min creation limit only surfaces its own headers when
it 429s. The trio is documented once here rather than referenced
into every individual response — see `components.headers` for the
schemas.
One exception: once a request presenting an API key enters an
authenticated route group through a registered route or mounted
subtree, while that key remains in the bounded pre-auth cache
(entries expire after 5 minutes, but may be evicted sooner once the
cache is at capacity), the global bucket is charged BEFORE
authentication and authorization run (issue #47) — so for that
cached key only, a `401` (key revoked while still cached), `403`,
`500`, or a `404` for an unmatched path inside a mounted subtree
also carries the `X-RateLimit-*` trio and spends one token. `429`
still comes first whenever the bucket is already empty. An uncached
key (first seen, expired, or evicted early at capacity), a session
token, an OAuth JWT, and any path outside every authenticated group
are all unaffected.
## Browser push notifications
Subscriptions registered via `POST /api/v1/push/subscribe` receive
fire-and-forget Web Push notifications on monitor state transitions
(down/up/flapping); per-monitor 5-minute throttle. No public API change
in this release — dispatch is internal to `internal/push.Dispatcher`.'
contact:
name: UptimeMonitoring.com
url: https://uptimemonitoring.com
license:
name: Proprietary
identifier: LicenseRef-Proprietary
servers:
- url: https://api.uptimemonitoring.com
description: Production
tags:
- name: Webhook Deliveries
paths:
/api/v1/webhook-deliveries:
get:
operationId: listWebhookDeliveries
summary: List recent webhook delivery attempts for the authenticated account
description: 'Two independent response shapes, selected by `?order=`:
- Default (`order` key absent entirely): offset mode. `created_at`
descending, `?limit=`/`?offset=`, `WebhookDeliveryListResponse`.
Unchanged since before issue #396 apart from the added
`enqueued_at` field on each delivery. A present `order` key —
even an empty `?order=` or a bare `?order` — never falls back
to this mode; see below.
- `order=id`: cursor mode (issue #396). `id` ascending,
keyset-paginated via `?cursor=`, `WebhookDeliveryCursorListResponse`.
Any `?offset=` (any value, including `0`) is rejected with 400.
A malformed or empty `?cursor=` is rejected with 400 — presence
of the parameter is checked independently of its value, so a
present-but-empty `?order=id&cursor=` is rejected the same as a
malformed one, not treated as "no cursor". `?cursor=` without
`order=id` is also rejected with 400, empty value included — a
cursor only has meaning in keyset order. `order` itself must
be present exactly once with the value `id`; a present but
empty `?order=`, a bare `?order`, a repeated `order=` key, or
any other value is rejected with 400 rather than silently
running offset mode. A malformed query string itself (e.g. an
unescaped `;`) is rejected with 400 before any parameter is
inspected.
`?recipient_id=` filters to one recipient''s own deliveries
(`webhook_deliveries.webhook_id`), still scoped to the caller''s
account. `?status=` is repeatable (OR-union) — a single
`status=x` behaves exactly as before issue #396.
**Recipient-queue drain recipe.** To prove a recipient''s
pre-attachment backlog is drained before deleting it (e.g.
rotating to a replacement recipient via
`DELETE /api/v1/recipients/{id}?replaced_by=`): scan
`?recipient_id={id}&status=pending&status=retrying&order=id`,
following `next_cursor` until it is `null`. Only an empty scan
result proves the queue is literally empty; any row the scan
returns proves the opposite, since the status filter selects
live rows by construction. The backlog is considered drained
once every returned row''s `enqueued_at` is non-null and greater
than the replacement''s `attached_at` on every monitor
(`GET /api/v1/monitors/{id}/recipients`''s `attached_at` field) —
but that timestamp comparison alone does not prove sibling
coverage (that each row has a counterpart delivery enqueued for
the replacement); it only establishes that a live row postdates
the replacement''s attachment. `DELETE /api/v1/recipients/{recipientID}`
is the authoritative check for a safe rotation — it verifies
sibling coverage server-side, in one transaction, rather than
trusting a client-computed timestamp comparison. See that
endpoint for the full recipe and its guarantees/limits.'
security:
- bearerAuth: []
parameters:
- name: monitor_id
in: query
schema:
type: integer
format: int64
- name: recipient_id
in: query
description: Filter to one recipient's deliveries (issue
schema:
type: integer
format: int64
- name: status
in: query
style: form
explode: true
description: 'Repeatable (issue #396): `status=pending&status=retrying`
returns the OR-union. A single `status=x` behaves exactly as
on master.
'
schema:
type: array
items:
type: string
enum:
- pending
- retrying
- delivered
- failed
- superseded
- cancelled
- name: since
in: query
schema:
type: string
format: date-time
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 50
- name: offset
in: query
description: 'Offset-mode only. Rejected with 400 (any value, including
`0`) when combined with `order=id`.
'
schema:
type: integer
minimum: 0
default: 0
- name: order
in: query
description: 'Omit for the default offset-mode shape. `id` switches to
cursor mode (issue #396). When present, the parameter must
appear exactly once with the value `id` — an empty
`?order=`, a bare `?order`, a repeated `order=` key, or any
other value is rejected with 400 rather than silently
falling back to offset mode.
'
schema:
type: string
enum:
- id
- name: cursor
in: query
description: 'Cursor-mode only (issue #396) — the opaque `next_cursor` value
from a previous page. Requires `order=id`.
'
schema:
type: string
responses:
'200':
description: Deliveries
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/WebhookDeliveryListResponse'
- $ref: '#/components/schemas/WebhookDeliveryCursorListResponse'
'400':
description: 'Bad request — an unknown `status` value; an `order` that is
empty, repeated, or anything other than `id` (including a
bare `?order`); a malformed `cursor`; `cursor` supplied
without `order=id`; or `offset` supplied together with
`order=id`.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Webhook Deliveries
components:
schemas:
WebhookDelivery:
type: object
properties:
id:
type: integer
format: int64
webhook_id:
type: integer
format: int64
description: 'A recipient id (issue #212) — the field name is kept for wire
compatibility; it has not named a row in the legacy `webhooks`
table since the recipients migration. A row can represent an
email delivery (issue #321), a Slack post (issue #359), or a
PagerDuty Events API v2 event (issue #363) as well as a
webhook POST — check the referenced recipient''s `type` to tell
which; `last_response_code` is SendGrid''s status for email
(202 on success, 0 on transport error), the destination''s HTTP
status for webhook, Slack''s own HTTP status for slack
(`response_body` carries Slack''s error string, e.g.
`"no_service"`), and PagerDuty''s own HTTP status for pagerduty
(`response_body` carries FIXED outcome text, e.g.
`"PagerDuty accepted the event (HTTP 202)"` or
`"PagerDuty rate-limited the event (HTTP 429)"` — never
PagerDuty''s actual response body, which could echo the
routing key) — for both slack and pagerduty, only 429 and 5xx
retry; every other non-2xx, 3xx included, is terminal on the
first attempt. A pagerduty row with no `outage_id` — enqueued
before issue #363, or an "up" recovery whose onset transition
left no open incident to inherit an outage id from — fails
immediately with `response_body` `"no outage key"` and no
request ever sent; such a recovery is never reported to
PagerDuty.
'
monitor_id:
type: integer
format: int64
account_id:
type: integer
format: int64
event_type:
type: string
example: down
payload_hash:
type:
- string
- 'null'
status:
type: string
enum:
- pending
- retrying
- delivered
- failed
- superseded
- cancelled
description: '`superseded` (added for issue #165): an older `pending`/`retrying`
delivery for the same webhook+monitor pair was replaced by a
newer one, bounding a swarm of stale notifications after an
outage recovers to at most one live delivery per pair at a
time. Not immutably terminal: the
replacement is not atomic with dispatch, so a delivery already
in flight when it was superseded can still complete and
transition to `delivered` for that one attempt — a subscriber
can still receive a delivery for a transition that was
superseded before it arrived.
`cancelled` (added for issue #338): a delayed `down` delivery
whose outage recovered before its `down_delay_s` elapsed, or
whose recipient''s events narrowed to drop `down` while the
row was still pending. Never reused for `superseded` — nothing
replaced the row, it was simply never sent. Also not
immutably terminal: a row already claimed for dispatch when an
events-narrowing edit cancels it can still complete and
transition to `delivered`.
'
attempt_count:
type: integer
collapsed_count:
type: integer
description: 'The number of older live (`pending`/`retrying`) delivery rows
this row superseded, plus their own accumulated collapsed
counts. Zero for a delivery that superseded nothing. This is
NOT a count of POSTs guaranteed suppressed, and NOT a count of
transitions the subscriber missed: a superseded row can still
be in flight and reach `delivered` after being counted here.
Mirrors the outbound `collapsed_transitions` payload field.
'
next_attempt_at:
type: string
format: date-time
last_attempt_at:
type:
- string
- 'null'
format: date-time
last_response_code:
type:
- integer
- 'null'
response_body:
type:
- string
- 'null'
description: 'Truncated copy (≤4096 bytes, NUL-stripped, valid UTF-8) of the
subscriber''s HTTP response body from the last attempt, for
`type=webhook`/`type=email`/`type=slack`. Present to help
debug rejected deliveries; null for rows recorded before this
was captured or when the subscriber returned no body. For
`type=pagerduty` (issue #363), this is NEVER the actual
response body — it is always one of a small set of fixed
outcome strings (e.g. `"PagerDuty accepted the event (HTTP
202)"`, `"PagerDuty rejected the event (HTTP 400)"`,
`"PagerDuty request failed (timeout)"`, or `"no outage key"`),
because a real PagerDuty response body could echo the request
back, including the routing key.
'
created_at:
type: string
format: date-time
cancelled_at:
type:
- string
- 'null'
format: date-time
description: Set only when status is `cancelled` (issue
enqueued_at:
type:
- string
- 'null'
format: date-time
description: 'The Postgres server clock''s own stamp of this row''s insert
moment (issue #396) — distinct from `created_at`, which is
the dispatcher''s own Go-clock transition time and so cannot
be compared against anything the database generates. Null
for a row written before this column existed; every reader
treats null as "before any boundary". Used by the
`?recipient_id=&order=id` drain scan (see
`/api/v1/webhook-deliveries`''s description) and the guarded
`DELETE /api/v1/recipients/{recipientID}?replaced_by=`
endpoint to decide whether a row predates a replacement
recipient''s attachment.
'
Error:
type: object
required:
- error
properties:
error:
type: string
example:
error: something went wrong
WebhookDeliveryCursorListResponse:
type: object
description: 'The response shape for `?order=id` (issue #396) — keyset-paginated,
ascending by `id`. No `total`/`offset`: cursor mode never runs the
COUNT(*) that would produce one. `next_cursor` is always present,
even on the last page, where its value is `null`.
'
required:
- deliveries
- limit
- next_cursor
properties:
deliveries:
type: array
items:
$ref: '#/components/schemas/WebhookDelivery'
limit:
type: integer
next_cursor:
type:
- string
- 'null'
description: 'Opaque base64url token. Pass back verbatim as `?cursor=` to
fetch the next page. `null` means no further page exists.
'
WebhookDeliveryListResponse:
type: object
properties:
deliveries:
type: array
items:
$ref: '#/components/schemas/WebhookDelivery'
total:
type: integer
limit:
type: integer
offset:
type: integer
responses:
RateLimited:
description: 'Per-account dispatcher rate limit exceeded (200 requests/minute).
The `Retry-After` header gives the integer seconds until at least
one token has refilled. The `X-RateLimit-*` trio reflects the
bucket whose rejection produced this response — usually the global
200/min cap; on `POST /api/v1/monitors` a 429 may instead reflect
the per-route 20/min creation limit.
'
headers:
Retry-After:
schema:
type: integer
description: Seconds until the next allowed request.
X-RateLimit-Limit:
$ref: '#/components/headers/XRateLimitLimit'
X-RateLimit-Remaining:
$ref: '#/components/headers/XRateLimitRemaining'
X-RateLimit-Reset:
$ref: '#/components/headers/XRateLimitReset'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: account rate limit exceeded; retry in 1s
headers:
XRateLimitReset:
schema:
type: integer
description: 'Unix epoch seconds at which the bucket would be full again if no
further requests are made. Sub-second fragments are rounded up so
`Reset - now` is never less than the actual refill duration.
'
XRateLimitLimit:
schema:
type: integer
description: 'Bucket capacity for the limiter that produced this response. `200`
for the global per-account dispatcher cap; `20` on a 429 from the
per-route monitor-create limiter.
'
XRateLimitRemaining:
schema:
type: integer
description: 'Tokens left in the bucket after this request was charged
(floor of the post-charge token count). `0` on a 429 means the
request was rejected with the bucket empty.
'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'API key (`umk_live_*`) or session token from OAuth exchange.
'