OwlViz · API docs

Asset & collection intelligence for Solana.

Base URL: https://api.owlviz.dev

Try example mint , agents, or quick start.

Errors & rate limits

All API errors use a consistent JSON envelope:

json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message."
  }
}

Unexpected server errors may use a legacy Fastify shape without error.code. Client code should handle non-2xx responses defensively.

Stack traces are never returned to clients.

Rate limiting

When Upstash Redis is configured, public asset and collection routes are rate limited using fixed-window counters in Redis.

**Commercial Sprint C2** applies plan-based limits for keyed requests. See [developer-plans.md](./developer-plans.md) for the full matrix.

**C2.5 durable metering:** successful keyed requests on rate-limited public routes also increment daily Postgres counters (api_usage_daily) for portal and operator visibility. Metering is **fail-open** (a write failure never blocks the API). Monthly totals are **not** the same as RPM — they are attribution counters only, not billing.

Modes

ModeBucket keyLimits
**Anonymous** (no ov_live_ key)Hashed client IPFree tier — same as free plan
**Keyed** (Authorization: Bearer ov_live_…)API key IDdeveloper_accounts.plan_id entitlements

Plan defaults (60s window)

plan_idGlobal RPMExpensive RPM
free12060
builder600300
pro20001000

Free keyed limits match anonymous IP limits. Builder and Pro raise RPM when a valid API key is presented.

Environment overrides

VariableDefaultMeaning
RATE_LIMIT_WINDOW_SECONDS60Window length in seconds
RATE_LIMIT_MAX120Anonymous + Free keyed global RPM
RATE_LIMIT_ROUTE_MAX60Anonymous + Free keyed expensive-route RPM
PLAN_BUILDER_GLOBAL_RPM600Optional Builder global override
PLAN_BUILDER_EXPENSIVE_RPM300Optional Builder expensive override
PLAN_PRO_GLOBAL_RPM2000Optional Pro global override
PLAN_PRO_EXPENSIVE_RPM1000Optional Pro expensive override

Production asset GET /v1/assets/:mint exposes X-RateLimit-Limit: 60 for anonymous/Free keyed traffic (the tighter of global + route limits is enforced).

Tiers

**Expensive** (global + per-route counters):

  • GET /v1/assets/:mint (+ /health, /inspection, /media)
  • POST /v1/assets/:mint/media/recover
  • GET /v1/collections/:address (+ /assets, /health, /holders, /market)
  • POST /v1/collections/:address/index

**Lightweight** (global counter only):

  • GET /v1/assets/:mint/snapshot, /snapshots
  • GET /v1/collections/:address/index
  • GET /v1/collection-index-jobs/:jobId

Exempt:

  • GET /health

**Not rate limited** (no matching rule):

  • Monitor, event, and webhook routes

Response headers

HeaderDescription
X-RateLimit-LimitMaximum requests in the current window (merged limit)
X-RateLimit-RemainingRemaining requests
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait (429 only)

429 envelope

json
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please retry after the window resets."
  }
}

Fail-open

If Redis errors during a rate limit check, the request is **allowed** and a warning is logged (RATE_LIMIT_CHECK_FAILED). Rate limiting is disabled entirely when Redis is not configured.

Client IP identity (anonymous only)

Rate limit keys hash the resolved client IP. X-Real-IP is honored **only** when the socket peer is in TRUST_PROXY_CIDRS (includes 100.0.0.0/8 on Railway). Untrusted peers ignore spoofed X-Real-IP. **X-Forwarded-For is not used** for rate-limit identity.

Keyed requests ignore IP for rate limiting — the API key ID is the bucket identifier.

Error catalog

HTTPCodeMeaningDeveloper action
400INVALID_ASSET_IDMint is not a valid Solana public keyFix the address parameter
400INVALID_COLLECTION_ADDRESSCollection address invalidFix the address parameter
400INVALID_QUERY_PARAMETERBad query valueFix query string
400INVALID_MONITOR_TARGETMonitor target invalidProvide valid mint/address
400INVALID_MONITOR_INTERVALInterval out of rangeUse allowed interval
400INVALID_EVENT_TYPEUnknown event type for targetUse enum values from monitoring docs
400INVALID_WEBHOOK_URLWebhook URL blocked or malformedUse HTTPS URL passing SSRF checks
401INVALID_API_KEYInvalid or revoked OwlViz API keyCreate a new key or fix the Bearer header
401DEVELOPER_KEY_REQUIREDForced refresh, indexing, or recovery without key/adminSend Authorization: Bearer ov_live_… or admin token

Portal API key management (session BFF on owlviz.dev — not the public api.owlviz.dev surface):

HTTPCodeMeaning
401UNAUTHORIZEDSign in required
404PROJECT_NOT_FOUND / API_KEY_NOT_FOUNDProject or key not owned by the signed-in account
409ALREADY_REVOKEDKey was already revoked
401MONITORING_AUTH_REQUIREDMissing/invalid Bearer tokenSend Authorization: Bearer <private-beta-token>
403MONITORING_WRITES_FORBIDDENWrites disabled on deploymentUse read APIs or request beta access
404ASSET_NOT_FOUNDAsset not found upstreamVerify mint exists
404SNAPSHOT_NOT_FOUNDNo persisted snapshotWait for persistence or use live resolve
404COLLECTION_NOT_FOUNDCollection not registered (anonymous lookup)Sign in, use ov_live_ key, or POST .../index
429COLLECTION_AUTO_INDEX_LIMIT_REACHEDDaily auto-index quota exhaustedWait for UTC day rollover or upgrade plan
404COLLECTION_NOT_INDEXEDIndex not startedStart indexing
404INDEX_JOB_NOT_FOUNDJob ID unknownVerify job ID
404MONITOR_NOT_FOUNDMonitor or event not foundVerify ID
404WEBHOOK_NOT_FOUNDWebhook not foundVerify ID
409INDEX_ALREADY_RUNNINGActive job + refresh=truePoll existing job
429RATE_LIMITEDToo many requestsBack off using Retry-After
429MONITOR_LIMIT_REACHEDMax active monitorsDelete or pause monitors
429MARKET_WATCH_LIMIT_REACHEDMax active market watches (plan or deployment cap)Delete or pause watches
429WEBHOOK_LIMIT_REACHEDMax webhook endpoints for planDelete endpoints or upgrade plan
502UPSTREAM_ERRORProvider failureRetry with backoff
502UPSTREAM_INVALID_RESPONSEProvider returned invalid dataRetry later
502WEBHOOK_DELIVERY_FAILEDTest delivery failedFix receiver endpoint
503SERVICE_NOT_CONFIGUREDResolver not configuredContact operator
503PERSISTENCE_UNAVAILABLEDatabase not configuredEnable persistence or use live routes
503API_KEY_AUTH_UNAVAILABLEAPI key pepper or database not configuredServer configuration issue
503MONITORING_WRITES_NOT_CONFIGUREDWrites enabled but no admin tokenServer configuration issue
504UPSTREAM_TIMEOUTProvider timeoutRetry with backoff

CORS

When CORS_ALLOWED_ORIGINS is set, browser requests must originate from an allowed origin. Exposed headers include rate limit and cache headers listed above.