Why we added it
Quellery's job is to tell you the truth about your database — what's nullable that shouldn't be, which keys actually hold, which relationships the data agrees with. The GUI is a fine way to look at that, but it isn't the only way somebody might want to use it. The most common ask we've had since launch is some variant of "can my CI pipeline read this?"
The answer is now yes. There's a read-only HTTP API under /api/v1/, with parity between what the GUI shows and what a tool can fetch. If you can see it in Quellery, you can read it programmatically — using the same group-membership-based authorisation that already governs the GUI.
The shape
Eight endpoints. All read-only. All authenticated with a bearer key. All authorised by group membership exactly the way the GUI is.
GET /api/v1/databases— list databases you can see.GET /api/v1/databases/{db}/schemas— list schemas in a database.GET /api/v1/databases/{db}/schemas/{schema}/models— list models bound to that schema.GET /api/v1/databases/{db}/schemas/{schema}/models/{model}— fetch the model definition.GET /api/v1/databases/{db}/schemas/{schema}/models/{model}/validation— fetch the latest validation result, includinglastValidatedAt.GET /api/v1/databases/{db}/schemas/{schema}/models/{model}/migration— fetch the generated migration SQL.GET /api/v1/health— liveness for API consumers.GET /api/v1/version— the build version.
The API is feature-flagged off by default. Set QUELLERY_API_ENABLED=true to turn it on; see the Configuration section on the Get Started page for the full env-var list.
Keys are minted in the GUI, on purpose
Honest disclosure: there is no POST /keys endpoint. You can't use the API to mint API keys. To create one, an administrator opens Admin → API Keys, clicks Create, copies the quellery_… secret from the reveal panel, and pastes it wherever the consumer needs it. The secret is shown once.
That's deliberate. A compromised admin key — or a compromised admin session, for that matter — shouldn't be able to mint replacement keys faster than the human operations team can revoke them. By keeping minting in a panel that requires an authenticated administrator session, an attacker who steals a single API key can use it until you notice and revoke it; they can't bootstrap themselves into a sprawl of fresh keys you've never heard of.
Revocation is a soft delete: a revoked key's history (created, last used, request count) stays visible on the API Keys tab so you can see what it did before you killed it.
Authorisation: keys live inside groups
API keys aren't a separate permissions system. They share the same group-membership model that governs human users. A key on its own has no access to anything; you grant it access by adding it to a group with the grants you want.
This means three things in practice. First, you can give a key exactly the access it needs — read-only for a CI consumer, read-and-see-models for a dashboard, and nothing it doesn't need. Second, you can audit a key the same way you audit a user, by looking at the groups it belongs to. Third, revoking a group grant revokes it for both users and keys at once.
A worked CI example
Here is the scenario we built the API to support. Your CI pipeline runs migrations against a staging database. After the migrations run, you'd like to assert that Quellery's continuous validators agree with your model — that all the constraints you've documented still hold against the live data.
Step 1: mint a key
Open Quellery as an administrator. Go to Admin → API Keys, click Create, give it a name like ci-staging-validation, and save. The reveal panel shows the secret once — it starts with quellery_ and is about 40 characters long. Copy it into your CI secret store. You won't be able to read it again.
Step 2: create a group with the grants the key needs
Still in the admin panel, switch to Groups and create a new group called Read-only API. Open its Permissions panel and add two grants:
ModelSee(Global)— so the key can see every model.SchemaSee(Global)— so the key can see every schema the models are bound to.
Both grants are read-only verbs. Nothing in this group lets a key change anything; it only lets it look.
Step 3: add the key to the group
Open the Members panel of the same group. The members panel has a Users / API Keys toggle near the top — flip it to API Keys, find ci-staging-validation in the left column, and add it. Commit-on-action: the membership is live as soon as you click.
The key now has the access it needs, scoped to read-only operations on models and schemas.
Step 4: the CI script
The CI script polls the validation endpoint until lastValidatedAt crosses the moment the CI run started, then asserts that the elements it cares about are Verified. That dance — wait for a validation cycle that ran after our migration ran, then check the result — is the whole point.
import os
import sys
import time
import datetime as dt
from urllib.parse import quote
import requests
BASE = os.environ["QUELLERY_BASE_URL"] # e.g. https://quellery.internal.example.com
KEY = os.environ["QUELLERY_API_KEY"] # quellery_...
DB = "staging-postgres"
SCHEMA = "public"
MODEL = "core-domain"
HEADERS = {"Authorization": f"Bearer {KEY}"}
def url(path: str) -> str:
return f"{BASE}/api/v1/{path}"
def fetch_validation():
path = (
f"databases/{quote(DB)}"
f"/schemas/{quote(SCHEMA)}"
f"/models/{quote(MODEL)}/validation"
)
response = requests.get(url(path), headers=HEADERS, timeout=10)
response.raise_for_status()
return response.json()
def wait_for_validation(after: dt.datetime, timeout_s: int = 300, poll_s: int = 5):
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
data = fetch_validation()
last = dt.datetime.fromisoformat(data["lastValidatedAt"].replace("Z", "+00:00"))
if last >= after:
return data
time.sleep(poll_s)
raise TimeoutError(
f"No validation cycle after {after.isoformat()} within {timeout_s}s"
)
def assert_verified(validation):
failed = [
entry for entry in validation["elements"]
if entry["result"] != "Verified"
]
if failed:
for entry in failed:
print(f" {entry['kind']} {entry['name']}: {entry['result']}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
started_at = dt.datetime.now(dt.timezone.utc)
print(f"CI started at {started_at.isoformat()}, waiting for next validation cycle...")
result = wait_for_validation(after=started_at)
print(f"Validation completed at {result['lastValidatedAt']}")
assert_verified(result)
print("All model elements verified against live data.")
Drop that in a CI step after your migration step. If anything you have documented in the model disagrees with the data after your migration, the step fails and you find out before the change reaches production.
Activity, in your own instance
Every API request is counted against the key that made it. Open the API Keys tab and you'll see, for each key: when it was created, when it was last used, and how many requests it has served. Useful for spotting keys that should be revoked because nothing is using them, and for sanity-checking that the CI key really is being called by the pipeline you think it is.
That activity column lives entirely on your instance. Quellery the company never sees it. We don't run a telemetry collector, we don't have an analytics endpoint, and there is nothing in the product that phones home. If you'd like to scrape per-key usage into your own dashboarding, the internal observability listener on 127.0.0.1:8081 exposes /internal/metrics in Prometheus text format — see the configuration table for how to point it at a different bind address if you're scraping from a sidecar in a compose network.
What's next
The full route reference, with request and response schemas for every endpoint, is at /api-docs.html inside your Quellery instance. It is visible to every edition, including Trial — so you can read the reference and decide whether the API fits your use case before upgrading.
One thing to be aware of: Quellery has had a WebSocket at /ws for a while now. That's an internal protocol the browser app uses to talk to the server, and we don't support it for external programmatic use. The shape can and does change between releases. If you're building an integration, build it against /api/v1/* — that's the contract.
If you have ideas for endpoints we haven't built yet, the FAQ has the link to file an issue. The API is read-only on purpose for this first cut, but the surface will grow as people tell us what they actually want to do with it.