API reference
Platform endpoints
Cross-tenant operator endpoints — infrastructure health and the abuse review queue. Restricted to an allow-list of operator emails, and deliberately unable to read payload contents.
Who these are for#
Everything under /v1/admin/platform/* is for whoever runs an Arcus instance, not for tenants. If you use hosted Arcus, you will never call these. If you self-host, this is your operations surface.
Two things live here: a real infrastructure health probe, and a cross-tenant abuse review queue.
Access control#
These endpoints require a Clerk session and operator status. Anyone else — including tenants on the highest paid tier — gets 403.
Operator status has exactly two sources, both Clerk-side:
publicMetadata.roleon the Clerk user —admin(orsuper_admin). Settable only from the Clerk dashboard or Backend API with the secret key. This is the real mechanism.SUPER_ADMIN_EMAILS, a comma-separated server-only allow-list, as the bootstrap path for the first operator — who has to be able to reach the control plane in order to hand out roles.
Neither is derived from anything in Arcus's own database, and neither is a tenant-controlled value. A customer can upgrade to the highest tier, set their own in-tenant role to ADMIN through the key-creation body, and still get 403 from every route here. Granting real operator access needs a Clerk-side change or a deploy, which means an application bug, an SQL injection or a compromised admin session cannot grant it.
Operator access is cross-tenant
These endpoints read across every account on the instance. Treat the allow-list as the sensitive configuration it is: the smallest possible set of addresses, on accounts with MFA, reviewed when people change roles.
What operators deliberately cannot see#
Cross-tenant visibility is scoped to metadata and enforcement outcomes. No platform endpoint returns:
- dispatch payload bodies or
messagecontent, - DLP matched values,
- API key hashes or raw keys,
- any tenant's audit event contents beyond what the queue needs to describe a signal.
An operator can see that a tenant's agent tripped DLP 40 times, and cannot see what it tried to send. This is a design constraint on the queries themselves, not a UI decision — running the platform does not require reading customers' data, so the endpoints are not written to.
Infrastructure health#
GET /v1/admin/platform/health
Live probes plus real aggregates. Everything here is measured; nothing is a placeholder.
| Component | What is measured |
|---|---|
| PostgreSQL | A timed SELECT 1. State derived from the observed latency — never forced |
| Redis | A timed PING, plus server version, uptime, memory used and connected clients |
| Delivery queue | Waiting / active / delayed / failed / completed job counts, and whether a worker is attached |
| API process | Uptime, RSS, load average, CPU count, Node version — per instance |
| Throughput | Real dispatch counts, 24 × 1-hour buckets, cross-tenant |
| p95 latency | A true percentile_cont(0.95) over deliveredAt - createdAt for successful deliveries |
| Parameter | Notes |
|---|---|
refresh | ?refresh=1 drops the 60-second memo on the two trend arrays |
Both datastore probes are wrapped so an outage is reported rather than returned as a 500 — an unreachable Redis is the single most important thing this endpoint can tell an operator, and it would be useless if it made the endpoint fail.
The two 24-hour trend arrays are the only expensive part and are memoised for 60 seconds, while the probes stay live on every call. A 10-second dashboard poll therefore costs two round trips, not a 24-hour scan.
What this endpoint does not report
There is no uptime 30d, no SLO figure, no disk or network metrics. Those need a historical probe store and host-level metrics, neither of which exists in this stack — so rather than showing a plausible number, they are absent. CPU and memory are the API process's real load average and RSS, labelled per instance.
For a simple liveness check, use the unauthenticated /health endpoint instead — it reports that the process is serving requests, which is what a load balancer needs.
Abuse review queue#
GET /v1/admin/platform/abuse
Cross-tenant enforcement signals, for triaging an account that is abusing the instance or is compromised.
The queue is derived, not stored: it groups TransferLog by (tenant, agent, status) over the enforcement outcomes — THREAT_DETECTED, BLOCKED_BY_DLP, BLOCKED_BY_POLICY, BLOCKED_BY_PARAMETER — within a 30-day window. A signal therefore cannot exist without the traffic that produced it, and nothing is duplicated.
On top of that sits a small triage overlay recording who claimed a signal and whether it was resolved. A signal with no overlay row is OPEN.
Each entry carries the tenant email, the agent, the signal class, the hit count, first and last occurrence, the newest decision reason, the issuing key prefix, the current review state and — if the agent is quarantined right now — its live TTL.
Priority is derived and documented: signal class × hit count × recency. Not assigned per row, so two operators looking at the same queue see the same ordering for the same reason.
Reopening#
Dismissal records a watermark rather than a permanent hide. If hits arrive after the dismissal timestamp, the signal reopens. An account dismissed once and then abusing again does not stay invisible — which is the failure mode a plain "dismissed" boolean would have.
Operator actions#
All four take the target in the body rather than the path, because the group key contains separators that would need escaping:
{ "tenantId": "user_2abc…", "agent": "intake-agent", "signal": "BLOCKED_BY_DLP" }POST /v1/admin/platform/abuse/claim
Assigns the signal to you — state IN_REVIEW, with your id, email and timestamp. Returns 409 if another operator already holds it, so two people cannot silently work the same signal. Re-claiming one you already hold is a no-op rather than a conflict.
POST /v1/admin/platform/abuse/dismiss
{ "tenantId": "user_2abc…", "agent": "intake-agent", "signal": "BLOCKED_BY_DLP",
"reason": "Tenant's staging agent misconfigured; they have fixed it" }State DISMISSED, with the reason, resolver and timestamp, and the reopening watermark set to now. reason is required, 1–500 characters.
POST /v1/admin/platform/abuse/throttle
{ "tenantId": "user_2abc…", "agent": "intake-agent", "signal": "THREAT_DETECTED",
"seconds": 3600, "reason": "Sustained DLP violations; contacted the account owner" }Quarantines the agent for seconds — bounded between 60 and 86,400 (24 hours). reason is required.
The important implementation detail: this writes the same Redis keys the automatic rate limiter writes. There is exactly one enforcement mechanism, so /v1/dispatch refuses the agent through the same gate with the same 429, the tenant's own /v1/admin/threats shows it identically, and release works the same way for both. The two paths cannot disagree about who is blocked.
POST /v1/admin/platform/abuse/release
Clears the quarantine and returns the signal to OPEN.
Billing overview#
GET /v1/admin/platform/billing
Instance-wide subscription state: tenant counts, the plan mix, paid-account counts, and figures derived from the billing provider.
If the provider is not configured, the response says so rather than returning an invented number. There is no historical revenue store in this stack, so there is no revenue trend — a figure that cannot be derived is absent rather than fabricated.
What is audited, and where#
This is the deliberate line, and it is worth understanding.
| Action | Recorded |
|---|---|
| Claim | Triage overlay only |
| Dismiss | Triage overlay only |
| Throttle | The affected tenant's audit chain — abuse.throttled |
| Release | The affected tenant's audit chain — threat.released |
| Tier override | The affected tenant's chain — subscription.changed |
Claim and dismiss are internal operator bookkeeping: they change nothing about what a tenant's agents may do, so writing them into a customer's evidentiary chain would be noise in a record meant for that customer's auditor.
Throttle and release do change what the tenant's agent is allowed to do. Those append to the tenant's own chain, with the reason and the actor. A tenant is never in a position where their traffic was blocked by a platform decision and their sealed record does not say so.
This is what to show a client who asks "what can the operator do to us?"
The answer is: impose or lift a quarantine, and change a tier — and every one of those actions appears in your audit chain, with a reason and an actor, sealed like everything else. They cannot read your payloads, cannot see your DLP matches, cannot read your keys, and cannot edit your chain without verification failing.
Operating notes#
Poll health at 10 seconds, not faster. The trend arrays are 60-second memoised and the probes are two round trips. Ten seconds is fast enough to see a Redis outage while you are looking at the page.
Watch the queue counts, not the queue. Waiting jobs climbing steadily means the worker is down or cannot keep up; failed jobs climbing means targets are rejecting deliveries.
Throttle with the shortest duration that solves the problem. An hour is usually enough to get a reply from the account owner. 24 hours is the ceiling, not the default — and it is a real outage for that tenant.
Always write a real reason. It lands in the tenant's sealed chain, and it is what you will be asked about later.
Fix the cause before releasing. Releasing an agent still in a retry loop restarts the flood.
Self-hosting these#
Set SUPER_ADMIN_EMAILS to the operator addresses and redeploy, or set publicMetadata.role on the Clerk user. With neither set, no account has platform access at all — which is the correct default for a single-tenant deployment where the abuse queue has nothing to review. See Running Arcus.