The account's integration-health alerts, newest first: every open one and those resolved in the last 30 days. Three kinds, each read every 5 minutes from live traffic in the request log and the delivery log: webhook_failing (more than half of an endpoint's attempts in the last hour failed on its side for events that have not arrived since, five at least, with two such events 23 minutes past their first attempt, or three events in a row were lost after their whole retry ladder and one in the hour has not arrived; a failure the retry recovered is not counted, and it resolves once two events arrived and none is missing; failures on our side are counted apart as platform_failed; an endpoint that takes test events only is not read, nor a sandbox event sent to one that takes both; it stays open, saying so, once the endpoint is switched off after its consecutive failures, which webhook.disabled announces), error_rate_spike (live calls failing well above the account's own rate over the 24 hours before, or the 7 days before when that day held fewer than 20 live calls: at least 20 calls and 10 failures in the hour, at least 10% of them and three times that rate, or 30% when even the week held fewer than 20; failures are 5xx answers and 401, 402, 403 and 429, not 400, 404, 409 or 422 answers about one request; while it is open, an hour with fewer than 10 live calls is read over the account's newest 10 instead, and fewer than that keeps it open, since no calls is no recovery) and key_expiring (a live key that stops working within 7 days, by its expiry or because it was rolled, that answered at least 3 calls in the last day and one in the last 6 hours; once the key stops, it stays open until 6 hours pass without a call since the stop; a key that went quiet and was called again before the same stop opens again without a notification or an email, and stays open until it stops). Each opens once, stays open while its condition holds, and resolves on a clearly better reading, never within 30 minutes of opening unless what it was about is gone; one with nothing to read for 7 days (no delivery attempt, or no live call) resolves as dormant, which is no recovery. evidence holds the numbers that opened it and latest the newest reading. Opening and resolving are the alert.opened and alert.resolved events (sent to the account's own live endpoints, an opened webhook_failing never to the endpoint it is about and a key_expiring to none; an error_rate_spike's top_errors and example_request_ids are empty in them), a notification in the dashboard, and an email when it opens (at most three a day). Best-effort signals over our own logs: one may miss a problem or open on one that fixes itself. What a credential is shown: a test key, none (every alert is read from live traffic); a webhook_failing alert needs webhooks:manage too, and a connected app sees those of the endpoints it subscribed; a key created with a scope list sees the key_expiring alert about itself and a connected app none; and an error_rate_spike alert's top_errors and example_request_ids are empty but to a key with no scope list. GET /events, the delivery log and a replay show the two events to a key with no scope list only. Paged by cursor (see Pagination); status and kind filter it, and an unknown value is refused.
- Credential
- An API key in
x-api-key - Scopes
events:read
Parameters
statusquery · stringopenfor the alerts still open,resolvedfor the resolved ones. Omit for both.One of
openresolvedkindquery · stringOne kind of alert. Omit for every kind.
One of
webhook_failingerror_rate_spikekey_expiringlimitquery · integerAlerts per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused.
Default 20
cursorquery · stringOpaque cursor: send back
meta.next_cursorfrom the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (onlylimitmay change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins overpagewhen both are sent.
Request
GET /api/v1/alerts
curl "https://portal.printedsimply.com/api/v1/alerts" \
-H "x-api-key: ps_test_YOUR_KEY"Response 200
alerts (newest first) + history_days; meta carries has_more and next_cursor
data
alertsobject[]requiredalerts[].idstringrequiredalerts[].kindstringrequiredWhich rule:
webhook_failing,error_rate_spikeorkey_expiring.One of
webhook_failingerror_rate_spikekey_expiringalerts[].statusstringrequiredOne of
openresolvedalerts[].subject_typestringrequiredWhat it is about:
webhook(subject_id is the endpoint's id, as GET /webhooks lists it),api_key(a key's id) oraccount(subject_idlive: the account's live calls).One of
webhookaccountapi_keyalerts[].subject_idstringrequiredalerts[].summarystringrequiredOne sentence: what the newest reading says while it is open, or why it resolved.
alerts[].opened_atstring (date-time)requiredalerts[].resolved_atstring (date-time) | nullrequiredNull while it is open.
alerts[].resolutionstring | nullrequiredWhy it resolved:
cleared(the reading got clearly better),subject_removed(the endpoint or key was deleted),endpoint_switched_off(you switched the endpoint off, or set it to take test events only),key_stopped(the key was revoked, or stopped working and is no longer called) ordormant(nothing to read for 7 days: no delivery attempt, or no live call; not a recovery). Null while it is open.One of
clearedsubject_removedendpoint_switched_offkey_stoppeddormantalerts[].last_evaluated_atstring (date-time)requiredWhen the alert was last read; it is read every few minutes while open.
alerts[].evidenceobjectrequiredThe reading that opened it.
alerts[].evidence.endpoint_hoststringThe endpoint URL's host, never its path or query.
alerts[].evidence.window_minutesintegeralerts[].evidence.attemptsintegerAttempts in the window, those that failed on our side (
platform_failed) left out.alerts[].evidence.failedintegerThose that failed on the endpoint's side, for events that have not arrived since: a failure the retry recovered is not counted.
alerts[].evidence.failure_rationumberfailed / attempts, to three decimals; 0 when nothing was sent.0 to 1
alerts[].evidence.platform_failedintegerAttempts in the window that failed on our side (the
egress,signingandinternalcauses): counted apart and never against the endpoint. /status counts them.alerts[].evidence.newest_deliveredboolean | nullWhether the endpoint's newest attempt, at any time, was delivered; null when nothing was ever sent to it.
alerts[].evidence.consecutive_failuresintegerEvents in a row lost after their whole retry ladder: the count that switches an endpoint off.
alerts[].evidence.disabledbooleanWhether it was switched off after its consecutive failures (
webhook.disabled).alerts[].evidence.top_causesobject[]The window's failures by cause, either side's, most first, at most three.
alerts[].evidence.top_causes[].causestringrequiredOne of the delivery log's causes.
One of
dnsunsafe_urlrefusedtimeouttlsresetnetworkredirectunauthorizedhttp_4xxhttp_5xxinbox_refusedegresssigninginternalalerts[].evidence.top_causes[].countintegerrequiredalerts[].evidence.example_order_idsstring[]Orders (
portal_order_id) whose events failed in the window and have not arrived (lost, or still retrying), newest first, at most three.alerts[].evidence.callsintegerLive calls in the window.
alerts[].evidence.errorsintegerThose answered 5xx, 401, 402, 403 or 429.
alerts[].evidence.error_ratenumbererrors / calls, to three decimals.0 to 1
alerts[].evidence.baseline_hoursintegeralerts[].evidence.baseline_callsintegeralerts[].evidence.baseline_errorsintegeralerts[].evidence.baseline_ratenumber0 to 1
alerts[].evidence.top_errorsobject[]The window's failures by error code and status, most first, at most three.
alerts[].evidence.top_errors[].codestring | nullrequiredThe answer's
error.code; null when it carried none.alerts[].evidence.top_errors[].statusintegerrequiredalerts[].evidence.top_errors[].countintegerrequiredalerts[].evidence.example_request_idsstring[]The
request_idof the window's newest failures, at most three. Empty, astop_errorsis, to a credential that reads only its own calls, and in thealert.*events.alerts[].evidence.key_prefixstringThe key's prefix, as the API keys page shows it; never more of the key.
alerts[].evidence.stop_reasonstringrotated(it was rolled, and works until its grace ends) orexpires(its own expiry).One of
rotatedexpiresalerts[].evidence.stops_atstring (date-time)alerts[].evidence.last_call_atstring (date-time) | nullalerts[].latestobjectrequiredThe newest reading while it was open: the one it resolved on, once resolved.
The same fields as
alerts[].evidencehistory_daysintegerrequiredHow long a resolved alert stays listed.
meta
pageintegerdeprecatedThe page number of the deprecated offset walk, on the lists that published it (orders and an order's events, on every request without
cursor; products, on a request that sentpage). Absent on a cursor walk, where no page number would be true.totalintegerEvery row matching the request's filters, on the lists that count them (orders, an order's events, assets and products).
limitintegerThe page size applied, which is not necessarily the one asked for: a larger
limitis clamped. Absent only on a GET /products request withoutcursorthat sent nolimit, which answers every match.pagesintegerdeprecatedtotaloverlimit, rounded up, besidepage(orders and an order's events). Absent on a cursor walk.next_cursorstring | nullrequiredSend it back as
cursorfor the next page; null exactly whenhas_moreis false. Opaque: pass it back unchanged. Good only on the list that returned it, with a key of the same account and mode (test or live), and with the same filters.has_morebooleanrequiredWhether another page follows this one. Decided from one row past this page, never from a page that came back short — so a page after
has_more: truecan come back empty only if rows were deleted in between, and the walk still ends at the firsthas_more: false.sincestring | nullOn GET /order/{id}/events only: where to ask from next — the position of the last event in this answer, or, when it has none, the position the request read from. Send it back as
since(withwait_secondsto wait for the next event). Unlikenext_cursorit is not null at the end of the history; it is null only when the order has no events and the request named no position. Opaque, and bound like a cursor to this order, account and key mode.
{
"status": "success",
"data": {
"alerts": [
{
"id": "cmb4alrt0000example0001",
"kind": "webhook_failing",
"status": "open",
"subject_type": "webhook",
"subject_id": "whk_SV8d5wGm2kUnSwx_RhZfY9OE",
"summary": "7 of 9 deliveries to example.com failed in the last 60 minutes.",
"opened_at": "2026-10-10T08:15:00.000Z",
"resolved_at": null,
"resolution": null,
"last_evaluated_at": "2026-10-10T08:25:00.000Z",
"evidence": {
"endpoint_host": "example.com",
"window_minutes": 60,
"attempts": 9,
"failed": 7,
"failure_ratio": 0.778,
"platform_failed": 0,
"newest_delivered": false,
"consecutive_failures": 0,
"disabled": false,
"top_causes": [
{
"cause": "http_5xx",
"count": 6
},
{
"cause": "timeout",
"count": 1
}
],
"example_order_ids": [
"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
]
},
"latest": {
"endpoint_host": "example.com",
"window_minutes": 60,
"attempts": 9,
"failed": 7,
"failure_ratio": 0.778,
"platform_failed": 0,
"newest_delivered": false,
"consecutive_failures": 0,
"disabled": false,
"top_causes": [
{
"cause": "http_5xx",
"count": 6
},
{
"cause": "timeout",
"count": 1
}
],
"example_order_ids": [
"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"
]
}
}
],
"history_days": 30
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a",
"meta": {
"limit": 20,
"has_more": false,
"next_cursor": null
}
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors.Missing, invalid or expired API key:
missing_api_key(none sent),invalid_api_key(unknown or revoked),key_expired(past the expiry set when it was created; create a new key).Rate limit exceeded. Two windows can produce this status and
error.codesays which:rate_limit_exceededfor the per-minute sliding window,daily_quota_exceededfor the plan's daily quota. Both carryRetry-After; the daily one also carriesX-Daily-LimitandX-Daily-Remaining. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.