API Reference
Opsis Programmatic API — v1
The Opsis API lets you run OSINT searches programmatically and retrieve results in structured JSON. It exposes the same search engine powering the web app, accessible via standard HTTP requests from any language or environment.
Base URL
All API requests are made to the following production base URL:
https://api.useopsis.com
Authentication
All /v1 endpoints require an API key. Include your key as a Bearer token in the Authorization header:
Authorization: Bearer opsis_live_...
Alternatively, the X-API-Key header is accepted as an equivalent alternative:
X-API-Key: opsis_live_...
API keys are created on the API Keys page. Keys are available to accounts on the Pro and Unlimited plans only. Your key is shown once at creation — store it securely. If you lose it, generate a new one from the same page.
Credits
Each search consumes 1 credit from your plan's shared monthly pool. API searches and web-app searches draw from the same pool. Credits reset at the start of each billing cycle and do not roll over.
- Pro: 100 searches / month
- Unlimited: uncapped
A Premium search costs 2 credits. Lookup endpoints (/v1/credits, /v1/me, /v1/health, /v1/capabilities, and reading results) are always free.
Premium Searches
A standard search deliberately skips solver-backed platforms — the ones requiring browser automation to resolve. Passing "premium": true includes them.
| Standard | Premium | |
|---|---|---|
| Credits | 1 | 2 |
| Extra platforms | — | facebook, etsy, crunchbase, askfm, dailymotion, grailed, … |
| Plan required | Pro / Unlimited | Basic / Pro / Unlimited |
| Search types | all | username only |
# Premium username search (2 credits) — adds solver-backed platforms
curl -N -X POST https://api.useopsis.com/v1/searches \
-H "Authorization: Bearer opsis_live_xxx" \
-H "Content-Type: application/json" \
-d '{"type":"username","query":"johndoe","premium":true,"stream":true}'- An ineligible plan returns
403 plan_requiredbefore any credit is charged. premiumis ignored foremailanddomainsearches — you are never charged 2 credits for those.- Every API-eligible plan is also Premium-eligible, so if you have API access you can always pass it. It is opt-in because it costs double.
Endpoints
POST /v1/searches — Start a search
Submit a new search. Returns a run_id immediately, or inline results if wait is true.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | One of email, username, domain |
| query | string | Yes | The email address, username, or domain to search |
| wait | boolean | No (default: false) | If true, the request blocks up to 30 seconds and returns results inline if the search finishes in time. If it does not finish in 30s, responds with status: "running" — poll the GET endpoint. |
| premium | boolean | No (default: false) | Include solver-backed platforms that a standard search skips. Username searches only and costs 2 credits instead of 1. See the Premium Searches section below. |
| stream | boolean | No (default: false) | If true, the response is a Server-Sent Events stream (text/event-stream) that pushes each account the moment its module resolves, plus progress frames and a final done frame. This is the recommended way to consume a full search — no polling, lowest latency. Takes precedence over wait if both are set. |
Responses
wait: false (default) — returns immediately:
{"run_id":"<uuid>","status":"running"}wait: true and search completed within 30s:
{"run_id":"<uuid>","status":"completed","total_results":12,"results":[ <result>, ... ]}wait: true but still running after 30s — poll with GET:
{"run_id":"<uuid>","status":"running"}stream: true — an SSE stream (see the Streaming section below):
event: run
data: {"run_id":"<uuid>","type":"username","query":"johndoe","status":"running"}
event: result
data: {"id":1,"type":"account","platform":"github","value":"https://github.com/johndoe","confidence":1.0,"source":"github","metadata":{},"created_at":"..."}
event: progress
data: {"results_so_far":1,"modules_total":512,"modules_completed":48,"modules_with_hits":1}
event: result
data: { ...next account the moment its module resolves... }
event: done
data: {"run_id":"<uuid>","status":"completed","total_results":12,"modules_total":512,"modules_completed":512,"modules_with_hits":12}GET /v1/searches/{run_id} — Fetch status and results
Poll a search by its run_id until status is completed or failed. Results accumulate as modules finish, so a mid-run poll returns the accounts found so far alongside a progress block.
{
"run_id": "<uuid>",
"status": "running" | "completed" | "failed",
"type": "<email|username|domain>",
"query": "<the original query>",
"total_results": 12,
"results": [ <result>, ... ],
"progress": {
"modules_total": 512,
"modules_completed": 512,
"modules_with_hits": 12
},
"created_at": "<ISO-8601>",
"completed_at": "<ISO-8601 or null>"
}GET /v1/searches/{run_id}/stream — Attach a stream to a run
SSE tail of an already-started run — use it when you started a search with wait:false and now want to stream the rest instead of polling. Emits the same frames as POST with stream:true. If the run is already terminal, it replays the results then a done frame immediately.
GET /v1/searches — List recent searches
Your most recent runs (metadata only), newest first. Accepts ?limit= (1–100, default 20). Call GET /v1/searches/{run_id} for a run's results.
Streaming (Server-Sent Events)
A full search touches ~500 modules and takes ~60–90 seconds. Instead of polling, set stream:true on POST /v1/searches (or call GET /v1/searches/{run_id}/stream) to receive results incrementally over a single long-lived connection. Pass curl -N (or an EventSource / SSE client) so frames are not buffered.
Frame types
| event | data |
|---|---|
| run | Sent once, first. The run's id, type, and query. |
| result | One account, emitted the instant its module resolves — the Result Object shape below. |
| progress | Emitted whenever the completed-module count advances: modules_completed / modules_total and results_so_far. |
| done | Sent once, last. Final status and total_results. The stream closes after this. |
| error | An unexpected server-side failure; the stream then terminates cleanly. |
Example frame sequence:
event: run
data: {"run_id":"<uuid>","type":"username","query":"johndoe","status":"running"}
event: result
data: {"id":1,"type":"account","platform":"github","value":"https://github.com/johndoe","confidence":1.0,"source":"github","metadata":{},"created_at":"..."}
event: progress
data: {"results_so_far":1,"modules_total":512,"modules_completed":48,"modules_with_hits":1}
event: result
data: { ...next account the moment its module resolves... }
event: done
data: {"run_id":"<uuid>","status":"completed","total_results":12,"modules_total":512,"modules_completed":512,"modules_with_hits":12}A stream stays open until the run finishes, or up to a hard ceiling (~180s) after which a done with status:"timeout" is sent and the connection closes. Lines beginning with : are keep-alive comments — ignore them.
Account & Credits
GET /v1/credits — Credit balance
Your remaining, used, and allowed credits for the current billing period. On the Unlimited plan, credits_remaining and credits_allowed are the string "unlimited".
{
"tier": "pro",
"credits_remaining": 88,
"credits_used": 12,
"credits_allowed": 100,
"period_start": "<ISO-8601>",
"period_end": "<ISO-8601>"
}GET /v1/me — Account & capabilities
A self-check endpoint: confirms your key works, your plan, API eligibility, your rate limit, and your live credit balance. The first call a new integration should make.
{
"user_id": "user_xxx",
"tier": "pro",
"api_access": true,
"rate_limit_per_minute": 30,
"supports_streaming": true,
"search_types": ["domain","email","username"],
"credits": { "remaining": 88, "used": 12, "allowed": 100, "period_end": "<ISO-8601>" }
}Discovery & Health
Both endpoints are public — no API key required — so you can probe availability and learn the contract before authenticating.
GET /v1/health — Service health
Liveness plus a lightweight database check. status is "degraded" if the DB ping fails. Returns no user-specific data.
{
"status": "ok",
"service": "opsis-api",
"version": "v1",
"database": "ok",
"streaming": true,
"time": "<ISO-8601 UTC>"
}GET /v1/capabilities — Discover the API
A machine-readable description of the whole API — search types, the three modes, the result schema, limits, auth, and links to these docs and the OpenAPI spec. The one call a tool builder or LLM makes to learn how to drive the API.
{
"version": "v1",
"search_types": ["domain","email","username"],
"modes": {
"async": "POST /v1/searches -> {run_id}; poll GET /v1/searches/{run_id}",
"sync": "POST /v1/searches with wait=true -> inline results",
"stream": "POST /v1/searches with stream=true -> text/event-stream"
},
"result_fields": { "id": "int ...", "type": "string ...", "...": "..." },
"limits": { "rate_limit_per_minute": 30, "read_rate_limit_per_minute": 120,
"credits_per_search": 1, "premium_credits_per_search": 2,
"sync_wait_seconds": 30, "stream_timeout_seconds": 180 },
"auth": { "scheme": "bearer", "header": "Authorization: Bearer opsis_live_...",
"plan_required": ["pro","unlimited"] },
"premium": { "applies_to": ["username"], "credits": 2, "module_count": 27,
"plan_required": ["basic","pro","unlimited"] },
"docs_url": "https://useopsis.com/docs/api",
"openapi_url": "https://api.useopsis.com/openapi.json"
}For Machines & LLMs
The API is built to be driven programmatically without reading prose:
GET /v1/capabilities— discovery (types, modes, schema, limits). No key required.GET /openapi.json— the full OpenAPI 3 schema for every/v1endpoint with typed models. Feed it to an SDK/codegen tool or an LLM to generate a working client. Interactive docs at/docs(Swagger UI) and/redoc.GET /v1/health— availability probe. No key required.
Recommended bootstrap: health → capabilities → me (verify key + plan) → first POST /v1/searches.
Status Lifecycle
A search transitions running → completed (or failed on an unrecoverable error). Prefer stream:true to receive results as they land. If you poll instead (wait:false), call GET /v1/searches/{run_id} every 2–5 seconds until the status is terminal — results and the progress block accumulate across polls.
Result Object
Each item in the results array has the following shape:
{
"id": 7,
"type": "<entity type, e.g. account>",
"entity_type": "<alias of type>",
"platform": "<string or null>",
"value": "<string or null>",
"confidence": 1.0,
"source": "<module name>",
"metadata": { },
"created_at": "<ISO-8601 timestamp>"
}| Field | Description |
|---|---|
| id | Sequential per-run identifier for the result (stable within a run) |
| type | Entity type returned by the module (e.g. account, profile). entity_type is an alias with the same value. |
| platform | Platform or service name, or null |
| value | Primary value of the entity (e.g. a username or URL), or null |
| confidence | Confidence score from 0.0 to 1.0 |
| source | Internal module name that produced this result |
| metadata | Additional key/value data specific to the module (may be an empty object) |
| created_at | ISO-8601 timestamp of when the result was recorded |
Error Responses
Errors are returned as JSON with one of two shapes: {"error":"<code>","message":"..."} or {"detail":{...}}.
| HTTP Status | Error Code | Meaning |
|---|---|---|
| 400 | invalid_request | Unknown type, missing query, or a query that is not a valid email address, username, or domain |
| 401 | invalid_api_key | Missing, malformed, or revoked API key (“Invalid API key”) |
| 402 | insufficient_credits | Out of monthly search credits |
| 403 | plan_required | The key's owner is not on the Pro or Unlimited plan — or requested premium without a Premium-eligible plan |
| 429 | (rate limited) | Exceeded the endpoint's rate limit: 30/min for POST /v1/searches, 60/min for streams, 120/min for reads |
Examples
Async (fire and poll)
Start a username search, receive a run_id, then poll until complete:
# Async: start a search, then poll
curl -X POST https://api.useopsis.com/v1/searches \
-H "Authorization: Bearer opsis_live_xxx" \
-H "Content-Type: application/json" \
-d '{"type":"username","query":"johndoe"}'
# -> {"run_id":"<uuid>","status":"running"}
curl https://api.useopsis.com/v1/searches/<uuid> \
-H "Authorization: Bearer opsis_live_xxx"Sync (wait inline)
Block up to 30 seconds and get results in a single request — ideal for email or domain lookups:
# Sync: wait inline (best for fast email/domain lookups)
curl -X POST https://api.useopsis.com/v1/searches \
-H "Authorization: Bearer opsis_live_xxx" \
-H "Content-Type: application/json" \
-d '{"type":"email","query":"[email protected]","wait":true}'
# -> {"run_id":"<uuid>","status":"completed","results":[ ... ]}Streaming (recommended)
One request, results pushed as each module resolves — the lowest-latency way to run a full search:
# Streaming: one request, results pushed as each module resolves
curl -N -X POST https://api.useopsis.com/v1/searches \
-H "Authorization: Bearer opsis_live_xxx" \
-H "Content-Type: application/json" \
-d '{"type":"username","query":"johndoe","stream":true}'Getting Started
1. Upgrade to Pro or Unlimited if you have not already.
2. Visit the API Keys page to generate your key. Copy and store it securely — it will not be shown again.
3. Make your first request using the curl examples above or any HTTP client.
Questions or issues? Reach us at [email protected].