The Node and Python SDKs
Two dependency-free wrappers around the same request, written so that nothing they do can raise inside your application. The document says where each one can be had today.
The two packages are not published yet.
npm i @silentoutage/sdkandpip install silentoutagewill answer 404 until the package registries carry them. The rawcurlpath below works today and is the same request both packages make, and nowhere else on this site is either install command offered to you as something that works.
Three ways to report a job or a business event, in descending order of how much you have to install:
| Install | What you get | |
|---|---|---|
| Next.js / Node | npm i @silentoutage/sdk | A route wrapper and a client object |
| Python | pip install silentoutage | A decorator, a context manager and a client |
| Raw `curl` | nothing | One line in your crontab |
All three speak the same protocol, because there is only one: POST /ping/<uuid>, with an optional /start, /success, /fail or /<exit status> on the end. The SDKs are a convenience over curl, never a second way in.
The rule the SDKs are built under
Failure to reach Silent Outage never breaks your application. Not "is unlikely to", not "is retried" — cannot. Concretely, in both SDKs:
- no call raises. Every one of them answers with a result object, whatever went wrong;
- every request carries a hard timeout — 3 s by default, and a ceiling of 10 s that
SILENTOUTAGE_TIMEOUT_MScannot be set past; - nothing is retried. A retry inside your request path spends your latency on our delivery, and the ping that did not arrive is exactly what the check on the other end is for;
- the wrappers re-raise your exception unchanged. An SDK that swallowed it would hide the bug it is there to report;
- there are no dependencies. Not "few" — none, in either package. A monitoring library with a dependency tree is one more way to break the thing it is monitoring.
Every one of those is asserted against a refused connection, a socket that accepts and never answers, a 500, a transport that throws, and a client with nothing configured at all.
Configuration
Both SDKs read the same environment variables, so one .env configures a Next.js app and the cron jobs beside it.
| Variable | Meaning |
|---|---|
SILENTOUTAGE_INGEST_URL | The ingestion origin. Needed only when you address checks by token rather than by full URL. |
SILENTOUTAGE_PROJECT_KEY | A scoped API key (sok_…, scope checks:read). Used only to turn a check name into its ping URL. |
SILENTOUTAGE_PROJECT_ID | Which project names are looked up in. Only needed when the key can see more than one. |
SILENTOUTAGE_API_URL | Your Silent Outage app's origin, for that lookup. |
SILENTOUTAGE_CHECK_<NAME> | A check's ping URL or token, pasted in. Resolves that name with no lookup at all. |
SILENTOUTAGE_TIMEOUT_MS | Per-request budget. Clamped to 100–10000. |
SILENTOUTAGE_DISABLED | 1 makes every call a no-op. For local development and CI. |
There is no default ingestion hostname in either SDK. An unset SILENTOUTAGE_INGEST_URL means a bare token cannot be addressed and the SDK says so, rather than posting your heartbeats to whatever host a default once named.
Addressing a check: three forms, and which to use
silentoutage.event('signup') # a name
client.heartbeat('11111111-2222-4333-8444-555555555555') # a token + SILENTOUTAGE_INGEST_URL
client.heartbeat('https://<ping-host>/ping/1111…') # the full URL the dashboard showsA name is resolved in two steps, in this order:
SILENTOUTAGE_CHECK_<NAME>in the environment —signup→SILENTOUTAGE_CHECK_SIGNUP,nightly report→SILENTOUTAGE_CHECK_NIGHTLY_REPORT. No network call, no credential.- Otherwise one authenticated
GET /api/v1/projects/{id}/checkswith the project key, memoised for the life of the process.
Prefer step 1 for anything whose silence would page somebody. Step 2 is convenient — it is what makes event('signup') work with nothing but a key in the environment — but it adds a failure mode step 1 does not have: a revoked key or a rate-limited read silences the heartbeat, and a silenced heartbeat is a false page. The directory is read once per process and never on the ping path itself.
The project key is never sent to the ingestion edge. The ping token in the path is the whole credential ingestion understands; adding a scoped API key would put it on a host with no use for it, and in the access log of every hop in between. Both SDKs' tests assert the Authorization header is absent from every ping.
Next.js / Node
npm i @silentoutage/sdk
// app/api/cron/nightly/route.ts — the one-line wrapper.
import { withHeartbeat } from '@silentoutage/sdk';
export const GET = withHeartbeat('nightly-report', async () => {
await rebuildReports();
return Response.json({ ok: true });
});That sends /start, runs your handler, then /success with the measured duration — or /fail with the error text, before re-throwing your error unchanged.
// app/api/signup/route.ts — a business event.
import { event } from '@silentoutage/sdk';
export async function POST(request: Request) {
const user = await createUser(await request.json());
await event('signup');
return Response.json({ id: user.id });
}A business event is an occurrence, not a job report: one bare ping against the business-event check that carries that name. There is one check per event name, because the check's state, its incident and its flapping counter are all per check.
For anything that is not a route handler:
import { SilentOutage } from '@silentoutage/sdk';
const silentoutage = new SilentOutage(); // reads the environment
await silentoutage.monitor('import-orders', async () => importOrders());
await silentoutage.start('import-orders');
await silentoutage.fail('import-orders', { exitStatus: 137, body: tailOfTheLog });Both pings in a wrapper are awaited on purpose. On a serverless runtime the process is frozen the moment your handler resolves, so a fire-and-forget ping is a ping that silently does not arrive. Pass { start: false } to skip the opening ping in a latency-sensitive route; the terminal one still carries the SDK's own measured duration.
The package is ESM and has no dependencies. Node ≥18.17.
Python
pip install silentoutage
import silentoutage
@silentoutage.monitor("nightly-report") # decorator
def nightly() -> None:
rebuild_reports()
with silentoutage.monitor("import-orders"): # …or context manager, same object
import_orders()
silentoutage.event("signup") # a business eventThe decorator and the context manager are the same thing: monitor() returns a contextlib.ContextDecorator. Both send /start, then /success or /fail with the measured duration, and neither suppresses your exception.
A SystemExit carrying an integer code is reported as that exit status — the same fact curl "$URL/$?" sends from a shell — so sys.exit(3) inside a monitored block arrives as /ping/<uuid>/3.
client = silentoutage.SilentOutage(ingest_url="https://<ping-host>") client.start(token) client.fail(token, exit_status=137, body=tail_of_the_log)
Standard library only. Python ≥3.9.
Raw curl
Healthchecks parity, and the zero-dependency path. Everything the SDKs do, a crontab can do with the URL the dashboard shows:
# it ran curl -fsS -m 10 "https://<ping-host>/ping/<uuid>" # start / finish, so the run's duration is measured curl -fsS -m 10 "https://<ping-host>/ping/<uuid>/start" ./nightly-report && curl -fsS -m 10 "https://<ping-host>/ping/<uuid>" # the one-liner a cron line actually carries: report whatever the job exited with ./nightly-report; curl -fsS -m 10 "https://<ping-host>/ping/<uuid>/$?" # with the tail of the log attached (truncated at 10 KiB, never rejected) ./nightly-report 2>&1 | tail -c 10000 | \ curl -fsS -m 10 --data-binary @- "https://<ping-host>/ping/<uuid>/$?"
GET, HEAD and POST are all accepted, so wget --spider and curl -X HEAD work too. The -m 10 matters for the same reason the SDK has a timeout: a hung curl in a cron job is a cron job that never finishes.
Why the SDKs are MIT
The Silent Outage server is AGPL-3.0-only. Both SDKs are MIT, and that is deliberate.
The AGPL is chosen to close the hosted-service loophole: the risk it guards against is a competitor running our server as a rival service. A client library is not that. It is installed into your application, and a copyleft client would place a source-disclosure obligation on every application that sends us a heartbeat — which is both a trap for you and the end of the promise that there is nothing to install on your server.
The permissive licence is only honest if no copyleft code travels inside it, so neither SDK depends on anything in the server: each carries its own copy of the ping protocol. Every copy is compared against the server's own on every build, all three implementations are compared against each other, and either package growing a dependency is a build failure.