The read-only pledge
You are handing us a key to infrastructure you are responsible for. Here is exactly what we do with it, and the code that makes the promise hard to break by accident.
We never write to your n8n.
Every request to a monitored instance goes through one HTTP client. That client refuses any method other than GET before a byte leaves our process. A unit test that tries POST, PUT, PATCH and DELETE must fail loudly on every commit.
We store metadata only.
Execution status, timestamps, the failing node name and the error message text. Never the items flowing through your workflows. When we fetch an execution's detail to read the error, we extract those two strings and discard the rest.
Your key is encrypted at rest.
With a key that lives only in our server's environment, decrypted only in the poller's memory, and shown to anyone, including us, as the last four characters.
Required API key scopes
A key with only these cannot change anything even if we wanted it to.
The part most tools do not tell you. Choosing scopes in the n8n interface is one of n8n's licensed features (feat:apiKeyScopes). Without that licence the key you create there carries the full access of the account that created it — not read access, every access. That is true whatever we do at our end, and it is the single best reason to be sceptical of anyone asking you for an n8n key.
You can still get a genuinely restricted key on any plan, because n8n enforces scopes whichever way the key was made — we verified that on Community 2.35.7 and 2.37.10, where a key missing workflow:list gets a 403. It has to be created through the API rather than the interface:
POST /rest/api-keys
{"label": "opsprey", "scopes": ["workflow:list", "workflow:read", "execution:read", "execution:list"], "expiresAt": null}
Set an expiry you are comfortable with either way, and ask any monitoring vendor — us included — how they store what you hand them.
This is the top of src/opsprey/client.py in the running service:
"""Minimal read-only n8n public API client.
Verified live against n8n 2.35.7 on 2026-08-29. Uses only GET endpoints — read-only
access to a customer's instance is the promise this module exists to keep.
This module is the single choke point for all HTTP to a customer's n8n.
``ReadOnlySession`` refuses any method other than GET before a byte leaves the
process. Nothing that writes to a customer instance belongs in this file.
"""
from __future__ import annotations
import requests
class ReadOnlyViolation(RuntimeError):
"""Raised when code attempts a non-GET request to a customer instance."""
class ReadOnlySession(requests.Session):
"""requests.Session that hard-blocks every method except GET."""
ALLOWED_METHODS = frozenset({"GET"})
def request(self, method: str, url: str, *args, **kwargs): # type: ignore[override]
if method.upper() not in self.ALLOWED_METHODS:
raise ReadOnlyViolation(
f"Opsprey is read-only: refused {method.upper()} {url}"
)
kwargs["allow_redirects"] = False # Session.get() defaults this to True; force off — never leave the customer host
return super().request(method, url, *args, **kwargs)
Tests that must pass on every commit
Verify it yourself
Connect an instance and watch what we do with the key. We test it with a single GET and never write to it.