REST API
Read and write your monitors, read and update incidents, and upload source maps. Everything here uses the same permission checks and the same tenancy filter as the dashboard — there is no separate API-side implementation that could drift from it.
Included on every plan, the free one included. There is no add-on to buy and no tier that unlocks it.
Base URL
https://vitrinaengine.com/api/v1Machine-readable description
/openapi.json is an OpenAPI 3.1 document covering every route on this page. Point a client generator at it, or hand it to an agent that has to call this API without being talked through it.
It is generated from the service rather than written beside it: the routes come from the surface the API publishes at GET /, the shapes and their prose from the same .proto files at /api/proto, and the rate limits from the code that enforces them. A build fails when the committed document and the API disagree, so it describes what is served rather than what was true when somebody last edited it.
It describes the shapes. The reasons are on this page — that a cross-tenant id answers 404, that config is never returned — and no generator can write those.
For agents: MCP
https://vitrinaengine.com/mcp is a Model Context Protocol server. Add it to an MCP client with an API key as the bearer token, and an agent can read your monitors, incidents, errors and analytics — and create, change and delete monitors — without anybody explaining how this API works.
{
"mcpServers": {
"vitrina-engine": {
"type": "http",
"url": "https://vitrinaengine.com/mcp",
"headers": { "Authorization": "Bearer vte_your_key_here" }
}
}
}It reads everything and writes three things. Every GET operation on this page is a tool, and so are POST /monitors, PATCH /monitors/{id} and DELETE /monitors/{id}. Nothing else that writes is reachable — not a workspace, a member, an API key, a status page or your account — because no tool for any of them exists, and the list of writes that do is three lines somebody has to add to on purpose.
A key that cannot write is still refused. A tool call is the same request the key would make directly, through the same permission check and the same tenancy filter, so an agent holding a read-only key reads. Give it one unless you mean it to change your monitoring — and expect a client to ask you before it deletes, because deleting a monitor takes its history with it and there is no undo. To silence a monitor without losing it, pause it with PATCH.
Because it is the same request, it sees exactly what the key sees, writes the same audit entry as the dashboard would, and counts against the same rate limit — twice, once for the call and once for the request inside it. A write charges the write limit.
One thing worth knowing before an agent edits a monitor: config is replaced whole, not merged. Read the monitor first, change the one field, and send the whole object back. A partial config silently drops the headers and credentials the check depends on, and leaves it running.
Authentication
Create a key under Settings → API keys, then send it as a bearer token. The key is shown once, when it is created, and stored only as a hash — if you lose it, make another.
curl https://vitrinaengine.com/api/v1/summary \
-H "Authorization: Bearer vte_your_key_here"A key never grants more than the person who created it had. It is bound to their membership, so it carries their role: a key made by somebody with a read-only role cannot write, whatever you send it. A key can also be confined to a single workspace, in which case every response is filtered to that workspace and anything outside it does not exist as far as the key is concerned.
Removing somebody from your organisation revokes the keys they created, in the same operation. Revoking access has to revoke the credentials with it, or the person keeps a working one.
Conventions
Every successful response is a JSON object with a data property. Every failure is a JSON object with an error property holding a sentence meant for a human.
{ "data": { "id": "8f14e45f-…", "name": "Marketing site" } }
{ "error": "This key cannot create monitors." }Responses are sent Cache-Control: private, no-store. This is per-key data on a shared origin and must never be held by a proxy.
Protobuf and gzip
Every endpoint under /api/v1 also speaks Protocol Buffers. Send Accept: application/x-protobuf and the answer comes back as protobuf; send a protobuf body with Content-Type: application/x-protobuf and the answer comes back in the same format. Send neither and it is JSON, exactly as before.
curl https://vitrinaengine.com/api/v1/monitors \
-H "Authorization: Bearer vte_your_key_here" \
-H "Accept: application/x-protobuf" \
--compressed -o monitors.pbThe messages are published: /api/proto lists every file. Download them keeping their paths and point protoc -I at the directory. Three things differ from what a protobuf user might expect: a field that can be empty is null in JSON and absent in protobuf; a few fields whose shape varies — a monitor’s or a channel’s config, for instance — are JSON documents carried in a string; and StringList wraps a list that can itself be null.
Gzip works in both directions: Accept-Encoding: gzip for responses, Content-Encoding: gzip for requests. Because two callers can now get different bytes from one URL, every response carries Vary: Accept, Content-Type.
Status codes
| Code | Means |
|---|---|
200 | Fine. 201 when something was created. |
400 | The body was not JSON, a field is missing, or nothing was asked for. |
401 | No key, or not a valid one. Deliberately never says which — a message distinguishing them is a way to test keys. |
403 | A valid key without the permission for this action. The message names it. |
404 | No such record for you. See below. |
413 | A source map over the size cap. |
429 | Over a rate limit. Carries Retry-After. See below. |
Refusal codes
Every non-2xx body carries a code beside the sentence — not_found, limit_reached, rate_limited — and that is the field to branch on. The error string is prose for a person reading a log and may be reworded at any time; the code is stable. Where the sentence names a value, vars carries those values as strings, so you can put them where your own grammar wants them rather than parsing them back out of the English.
{
"error": "The Pro plan includes 50 monitors, and you have 50.",
"code": "limit_reached",
"vars": { "plan": "pro", "limit": "50", "current": "50", "resource": "monitors" }
}This is what our own apps render a refusal from, in the reader’s language. Treat a code you do not recognise the way they do — fall back to error — because codes are added without a version bump.
Rate limits
Per organisation, not per key — minting another key does not raise them.
| Bucket | Limit |
|---|---|
Reads (GET) | 120 a minute |
Writes (POST, PATCH, DELETE) | 30 a minute |
| Source map uploads | 300 an hour |
Over the limit answers 429 with a Retry-After header giving whole seconds until the window resets. Honour it rather than retrying immediately.
These are set where a normal integration never reaches them: a wallboard polling every ten seconds spends six of the 120 reads. They are the same on every plan, because the collection endpoints return everything in one response — an account with five hundred monitors does not need more requests than one with twenty. Source maps get an hourly bucket because they arrive in bulk at deploy time, where a front end can easily have a hundred chunks.
404 rather than 403, on purpose
An id belonging to another customer answers “Not found.”, not “Forbidden.” A 403 would confirm the record exists, which turns the endpoint into a way to test whether an id is real. Do not read a 404 as proof that nothing exists anywhere — only that nothing exists that this key may see.
Endpoints
GET /search
Everything in the account matching ?q=, whatever kind of thing it is — monitors, incidents and their postmortems, hosts, status pages, workspaces, notification channels, maintenance windows, error projects and issues, analytics sites, members and invitations, API keys, agents and private probes. One request rather than one per kind, because this is what a search box calls on a keystroke.
?type= narrows it to a comma-separated list of kinds: monitor, incident, postmortem, host, status_page, workspace, channel, maintenance, error_project, error_issue, analytics_site, member, invitation, api_key, agent, probe. An unknown one is refused rather than ignored, so a request that narrows cannot quietly get a wider answer. ?limit= caps how many of each kind come back, between 1 and 20, default 5. There is no cursor: search finds a thing, and each kind’s own collection endpoint pages through it.
Each kind is gated on the same permission its own endpoint needs, so search is never a way round one. A kind your key may not read is absent rather than refused, and absent in exactly the way a kind with no matches is. A term shorter than two characters answers with an empty list rather than an error — it means “keep typing”.
Nothing sealed, hashed or secret is matched against or returned: not a monitor’s configuration, not a channel’s, not an API key’s hash, not a heartbeat token. An API key is matched on its name and on the displayable prefix that exists to be shown.
{
"data": [
{
"type": "monitor",
"id": "3f1c…",
"title": "Checkout API",
"subtitle": "Acme",
"status": "up",
"workspace": { "id": "9b2e…", "name": "Acme" }
},
{
"type": "incident",
"id": "77a0…",
"title": "Checkout API timed out",
"subtitle": "Checkout API",
"status": "resolved",
"workspace": { "id": "9b2e…", "name": "Acme" }
}
]
}Parameters and responses
| Name | In | Type |
|---|---|---|
limit | query | integer |
q | query | string |
type | query | string |
| Response | SearchResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /summary
Counts, for a wallboard or a daily digest. Needs monitor:read.
{
"data": {
"total": 42, "up": 39, "degraded": 1, "down": 1,
"paused": 1, "pending": 0,
"openIncidents": 2, "suppressedIncidents": 1
}
}suppressedIncidents counts incidents held back because they are blast radius of another one. They are real, and they are not separate outages.
Parameters and responses
No declared parameters or body fields.
| Response | GetSummaryResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /setup
What a new account still has to do — the same four questions the dashboard’s setup checklist asks, derived on every read. Needs monitor:read.
{
"data": {
"hasMonitor": true,
"hasConfirmedChannel": false,
"hasStatusPage": false,
"hasColleague": false,
"dismissedAt": null
}
}hasConfirmedChannel asks whether an alert could actually arrive, not whether a channel exists. Those came apart badly once, and a checklist that ticked “alerts are set up” for a channel the notifier refuses would repeat that bug as reassurance.
hasColleague counts memberships and never invitations: an invitation sent and not accepted has added nobody.
Not folded into /summary, because that endpoint is counts by status and nothing else — a wallboard polling it every few seconds does not want five extra lookups to render a nudge aimed at somebody who set the account up months ago.
Parameters and responses
No declared parameters or body fields.
| Response | GetSetupResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
DELETE /setup
Skips the checklist. Needs org:update, because the dismissal is one organisation-wide row rather than a per-person preference — a colleague who arrives after it was skipped is not shown it again.
Nothing else about the account changes, and no audit entry is written: hiding a nudge changes nothing about how the product behaves, alerts or bills.
Parameters and responses
No declared parameters or body fields.
| Response | DismissedResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /monitors
Every monitor the key can see. Needs monitor:read. Each carries id, name, kind, status, statusSince, enabled, intervalSeconds, tags, workspace, lastCheckedAt, lastResponseTimeMs, lastMessage, uptime24h, uptime30d and openIncidentId.
?tag=prod,eu narrows the list to monitors carrying every tag named — AND, not OR, so naming a second tag always returns fewer monitors rather than more. The dashboard’s own filter reads the same way. Tags are stored folded to lower case, and the value here is folded the same way before it is compared, so ?tag=Prod finds them. An entry that could not be a tag — blank, or longer than a tag may be — is dropped and the rest of the filter still applies, rather than the call being refused: a link should not stop working because one of its tags was renamed.
config is never returned. For some kinds it holds request headers and credentials, and a read-scoped key should not be a way to read back the secrets somebody typed into a form.
Parameters and responses
| Name | In | Type |
|---|---|---|
tag | query | string |
| Response | ListMonitorsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /monitors
Needs monitor:create. Requires name, kind, workspaceId and config. Optional: intervalSeconds (default 300), confirmations, dependsOn.
curl -X POST https://vitrinaengine.com/api/v1/monitors \
-H "Authorization: Bearer vte_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Marketing site",
"kind": "http",
"workspaceId": "…",
"intervalSeconds": 60,
"config": {
"url": "https://example.com",
"assertions": [{ "type": "status_code", "operator": "lt", "value": "400" }]
}
}'Answers 201 with the new id. The interval is clamped to your plan’s floor rather than refused, so asking for 10 seconds on a plan whose floor is 60 gives you 60 — read it back if it matters.
Parameters and responses
| Name | In | Type |
|---|---|---|
name | body | string |
kind | body | string |
workspaceId | body | string |
config | body | string |
intervalSeconds | body | integer |
confirmations | body | integer |
dependsOn | body | string[] |
probeId | body | string |
tags | body | string[] |
| Response | CreatedResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /monitors/{id}
One monitor, with everything the list gives plus paused. Needs monitor:read.
For mail_posture, tls_audit and snmp it also carries lastCheckDetail, the last check’s own result: the findings behind the status, each a stable code and a severity, plus a TLS grade or the SNMP readings. It is null until the first check runs, and absent for every other kind.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetMonitorResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PATCH /monitors/{id}
Send only what you want changed: name, intervalSeconds, confirmations, enabled, config, or paused.
paused is checked against monitor:pause and everything else against monitor:update, separately — a key allowed to pause but not to edit can still pause. Sending an empty change is a 400.
The monitor is re-read and returned rather than the input echoed back, so what you see is what was stored.
# Silence a monitor for the length of a deploy.
curl -X PATCH https://vitrinaengine.com/api/v1/monitors/$ID \
-H "Authorization: Bearer vte_…" \
-H "Content-Type: application/json" \
-d '{"paused": true}'Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
name | body | string |
intervalSeconds | body | integer |
confirmations | body | integer |
enabled | body | boolean |
paused | body | boolean |
config | body | string |
tags | body | StringList |
| Response | UpdatedMonitorResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /monitors/{id}
Needs monitor:delete. Answers { "data": { "id": "…", "deleted": true } }.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /hosts · POST /hosts
Hosts are the second way monitors group: a workspace says whose a monitor is, a host says what it runs on. Entirely optional — an account that never creates one loses nothing, and most monitors name no host at all.
Each host carries status, pinned and spanning. status is derived only from monitors pinned to that host alone. A monitor that runs on several machines counts towards spanning and colours none of them, because a load-balanced endpoint failing says the service is broken and not which machine broke it. A host with nothing pinned to it answers "status": null rather than up.
POST needs monitor:create and a name. The optional provider, address and notes are yours to read and nothing in the product acts on them. A name that folds to one you already have — case and a trailing dot are ignored — answers 409.
Parameters and responses
GET /hosts
No declared parameters or body fields.
| Response | ListHostsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /hosts
| Name | In | Type |
|---|---|---|
name | body | string |
provider | body | string |
address | body | string |
notes | body | string |
| Response | CreatedResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /hosts/{id} · PATCH /hosts/{id} · DELETE /hosts/{id}
The detail carries the host and the monitors on it, each flagged pinned and, when it is not, alsoOn: the other machines it answers from, which is what you read before rebooting one. DELETE retires the host and every monitor on it keeps running — a host owns no schedule and no history.
Parameters and responses
GET /hosts/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetHostResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PATCH /hosts/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
name | body | string |
provider | body | string |
address | body | string |
notes | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /hosts/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /hosts/{id}/monitors · DELETE /hosts/{id}/monitors
One monitor on or off one host, by monitorId in the body or ?monitorId=. Deliberately not a whole list: on an agency account a host carries monitors from several workspaces, and a caller replacing “the list” would be replacing rows it was never shown. Both directions are idempotent.
Parameters and responses
POST /hosts/{id}/monitors
| Name | In | Type |
|---|---|---|
id required | path | string |
monitorId | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /hosts/{id}/monitors
| Name | In | Type |
|---|---|---|
id required | path | string |
monitorId | query | string |
monitorId | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /monitors/{id}/hosts · PUT /monitors/{id}/hosts
The other side of the same relationship, and this one is a whole list — these links belong to one monitor, so there is nothing on the far end that another workspace can see. Send hostIds; an empty array is a real request and means the monitor runs on no host in particular. Ids that are not yours are dropped rather than refused. Every monitor response also carries hosts, empty when there are none.
Parameters and responses
GET /monitors/{id}/hosts
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | ListMonitorHostsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PUT /monitors/{id}/hosts
| Name | In | Type |
|---|---|---|
id required | path | string |
hostIds | body | string[] |
| Response | ListMonitorHostsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /incidents
Needs incident:read. Takes ?status= — one of open, acknowledged, resolved or suppressed — and ?open=true for the three that are not resolved. An unrecognised status is ignored rather than refused, the same way ?limit= (default 50) is clamped to 200 rather than rejected.
Each carries id, monitorId, monitorName, title, cause, status, severity, startedAt, resolvedAt, durationSeconds, acknowledgedAt and rootIncidentId.
rootIncidentId is the field to look at when building an alert feed. When it is set, this incident is the blast radius of another one — the database host went down and this is one of the twelve services behind it. Skip those and you get one alert instead of thirteen.
Parameters and responses
| Name | In | Type |
|---|---|---|
limit | query | integer |
open | query | boolean |
status | query | string |
| Response | ListIncidentsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /incidents/{id}
One incident. Needs incident:read. The same fields as the list, plus workspaceId.
It does not return the incident’s timeline — the comments and status changes shown in the dashboard are not on this endpoint. If you need them, say so and they can be added; documenting them here while they do not exist would be worse than the gap.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetIncidentResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PATCH /incidents/{id}
Send status of "acknowledged" or "resolved", or a comment, or both. Each is checked against its own permission: incident:acknowledge, incident:resolve, incident:comment. Adding "publish": true to a comment puts it on the status page and additionally needs status_page:manage.
# A runbook that fixed the thing itself can close its own incident.
curl -X PATCH https://vitrinaengine.com/api/v1/incidents/$ID \
-H "Authorization: Bearer vte_…" \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "comment": "Restarted by runbook.", "publish": true}'Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
status | body | string |
comment | body | string |
publish | body | boolean |
| Response | GetIncidentResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
PUT /incidents/{id}/postmortem
Write or replace the incident’s postmortem. Needs incident:resolve, and the incident must already be resolved — otherwise 409 with code incident_not_resolved. Send body as Markdown, 1 to 50,000 characters after trimming (postmortem_required, postmortem_too_long). There is one per incident, so each call replaces the last. The response is the incident, as GET /incidents/{id} returns it.
GET /incidents/{id} carries it as postmortem: { body, authorName, updatedAt }, or null, and the list carries hasPostmortem. Render body as Markdown and never as HTML: it is text a person typed. A postmortem is internal and never appears on a status page.
curl -X PUT https://vitrinaengine.com/api/v1/incidents/$ID/postmortem \
-H "Authorization: Bearer vte_…" \
-H "Content-Type: application/json" \
-d '{"body": "## What happened\n\nThe connection pool ran dry after a deploy."}'Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
body | body | string |
| Response | GetIncidentResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /probes
Your organisation’s own private probes: id, name, lastSeenAt, version, hostname, monitorCount, revoked and createdAt. Needs agent:read. This is what a monitor’s probe picker offers.
There is no region field, and there will not be one. You choose a probe by name; where a check runs behind it is ours to change. Revoked probes are listed with revoked: true and cannot be given work, so do not offer them.
Parameters and responses
No declared parameters or body fields.
| Response | ListProbesResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /monitors/{id}/push-url
A heartbeat monitor’s ping URL, shown again. Needs monitor:read. url is null for a kind that is not pinged, and for a token that can no longer be decrypted — the monitor keeps working, and rotating the token in the dashboard issues a URL that can be shown.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetPushUrlResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /organizations · PUT /organizations/active
The organisations the signed-in person belongs to, each with their role there and active on the one in use, and switching between them. Send organizationId; the response is the membership now in use.
A signed-in session only. A key belongs to one membership and answers 403 with session_required. Switching moves every session that person holds, the dashboard included, and an organisation they are not in answers 404.
Parameters and responses
GET /organizations
No declared parameters or body fields.
| Response | ListOrganizationsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PUT /organizations/active
| Name | In | Type |
|---|---|---|
organizationId | body | string |
| Response | ActiveOrganizationResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /sms/enrolment · POST /sms/enrolment/confirm
Enrol your own mobile number for SMS alerts: send phone (and optionally name) and we text a six-digit code to it; send that code to the second route to confirm. Needs notification_channel:manage and a signed-in session — a number is added by the person holding it, never by a key.
The number is stored unconfirmed and receives nothing until the code comes back. Enrolling again replaces your previous number. Refusals: sms_not_included (402, the plan has no SMS allowance), not_a_phone_number, sms_country_unsupported, sms_route_not_open, verification_code_not_sent (502; call again for a new code), verification_code_malformed and verification_code_incorrect. A successful start carries country and caveat — sender-replaced, registration-pending, unverified or null — which is worth showing before somebody relies on the number.
Parameters and responses
POST /sms/enrolment
| Name | In | Type |
|---|---|---|
phone | body | string |
name | body | string |
| Response | SmsEnrolmentResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /sms/enrolment/confirm
| Name | In | Type |
|---|---|---|
code | body | string |
| Response | SmsConfirmedResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /channels/{id}/confirmation
Send an email channel’s confirmation message again. Needs notification_channel:manage. Each call issues a new link and the previous one stops working. sent: false means there was nothing to send — the address has already confirmed — and a confirmed address is never mailed again this way. A send the mail provider refused is 502 with confirmation_not_sent.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | ChannelConfirmationResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
PATCH /workspaces/{id}
Rename a workspace. Needs workspace:update. name is required (1 to 80 characters); clientReference is optional, and null clears it. The slug never changes, because it is in status-page URLs. The response is the workspace as it now reads.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
name | body | string |
clientReference | body | requests.NullableString |
| Response | UpdatedWorkspaceResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /sourcemaps
Upload a source map so minified stack traces resolve. Needs monitor:create. Requires projectRef (a number) and filename, plus the map itself as either map (JSON) or mapGzipBase64. Optional debugId and release.
Send a debug id or a release. A map with neither cannot be matched to a stack trace and will sit there doing nothing. Matching is by debugId first, then release plus filename.
Maps are resolved when an issue is read, not at upload, so one uploaded after the errors arrived still helps — which is the usual order.
Parameters and responses
| Name | In | Type |
|---|---|---|
projectRef | body | integer |
filename | body | string |
debugId | body | string |
release | body | string |
map | body | string |
mapGzipBase64 | body | string |
| Response | UploadedSourceMapResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 300 an hour |
POST /symbols
The same idea for a native app: upload an iOS .dSYM or an Android R8 mapping.txt so crash stacks resolve. Needs monitor:create. Twenty uploads an hour, and twenty builds’ worth kept per project.
Unlike /sourcemaps, the body is the gzipped file itself and the metadata is in headers: X-Vitrina-Project-Ref, X-Vitrina-Platform (ios or android), X-Vitrina-Symbol-Name, and X-Vitrina-Release. A dSYM is tens of megabytes, and base64 in a JSON field costs a third again in transfer.
The two platforms are matched differently. An iOS frame names the Mach-O UUID of the image it belongs to, so an iOS upload must carry X-Vitrina-Debug-Ids and is matched on it exactly. R8 emits no such id, so an Android mapping is matched on the release alone — which must be exactly what the app reports, <applicationId>@<versionName>+<versionCode>. One digit out and frames resolve to lines that look plausible and are wrong.
Re-uploading the same build replaces its symbols rather than failing, so a retried release job is not an error. As with source maps, resolution happens when an issue is read.
Parameters and responses
No declared parameters or body fields.
| Response | UploadedSymbolsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 20 an hour |
GET /errors/projects
The error-tracking projects this account holds. Needs monitor:read. Each carries ref — the number in the DSN — plus publicKey, unresolved and eventsLast24h. Read-only: a project is created with a key that still has to be pasted into an application’s config, so there is nothing here an endpoint would finish.
Parameters and responses
No declared parameters or body fields.
| Response | ListErrorProjectsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /errors/issues
Grouped errors, newest activity first. Needs monitor:read. An issue is a fingerprint rather than an event, so a thousand occurrences of one throw are a single row with timesSeen at a thousand. ?status= defaults to unresolved and also takes resolved, ignored or all; ?project= filters by project id, ?q= searches the type, value and culprit, and ?limit= is capped at 200. spark is twenty-four hourly counts, oldest first.
Parameters and responses
| Name | In | Type |
|---|---|---|
limit | query | integer |
project | query | string |
q | query | string |
status | query | string |
| Response | ListIssuesResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /errors/issues/{id}
One issue with its stack. Needs monitor:read. exceptions is the SDK’s own chain, thrown error last and its causes before it, and its frames are resolved against any source map you have uploaded — the same resolution the dashboard does, so a script and the person you are talking to are never looking at different stacks. A map that cannot be applied falls back to the original frames rather than failing the request.
lastEvent carries two timestamps on purpose. occurredAt is what the SDK reported and receivedAt is when ingest wrote it; a device that was offline, or a queue that backed up, is exactly the gap between them.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetIssueResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PATCH /errors/issues/{id}
Resolve, ignore or reopen: { "status": "resolved" }. Needs monitor:update. ignoreHours may accompany ignored to un-ignore it again later, and is capped at 90 days.
Resolving records the release it was resolved in, so a straggler from an older deployment does not reopen the issue. When the SDK sent no release there is nothing to compare against and any later event does reopen it — with no way to tell a straggler from a regression, the safe assumption is that the bug is back.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
status | body | string |
ignoreHours | body | integer |
| Response | IssueStatusResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /analytics
The websites this account measures. Needs monitor:read. Carries publicId — the value that goes in the script tag, which is public by design — plus viewsLast24h and trafficAlerting, which is true while a site’s traffic is below what that hour normally sees.
Parameters and responses
No declared parameters or body fields.
| Response | ListAnalyticsSitesResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /analytics/{id}
One site’s numbers, for a spreadsheet or a report. Needs monitor:read. ?days= defaults to 7 and is capped at 365. ?dimensions= takes a comma-separated list from path, referrer, country, browser, os, device, utm_source, utm_medium, utm_campaign and event; an unknown one is a 400 rather than an empty answer, because a typo that returns nothing reads as “no traffic”.
The visitor figure is called dailyUniqueVisitors, and that is what it is. It is the sum of daily uniques and cannot be anything else: the secret behind a visitor hash is destroyed at the end of its UTC day, so somebody who visited on two days counts twice and there is no key that would join them. That is the privacy design working rather than an approximation — but a field called visitors beside a 30-day range would invite you to report a number that means something else.
curl "https://vitrinaengine.com/api/v1/analytics/$ID?days=30&dimensions=path,referrer" \
-H "Authorization: Bearer vte_…"Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
days | query | integer |
| Response | GetAnalyticsSiteResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /billing
The plan, and where it is billed. Needs billing:read. Carries plan, status, cadence, currentPeriodEnd, cancelAt, addOnPacks and the last dozen receipts.
source is paddle, apple, manual or none. It matters: a subscription bought in the iOS app is Apple’s to change, and the plan, the card and the cancellation all live in the customer’s App Store settings rather than here. canCheckoutOnWeb and canPurchaseInApp say which offers are safe to show, so a client does not have to re-derive that rule and get it wrong in the direction that bills somebody twice.
Parameters and responses
No declared parameters or body fields.
| Response | GetBillingResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /monitors/{id}/history
Daily uptime for one monitor. Needs monitor:read. Takes days, up to your plan’s history retention.
Computed from daily rollups rather than from individual check results, which is why it keeps answering for periods whose raw results retention has already deleted.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
days | query | integer |
| Response | GetMonitorHistoryResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /monitors/{id}/graph · GET /monitors/graphs
The last hours of one monitor, hour by hour, or of every monitor this key can see in one response — what the dashboard draws on a monitor row and page. Needs monitor:read. Takes hours, 24 by default and at most 168.
An uptime graph carries the share of each hour’s checks that passed, with null for an hour with none; a value graph, for the quantitative kinds, carries its readings as series with a unit. Every hour in the window is present whether or not anything was recorded, so a gap is visibly a gap. Readings are sent as measured: a raw series has no known scale, so draw it against its own highest reading and never print that share as the reading.
Parameters and responses
GET /monitors/{id}/graph
| Name | In | Type |
|---|---|---|
id required | path | string |
hours | query | integer |
| Response | GetMonitorGraphResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /monitors/graphs
| Name | In | Type |
|---|---|---|
hours | query | integer |
| Response | ListMonitorGraphsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /monitors/{id}/channels · PUT /monitors/{id}/channels
Which channels this monitor alerts, and setting them. Reading needs notification_channel:read; writing needs monitor:update.
PUT replaces the whole set — send every channel id you want, not the ones you are adding. An empty array means the monitor alerts nobody, which is a valid thing to want and a bad thing to do by accident.
Parameters and responses
GET /monitors/{id}/channels
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetMonitorChannelsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PUT /monitors/{id}/channels
| Name | In | Type |
|---|---|---|
id required | path | string |
channelIds | body | StringList |
| Response | GetMonitorChannelsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /incidents/{id}/events
The timeline for one incident: state changes, acknowledgements, comments, escalation steps. Needs incident:read. This is the timeline that GET /incidents/{id} does not include.
Parameters and responses
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | ListIncidentEventsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /channels · POST /channels
Reading needs notification_channel:read; creating needs notification_channel:manage. Each carries id, kind, name, target and workspaceId.
secret is returned only when a channel is created, and never again. It is the signing key for a webhook; storing it somewhere you can read it back would make a read-scoped key a way to forge signed requests.
You can create the kinds a person can create in the dashboard: email, webhook, telegram, slack, teams, discord, pagerduty and jsm. SMS, WhatsApp, satellite and push are enrolled by the person who receives them and cannot be created through the API — that is what makes them consent rather than a field somebody typed.
An email channel is created unconfirmed and receives one message asking whether it wants alerts. Nothing else is sent until it agrees, so creating one through the API does not put an address on your paging rota by itself.
Parameters and responses
GET /channels
No declared parameters or body fields.
| Response | ListChannelsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /channels
| Name | In | Type |
|---|---|---|
kind | body | string |
name | body | string |
target | body | string |
workspaceId | body | requests.NullableString |
| Response | CreatedChannelResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
PATCH /channels/{id} · DELETE /channels/{id} · POST /channels/{id}/test
All need notification_channel:manage. The test send is refused for an address that has not confirmed — otherwise it would be an unlimited way to mail an unconsenting address, wearing a helpful label.
Parameters and responses
PATCH /channels/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
enabled | body | boolean |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /channels/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /channels/{id}/test
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | TestDeliveryResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /status-pages · POST /status-pages
Reading needs status_page:read; writing needs status_page:manage. Each carries id, name, slug, visibility, customDomain, domainVerifiedAt, subscribersEnabled, componentCount, workspace and createdAt.
Parameters and responses
GET /status-pages
No declared parameters or body fields.
| Response | ListStatusPagesResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /status-pages
| Name | In | Type |
|---|---|---|
name | body | string |
workspaceId | body | string |
| Response | CreatedResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /status-pages/{id} · PATCH /status-pages/{id} · DELETE /status-pages/{id}
Reading needs status_page:read; changing or deleting needs status_page:manage.
Parameters and responses
GET /status-pages/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | GetStatusPageResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
PATCH /status-pages/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
name | body | string |
headline | body | requests.NullableString |
description | body | requests.NullableString |
logoUrl | body | requests.NullableString |
themeAccent | body | string |
language | body | string |
historyDays | body | integer |
showResponseTimes | body | boolean |
showIncidentHistory | body | boolean |
subscribersEnabled | body | boolean |
visibility | body | string |
password | body | requests.NullableString |
customCss | body | requests.NullableString |
hideVitrinaBranding | body | boolean |
groupByHost | body | boolean |
| Response | UpdatedStatusPageResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /status-pages/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /status-pages/{id}/components · DELETE /status-pages/{id}/components
Add or remove a monitor from a page. Needs status_page:manage.
The monitor must be in the same workspace as the page, not merely in the same organisation. In an agency account that is what stops one client’s monitor being published on another client’s page.
Parameters and responses
POST /status-pages/{id}/components
| Name | In | Type |
|---|---|---|
id required | path | string |
monitorId | body | string |
displayName | body | string |
| Response | ComponentAckResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /status-pages/{id}/components
| Name | In | Type |
|---|---|---|
id required | path | string |
component | query | string |
componentId | body | string |
| Response | ComponentAckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /workspaces · POST /workspaces · DELETE /workspaces/{id}
Reading needs workspace:read, and the rest their own permissions. Each carries id, name, slug, clientReference and isDefault.
A key belonging to a confined member sees only the workspaces that member is confined to. The default workspace cannot be deleted.
Parameters and responses
GET /workspaces
No declared parameters or body fields.
| Response | ListWorkspacesResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /workspaces
| Name | In | Type |
|---|---|---|
name | body | string |
clientReference | body | string |
| Response | CreatedResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /workspaces/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /members · POST /members
Reading needs member:read; inviting needs member:invite. The response has members and invitations separately — an invitation that nobody has accepted has not added anybody, and merging the two would make a seat count that disagrees with what you are billed for.
POST sends an invitation rather than creating an account. It takes an email, a role, and optionally workspaceIds to confine them.
Parameters and responses
GET /members
No declared parameters or body fields.
| Response | ListMembersResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /members
| Name | In | Type |
|---|---|---|
email | body | string |
role | body | string |
workspaceIds | body | string[] |
| Response | AckResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
PATCH /members/{id} · DELETE /members/{id} · DELETE /invitations/{id}
Changing a role needs member:update_role; removing needs member:remove; revoking an invitation needs member:invite.
Removing a member deletes their personal alert channels — their handset, their private chat, their mobile number — along with their API keys. Shared channels are untouched. The last owner cannot be removed or demoted.
Parameters and responses
PATCH /members/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
role | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /members/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /invitations/{id}
| Name | In | Type |
|---|---|---|
id required | path | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /maintenance · POST /maintenance · DELETE /maintenance
Windows during which nothing pages anybody. By default checks still run and still record, so those minutes count towards uptime like any others; with keepChecking false nothing is checked while the window is open, which leaves a gap in the history rather than a dip. Reading needs incident:read; writing needs maintenance:manage.
Carries title, startsAt, endsAt, timezone, monitorIds, recurrenceRule, keepChecking, showOnStatusPage and notifySubscribers. A recurring window is one row rather than many.
A rule repeats the window at the same wall-clock time in its own timezone, so a window through a clock change stays at the hour it was scheduled for. FREQ=DAILY, FREQ=WEEKLY and FREQ=MONTHLY are expanded, optionally with INTERVAL, COUNT, UNTIL, and a BYDAY or BYMONTHDAY naming the day the window itself starts on. Any other rule is stored and returned to you unchanged and holds alerts back for its first occurrence only — it is never guessed at.
Parameters and responses
GET /maintenance
| Name | In | Type |
|---|---|---|
past | query | boolean |
| Response | ListMaintenanceResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /maintenance
| Name | In | Type |
|---|---|---|
title | body | string |
description | body | string |
workspaceId | body | string |
monitorIds | body | string[] |
startsAt | body | string |
endsAt | body | string |
recurrenceRule | body | string |
timezone | body | string |
keepChecking | body | boolean |
showOnStatusPage | body | boolean |
notifySubscribers | body | boolean |
| Response | CreatedResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /maintenance
| Name | In | Type |
|---|---|---|
id | query | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /on-call
Schedules, escalation policies, overrides, and who is on call right now. Needs oncall:read.
onCallNow is resolved at the moment you ask, with viaOverride saying whether it came from the rotation or from somebody covering. unreachable names participants with no channel that could actually reach them, which is the failure worth catching before an incident rather than during one.
Parameters and responses
No declared parameters or body fields.
| Response | GetOnCallResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /agents · POST /agents · DELETE /agents · GET /agents/{id}/metrics
Server agents report CPU, memory and disk from your own machines. Enrolling needs agent:enroll, reading needs agent:read, removing needs agent:delete.
Enrolling returns a token, once. It is what the agent authenticates with, so it is never readable afterwards.
Parameters and responses
GET /agents
No declared parameters or body fields.
| Response | ListAgentsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /agents
| Name | In | Type |
|---|---|---|
name | body | string |
workspaceId | body | requests.NullableString |
| Response | EnrolledAgentResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /agents
| Name | In | Type |
|---|---|---|
id | query | string |
id | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /agents/{id}/metrics
| Name | In | Type |
|---|---|---|
id required | path | string |
hours | query | integer |
limit | query | integer |
| Response | GetAgentMetricsResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /api-keys · POST /api-keys · DELETE /api-keys
Reading needs api_key:read; creating and revoking need api_key:manage. Each carries id, name, prefix, scopes, workspaceIds, owner, createdAt, lastUsedAt, expiresAt and revokedAt.
token is returned only at creation. prefix is the displayable part, which is what lets you tell two keys apart in a list without either being readable.
A key inherits the role and workspace confinement of the membership that created it, so a key cannot be a way around a limit its author has.
Parameters and responses
GET /api-keys
No declared parameters or body fields.
| Response | ListApiKeysResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
POST /api-keys
| Name | In | Type |
|---|---|---|
name | body | string |
scopes | body | string[] |
workspaceIds | body | string[] |
expiresInDays | body | requests.NullableInt64 |
| Response | CreatedApiKeyResponse |
|---|---|
| Status codes | 201 400 401 403 404 429 |
| Rate limit | 30 a minute |
DELETE /api-keys
| Name | In | Type |
|---|---|---|
id | query | string |
id | body | string |
| Response | AckResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /audit
Configuration changes, with who made them and what changed. Needs audit_log:read, which is Business and above.
Paginated by nextBefore rather than by page number, so a page boundary does not shift while you are reading it. Entries are recorded on every plan — the plan gates reading, so upgrading opens the whole history rather than starting one.
Credentials, hashes and secrets are never written into changes.
Parameters and responses
| Name | In | Type |
|---|---|---|
limit | query | integer |
| Response | ListAuditResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
GET /me
Who this key is, what it can reach, and what the plan allows. No permission needed — every key can describe itself.
Carries user, organization, membership(role and workspace confinement), entitlements (the resolved plan limits, with any add-on packs already folded in) and usage. Use it to check a limit before attempting a create rather than after being refused.
A session also gets organizations, listing every organisation the person belongs to. An API key does not: a key is issued against one membership and confined to it, so listing the others would advertise organisations that credential cannot reach.
Parameters and responses
No declared parameters or body fields.
| Response | GetMeResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 120 a minute |
DELETE /account
Deletes the organisation and everything in it. No permission constant guards this one because ownership is the check: it is refused for anybody who is not an owner.
This is not reversible and is not a pause. Use it deliberately.
Parameters and responses
No declared parameters or body fields.
| Response | AccountDeletedResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
POST /billing/apple/verify
Confirms an in-app purchase made in our own mobile app. Needs billing:manage.
Documented for completeness rather than for use: it is only callable with a receipt Apple issued to our app, so there is nothing a third-party integration can do with it. Listed here because a route that exists and is described nowhere is indistinguishable from one somebody forgot.
Parameters and responses
| Name | In | Type |
|---|---|---|
transactionId | body | string |
| Response | AppleVerifyResponse |
|---|---|
| Status codes | 200 400 401 403 404 429 |
| Rate limit | 30 a minute |
GET /
The index. Lists the routes this deployment serves, which is the machine-readable version of this page and always current — it is generated from what is actually mounted.
Incident feed (RSS and Atom)
Every incident in your account as a feed, for a reader rather than a client library. Create the URL in the dashboard under Settings → Notifications:
https://vitrinaengine.com/feeds/incidents/vtf_…/feed.xml
https://vitrinaengine.com/feeds/incidents/vtf_…/atom.xmlThe URL is the credential. There is no header to send: anybody holding the link can read every incident in the account, across every workspace. It is shown in the dashboard whenever you need it again, because it lives in a feed reader somebody will one day reinstall — and it can be replaced or revoked, which takes effect on the next request rather than whenever a cache expires.
It carries incident-level facts only: the monitor’s name, the cause, severity, status, when it opened and closed, and the updates marked public. Never a postmortem, never monitor configuration, and never where a check ran from. Most readers sync through somebody else’s servers, so the document is written as though it leaves your building.
Polling once a minute is expected. Past sixty requests a minute for one URL it answers 429, and a URL that was never valid, has been replaced or has been revoked answers 404 — the same answer for all three, so it cannot be used to find out which tokens were once real.
What this API deliberately does not return
- Monitor
config. It can contain credentials. - Which region a check ran in. Where we check from is ours to decide and ours to change as probes are placed and moved. It would be a promise about our infrastructure that you could not act on, and it is not shown anywhere else in the product either.
Exporting your data
Two routes hand back a file rather than a message, so that taking your data out does not mean paging through a collection endpoint a thousand times. Both are on every plan, the free one included — what a plan changes is how much history there is to export, not whether you may.
GET /export/{entity}
One kind of row, as CSV or JSON. entity is one of monitors, hosts, status-pages, notification-channels, members, incidents, incident-events, postmortems, audit-log, error-issues, uptime-daily, uptime-hourly or analytics-hourly. Anything else answers 404.
format is csv (the default) or json. from and to are RFC 3339 timestamps bounding the entity’s own time column. bom prepends a UTF-8 byte-order mark to a CSV: Excel needs it to read a non-ASCII name correctly, and most scripting languages read it as part of the first column heading — so it is off unless you ask.
curl -H "Authorization: Bearer $KEY" \
"https://vitrinaengine.com/api/v1/export/incidents?format=json"Parameters and responses
| Name | In | Type |
|---|---|---|
entity required | path | string |
bom | query | boolean |
format | query | string |
from | query | string |
to | query | string |
| Status codes | 200 400 401 403 404 429 |
|---|---|
| Rate limit | 6 an hour |
GET /export
Everything the key may read, as a zip: one file per entity in the format you asked for, plus a manifest.json saying what the archive holds, how many rows each file has and which entities are absent. Entities your key’s role cannot read are left out rather than refused — a key scoped to one workspace should still be able to take that workspace’s data — and the manifest is what stops that being silent.
Parameters and responses
| Name | In | Type |
|---|---|---|
bom | query | boolean |
format | query | string |
from | query | string |
to | query | string |
| Status codes | 200 400 401 403 404 429 |
|---|---|
| Rate limit | 6 an hour |
What an export does not contain
- Nothing that authenticates. No channel configuration — a webhook URL or chat credential — no heartbeat tokens, no password hashes, no API keys. A monitor ’s configuration is included with anything credential-shaped removed: a request body, request headers, an SNMP community string, a mailbox login and any field named like a password or a key. The URL, the host, the port and the assertions stay.
- Raw check results and raw error events. Those are dropped whole by retention — three days on the free plan — so an export of them would answer differently depending on the hour it ran. Uptime comes from the rollups instead, which is what the product itself shows beyond that window.
Size, and the one refusal you may meet
The body is streamed, so it starts arriving before the whole result has been read out of the database, and no part of your account is ever held in memory in full. The hourly series — uptime-hourly and analytics-hourly — are one row per subject per hour, so they are capped at 92 days per request and a wider window is refused with export_window_too_wide rather than quietly narrowed: a file that is silently missing two years is worse than one you were told to ask for again. uptime-daily carries the same span at a twenty-fourth of the size and is what an archive or an SLA report wants. For the same reason the two hourly series are not in the whole-account zip; ask for them separately with a window.
Exports have their own rate bucket — 6 an hour, per organisation — because one request walks every table your account touches, which is not the shape the 120-a-minute read ceiling was sized for.
Versioning
The version is in the path. Fields will be added to responses — treat unknown ones as something to ignore rather than an error — but nothing will be removed from v1 or change meaning under it.
Anything missing?
The surface is deliberately small: it covers a wallboard, a chat digest and a deploy script that silences a monitor for the length of a release. If you are building something it does not reach, write to support@vitrinaengine.com — knowing what people actually want is how the next endpoint gets chosen.