The REST API.
Your audit trail, library, tool registry, usage and connected clients over plain HTTP. JSON in, JSON out, one bearer key, no SDK required.
https://omniio.dev/api/v1Overview
Every endpoint is under https://omniio.dev/api/v1, takes and returns application/json, and is scoped to the account the key belongs to. There is no account or workspace id in any path — the key is the scope.
The API is deliberately narrow. Five resources are readable and exactly one field is writable, because the things worth doing from a script are reporting and deployment checks, and the things that hand Omniio a credential belong behind a session where a person can see what they are connecting.
- GET /activity
- Tool calls, newest first, as far back as your retention reaches.
- GET /servers
- Your library, with each server's real resolved status.
- GET · PATCH /servers/{slug}
- One server, and the one thing about it a program may change.
- GET /tools
- Every qualified tool name your endpoint currently answers.
- GET /usage
- This month's calls, the plan behind them, and when the counter rolls.
- GET /clients
- Clients holding a credential for this account, and ones that ever called.
Authentication
One header. Keys are created at settings → API keys, shown once, and stored as a SHA-256 digest — we cannot show you an existing key, only issue another.
curl -H "Authorization: Bearer omn_…" \ "https://omniio.dev/api/v1/usage"- Authorizationrequired
- Bearer omn_…. A missing or unparseable header answers 401 with WWW-Authenticate: Bearer.
- Content-Typewrites only
- application/json on PATCH. Reads take no body.
Every request stamps its key’s last used time, so a key nothing has touched in months is visible as such on the settings screen. Revoking one takes effect on the next request.
Rate limits
Counted per key, per minute, in a fixed window aligned to the clock. Every plan’s ceiling:
- Free
- 120 requests a minute per key · audit retention 7 days
- Pro
- 300 requests a minute per key · audit retention 30 days
- Scale
- 600 requests a minute per key · audit retention 3 months
- Business
- 1,200 requests a minute per key · audit retention 6 months
- Enterprise
- Negotiated. No counter runs and no rate-limit headers are sent.
This is a different budget from the burst limit your agents call through, on purpose. That one is per account because it protects the upstream servers everybody shares; this one is per key because nothing behind a read is shared — it is your own rows out of your own database. Counting them together would let a script looping on /activity refuse an agent’s tool call, which is the opposite of what a limit is for. API requests are not billed as calls.
Every metered response carries where you stand:
HTTP/1.1 200 OKX-RateLimit-Limit: 300X-RateLimit-Remaining: 287X-RateLimit-Reset: 1760000040- X-RateLimit-Limitinteger
- Requests this key may make in a window.
- X-RateLimit-Remaininginteger
- How many are left in the current one. Floors at zero rather than going negative.
- X-RateLimit-Resetinteger
- When the window rolls, as seconds since the Unix epoch — the same convention GitHub and Stripe use, so existing back-off code reads it without a special case.
- Retry-Afterinteger
- Seconds to wait. Sent only with a 429.
Over the line, the answer is 429 with the wait in both a header and the body:
{ "error": "rate_limited", "message": "Rate limit reached: the Pro plan serves 300 API requests per minute per key, and this key has spent this minute's. Retry after 12 seconds. This limit is per key and separate from your MCP burst limit — your agents are unaffected, and nothing was billed. Raise it at https://omniio.dev/pricing.", "retryAfter": 12}Errors
Every failure has the same shape: a stable error code to branch on and a message written for whoever has to work out why the nightly sync stopped. The HTTP status agrees with the code; neither is ever the only signal.
{ "error": "invalid_request", "message": "\"limit\" must be a whole number between 1 and 200."}- unauthorized401
- No bearer credential, or one that is not a live Omniio key. Revoked keys answer this too.
- invalid_request400 · 409
- A parameter or body field is wrong (400), or the change asked for conflicts with the state of the thing (409). The message names the field.
- not_found404
- No such resource on this account. Slugs are checked against your library, so a server you have not added reads as missing rather than forbidden.
- rate_limited429
- This key has spent its minute. Wait the number of seconds in Retry-After and repeat the request unchanged.
- method_not_allowed405
- The endpoint does not answer that verb. The Allow header lists the ones it does.
- server_error500
- Something broke on our side. Safe to retry; if it persists, tell us and quote the time.
429 is the only status worth retrying automatically, and only after the wait it names. Retrying a 400 unchanged will fail identically.
Pagination
Two schemes, because two different kinds of set are being walked.
Cursors, for the audit trail
/activity returns nextCursor: pass it back as ?cursor= for the next page, and stop when it is null. The cursor is the timestamp of the oldest row you were given, so new calls arriving mid-walk cannot shift rows into a page you have already read — which is exactly what an offset would do on a table that grows while you page through it.
Offsets, for the registry
/tools returns nextOffset and total. This set is small, finite and not appended to while you read it, so an offset is honest here and simpler to resume.
Neither, for the short lists
/servers and /clients return everything with a total beside it. Both are lists in the low hundreds at most; saying so plainly beats leaving you to infer it from a missing cursor.
GET/activity
Tool calls on this account, newest first. The same rows the activity screen shows and the same rows a webhook delivers, so a receiver that missed a delivery catches up here.
curl -H "Authorization: Bearer omn_…" \ "https://omniio.dev/api/v1/activity?limit=50&client=claude-code"Query parameters
- limitinteger · default 50
- Rows to return, 1 to 200.
- cursorISO 8601
- The previous response's nextCursor. Anything unparseable as a date is refused.
- clientstring
- Narrow to one client id, as reported by /clients.
Response
{ "data": [ { "id": "0f4a6f0c-1b7e-4a3f-9c21-6d8a1f5b2c34", "createdAt": "2026-01-14T09:31:04.812Z", "clientId": "claude-code", "server": "GitHub", "serverSlug": "github", "tool": "create_pull_request", "qualifiedName": "github__create_pull_request", "ok": true, "durationMs": 812, "error": null, "arguments": { "owner": "acme", "repo": "web", "title": "Fix login" }, "result": { "url": "https://github.com/acme/web/pull/412" } } ], "nextCursor": "2026-01-14T09:12:55.104Z", "retentionDays": 30}- data[]array
- The calls. Fields below.
- nextCursorstring | null
- Pass back as ?cursor= for the next page. Null at the end of the window.
- retentionDaysinteger
- How far back this plan serves. Rows older than this are pruned nightly and are never returned, so this is how far a full walk can reach.
A call
- idstring
- The audit row's UUID. Stable, and unique across the account — use it to deduplicate.
- createdAtstring
- ISO 8601 UTC, when the call finished. This is the value nextCursor pages on.
- clientIdstring | null
- Which connected client made the call. Null for calls made before client identification, or from a client that registered without one.
- serverstring
- The upstream server's display name, as the library shows it.
- serverSlugstring
- Its slug — the half of a qualified name before the separator, and what ?server= takes.
- toolstring
- The upstream tool's own name, unqualified.
- qualifiedNamestring
- The name your agent called, "slug__tool".
- okboolean
- Whether the upstream returned a result. A refusal by Omniio — quota, burst limit, a policy — is false with the reason in error.
- durationMsnumber | null
- How long the upstream took, in milliseconds. Null where the call never reached one.
- errorstring | null
- The failure, in a sentence. Null when ok is true.
- argumentsobject | string | null
- What was sent upstream, parsed. A payload past the 64 KB storage cap arrives as a string with a sentence saying where it was cut.
- resultobject | string | null
- What came back, under the same rule.
Servers
GET/servers
Your library, with defaults already applied: a server you have never explicitly toggled reports the status it would actually answer with, not the catalog’s opinion of it. That is the difference between an API a deployment check can trust and one that has to reimplement the resolution rules to be believed.
Query parameters
- status"enabled" | "pending" | "disabled"
- Filter by resolved status. Anything else is refused.
- categorystring
- Filter by catalog category, as it appears on any server.
Response
{ "data": [ { "slug": "github", "name": "GitHub", "description": "Repositories, issues and pull requests.", "category": "development", "status": "enabled", "authType": "oauth", "endpoint": "https://api.githubcopilot.com/mcp/", "toolCount": 41, "lastAttemptAt": "2026-01-14T09:30:58.204Z", "lastError": null, "owned": false } ], "total": 1}- slugstring
- The stable identifier. This is what a qualified tool name is built from and what the path takes.
- namestring
- Display name.
- descriptionstring
- One line, from the catalog.
- categorystring
- The catalog category. Any value here is valid as ?category=.
- status"enabled" | "pending" | "disabled"
- As this account would actually answer, defaults resolved. Pending means it is on but has not been catalogued yet.
- authType"none" | "bearer" | "oauth"
- What it takes to connect. Only none can be switched with PATCH.
- endpointstring
- The upstream MCP URL Omniio calls.
- toolCountnumber
- Tools in the cached catalog. Zero on a server that has never been reached.
- lastAttemptAtstring | null
- ISO 8601, the last time Omniio tried to catalogue it.
- lastErrorstring | null
- Why that attempt failed, if it did. This is the field a health check should read.
- ownedboolean
- True for a server this account added itself rather than one from the catalog.
GETPATCH/servers/{slug}
GET returns one server in the shape above, or 404 if this account has no server by that slug.
PATCH is the API half of the library toggle and shares its implementation, so enabled means exactly what it means on the screen. The response is a fresh read of the server, not an echo of what you sent.
curl -X PATCH \ -H "Authorization: Bearer omn_…" \ -H "Content-Type: application/json" \ -d '{"enabled": true}' \ "https://omniio.dev/api/v1/servers/astro-docs"Body
- enabledboolean · required
- The only field this endpoint changes. Any other key is ignored; a body without this one is refused.
Enabling a server starts a catalog refresh in the background. A server that has never been reached comes back pending with toolCount: 0; poll GET until status is enabled, or read lastError to see why it will not be.
GET/tools
Every qualified tool name your endpoint currently answers — the registry screen’s data, without having to speak MCP to ask for it. This is what makes does our deployment still expose github__create_pull_request a question you can fail a build over.
Served from the cache, with any stale server refreshed after the response is sent: a caller polling this gets an answer in milliseconds rather than waiting on however many upstreams are cold.
Query parameters
- limitinteger · default 100
- Tools to return, 1 to 500.
- offsetinteger · default 0
- How many to skip, up to 100,000.
- serverstring
- Narrow to one server slug.
- schemaboolean · default false
- Set to "true" to include each tool’s full JSON input schema. Off by default because it is a large body and almost never what a list is for.
Response
{ "data": [ { "qualifiedName": "github__create_pull_request", "name": "create_pull_request", "serverSlug": "github", "server": "GitHub", "title": "Create pull request", "description": "Opens a pull request on a repository." } ], "total": 214, "nextOffset": 100}- data[].qualifiedNamestring
- The name an agent calls, "slug__tool".
- data[].namestring
- The upstream tool's own name.
- data[].serverSlugstring
- Which server it comes from.
- data[].serverstring
- That server's display name.
- data[].titlestring | null
- The upstream's human title, where it publishes one.
- data[].descriptionstring | null
- What the tool does, as the upstream describes it.
- data[].inputSchemaobject
- JSON Schema for the tool's arguments. Present only when ?schema=true.
- totalinteger
- Tools matching the filter, before limit and offset.
- nextOffsetinteger | null
- Pass as ?offset= for the next page. Null on the last one.
GET/usage
What this account has spent this month and what it is allowed — the number on the settings screen, reachable from a dashboard or an alerting rule. Team-aware in the same way the screen is: a member reads the team’s pooled total, because that is the number that will actually stop their agents.
{ "plan": { "id": "pro", "name": "Pro", "includedCalls": 50000, "hardLimit": false, "overageEurPer1k": 2, "burstPerMinute": 120, "apiPerMinute": 300, "auditRetentionDays": 30 }, "period": "2026-01", "calls": 18402, "includedCalls": 50000, "blocked": false, "resetsAt": "2026-02-01T00:00:00.000Z", "team": null}- planobject
- The plan this account is on, with every published ceiling on it.
- plan.includedCallsinteger | null
- Calls included each month. Null on a negotiated plan.
- plan.hardLimitboolean
- True where exceeding the allowance refuses calls rather than billing them.
- plan.overageEurPer1knumber | null
- What a thousand calls past the allowance costs, in euro. Null where there is no overage.
- plan.burstPerMinuteinteger | null
- MCP calls a minute, per account. This is the agents' limit, not this API's.
- plan.apiPerMinuteinteger | null
- REST requests a minute, per key. The limit this endpoint is counted against.
- plan.auditRetentionDaysinteger
- How long tool calls are kept.
- periodstring
- The billing month being reported, "YYYY-MM".
- callsinteger
- Calls made in it so far, pooled across the team where there is one.
- includedCallsinteger | null
- The allowance in force, which is the plan's unless a negotiated one overrides it.
- blockedboolean
- True when a hard-limited plan has spent its allowance and is refusing calls right now. This is the field an alert should watch.
- resetsAtstring
- ISO 8601, when the counter rolls.
- teamstring | null
- The team the allowance is pooled with, or null on a solo account.
GET/clients
Every client that holds a credential for this account, or ever reached the endpoint. Both halves are here for the reason they are on the connections screen: a client that authorized and never called still holds a key, and a revoked one stays listed so the revocation is checkable rather than merely absent.
{ "data": [ { "clientId": "claude-code", "label": "Claude Code", "registeredName": "Claude Code", "active": true, "authorizedAt": "2026-01-02T11:04:21.000Z", "firstSeenAt": "2026-01-02T11:04:44.000Z", "lastSeenAt": "2026-01-14T09:31:04.000Z", "revokedAt": null, "calls": 1284, "lastCallAt": "2026-01-14T09:31:04.812Z" } ], "total": 1, "retentionDays": 30}Query parameters
- active"true" | "false"
- Filter to live credentials or to revoked ones. Omit for both.
A client
- clientIdstring
- The OAuth client id. This is the value /activity's clientId matches.
- labelstring | null
- The name you gave it, if any.
- registeredNamestring | null
- The name the client registered itself under.
- activeboolean
- False once revoked.
- authorizedAtstring | null
- ISO 8601, when it completed the OAuth flow.
- firstSeenAtstring | null
- When it first called the endpoint. Null on a client that authorized and never called — worth reviewing.
- lastSeenAtstring | null
- When it last called.
- revokedAtstring | null
- When its credential was retired.
- callsinteger
- Tool calls attributed to it inside the retention window, so this figure moves as old rows are pruned.
- lastCallAtstring | null
- The most recent one.
CORS
Every endpoint answers Access-Control-Allow-Origin: * and handles OPTIONS, with a preflight cached for 10 minutes. The rate-limit headers and Retry-After are on the expose list, so a browser client can read them.
Access-Control-Allow-Credentials is deliberately absent. There is no cookie and no ambient credential in play — every request carries a bearer key explicitly — so a page that has not been given one cannot borrow anybody’s session by calling from a browser.
TypeScript client
One file, no dependencies, no build step. It runs anywhere fetch exists — Node 18+, Deno, Bun, Cloudflare Workers — and it handles the two things that are actually awkward about this API: walking a cursor to the end of the retention window, and backing off when a key’s minute is full, using the server’s own Retry-After rather than an invented curve.
It is not published to a package registry. Download it into your project and it is yours — you can read all of it in one sitting, which is more than can be said for most generated clients:
curl -O https://omniio.dev/sdk/omniio.tsimport { Omniio, OmniioError } from "./omniio";const omniio = new Omniio({ apiKey: process.env.OMNIIO_API_KEY! });// One page, newest first.const page = await omniio.activity({ limit: 50 });// Or the whole retention window, a page at a time, cursor handled for you.for await (const call of omniio.activityAll()) { if (!call.ok) console.warn(call.qualifiedName, call.error);}try { await omniio.setServerEnabled("astro-docs", true);} catch (error) { if (error instanceof OmniioError && error.code === "invalid_request") { // That server needs a credential, which is connected in the app. }}- activity(params)
- One page. Returns data, nextCursor and retentionDays.
- activityAll(params)
- An async iterator over every call, following the cursor. Yields rows rather than pages, because a busy month held in one array is how a warehouse sync runs out of memory.
- servers(params)
- The library, filtered by status or category.
- server(slug)
- One server.
- setServerEnabled(slug, enabled)
- The PATCH, returning the server as it now stands.
- tools(params)
- One page of the registry.
- toolsAll(params)
- An async iterator over every tool, following the offset.
- usage()
- This month's counts and the plan behind them.
- clients(params)
- Connected clients.
Failures arrive as an OmniioError carrying code, status and, on a 429, retryAfter — so your retry logic branches on the code while your logs get the sentence. Constructor options are apiKey, baseUrl, fetch and maxRetries (three by default; zero throws the rate limit straight through).