Deep Water API
A deterministic research pipeline you call over REST. Submit a query, get back a verified report with every claim cited to its source span. Pay for tokens actually consumed — minimum $0.10 per job.
Base URL
https://api.deepwater.live
Auth
Authorization: Bearer <key>
Status
LiveAuthentication
Every request is authenticated with an API key. Create a key from the dashboard at admin.deepwater.live. Keys are scoped per user and can be revoked at any time without affecting other keys.
curl https://api.deepwater.live/v1/research \
-H "Authorization: Bearer dw_live_…" \
-H "Content-Type: application/json" \
-d '{"query":"market share of electric vehicles in the EU in 2025","depth":"standard"}'
Treat keys like passwords. Never embed a key in front-end code — use the dashboard to create per-environment keys and rotate on a schedule.
Quickstart
- Create an API key in the dashboard.
- POST a query to
/v1/research. You’ll get back a job id and an initialqueuedstatus. - Choose how you want to be notified of progress: WebSocket (stream) or webhook (HTTP callback). Both can run in parallel.
- Read the final report with
GET /v1/research/:id.
Create research
POST /v1/research
Submits a new research job. The response returns immediately with 202 Accepted and the job id — the actual research runs asynchronously.
{
"query": "summarise recent developments in ultrasound neuromodulation",
"depth": "deep",
"webhook_url": "https://your-app.example.com/hooks/deepwater",
"task_model": "gpt-5.4-nano",
"report_model": "gpt-5.4",
"search_provider": "serpapi"
}
Request body
| Field | Type | Notes |
|---|---|---|
query | string | Required. Plain-language research question. |
depth | string | One of light, standard, deep, heavy. |
webhook_url | string | Optional HTTPS URL to receive the terminal callback. |
task_model | string | Optional override for the model that handles scoping, extraction tasks, and verification. |
report_model | string | Optional override for the final synthesis model. |
search_provider | string | Optional. Defaults to account setting. |
Response
{
"id": "res_01H9XZY5",
"status": "queued",
"depth": "deep",
"created_at": "2026-04-11T02:30:18.912Z",
"config": {
"max_urls": 500,
"max_rounds": 8,
"max_sub_questions": 8,
"max_output_tokens": 8000,
"min_output_tokens": 3000,
"section_synthesis": true
}
}
Get research
GET /v1/research/:id
Fetches the full job snapshot including the current status, sub-questions, sources, usage totals, and — once complete — the final markdown report.
{
"id": "res_01H9XZY5",
"status": "complete",
"query": "...",
"report": "# Ultrasound neuromodulation ...",
"sources": [ { "url": "https://...", "title": "..." } ],
"sub_questions": [ { "id": "sq_1", "question": "...", "status": "complete" } ],
"usage": {
"cloud_calls": 3,
"cloud_prompt_tokens": 18240,
"cloud_completion_tokens": 4210,
"cloud_cost_usd": 0.41,
"urls_fetched": 487,
"gather_rounds": 6
}
}
Cancel research
POST /v1/research/:id/cancel
Cancels an in-flight job. You’ll only be charged for work already completed. Jobs that have already entered the verifying phase cannot be cancelled — they’re nearly done anyway.
Depth presets
Each preset maps to a budget for sub-questions, gather rounds, and URLs. Heavier presets search more of the web and synthesise longer, more structured reports.
| Depth | Sub-questions | Gather rounds | URLs | Typical price |
|---|---|---|---|---|
light | 3 | 2 | ~50 | 1 token |
standard | 5 | 5 | ~200 | 3 tokens |
deep | 8 | 8 | ~500 | 8 tokens |
heavy | 12 | 12 | ~1000 | 15 tokens |
These are typical minimums. Complex research with long synthesis or heavy paywalls may cost more. 1 token = $1.00.
WebSocket progress
Open a WebSocket to watch a job in real time. As the engine transitions between phases (scope → gather → synthesise → verify) and completes gather rounds, you’ll receive JSON events on the socket. The stream closes on its own when the job reaches a terminal status.
GET wss://api.deepwater.live/v1/research/:id/stream
Connecting
// Browser
const socket = new WebSocket(
`wss://api.deepwater.live/v1/research/${jobId}/stream`,
);
socket.onmessage = (event) => {
const payload = JSON.parse(event.data);
console.log(payload.kind, payload.status, payload.message);
};
socket.onclose = () => {
console.log('stream closed, fetch final report');
};
Event schema
{
"jobId": "res_01H9XZY5",
"kind": "phase_enter" | "phase_exit" | "scope_outline" | "gather_round" | "synthesis_token" | "terminal",
"phase": "scope" | "gather" | "synthesise" | "verify",
"status": "queued" | "scoping" | "gathering" | "synthesising" | "verifying" | "complete" | "failed",
"message": "Searching, fetching and extracting sources",
"data": { "urls": 487, "rounds": 6 },
"at": "2026-04-11T02:31:04.221Z"
}
Event kinds
| Kind | When it’s emitted |
|---|---|
phase_enter | Engine has transitioned into a new phase. |
scope_outline | Sub-questions have been identified. data.outline contains them. |
gather_round | Gather phase completed. data.urls, data.rounds, data.artifacts. |
synthesis_token | Reserved for token-level streaming in a later release. |
terminal | Job reached complete, failed, or cancelled. The server closes the socket immediately after. |
Reconnection
If the socket drops, re-open it against the same job id — you’ll immediately receive a phase_enter event reflecting the current phase, followed by any future events. If the job already finished while you were disconnected, the socket will send a final terminal event and close. Always use GET /v1/research/:id as the source of truth for the final report.
Webhook callbacks
Include webhook_url in the create-research request and we’ll POST to that URL when the job reaches a terminal status. Webhooks are delivered with up to 3 retries and exponential backoff. Use this when you don’t want to hold a WebSocket connection open.
Request format
POST https://your-app.example.com/hooks/deepwater
Content-Type: application/json
X-Deepwater-Signature: sha256=2c1d…
{
"event": "research.complete",
"job_id": "res_01H9XZY5",
"status": "complete",
"report": "# Ultrasound neuromodulation ..."
}
On failure:
{
"event": "research.failed",
"job_id": "res_01H9XZY5",
"status": "failed",
"failure_reason": "scope phase timed out after 600s"
}
Signature verification
Every webhook carries an X-Deepwater-Signature header. Verify it with the shared secret assigned when you created the key:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
Retries
If your endpoint returns a non-2xx or the request times out, we retry up to 3 times with exponential backoff (100ms, 400ms, 900ms). Keep the handler idempotent — you may see the same job_id more than once, and should dedupe by job_id + event.
CLI
The Deep Water CLI is a thin wrapper around the REST API. Install globally and point it at your key.
npm install -g @unlikeotherai/deepwater
export DEEPWATER_API_KEY=dw_live_…
deepwater research "recent breakthroughs in room-temp superconductors" --depth deep --stream
The --stream flag opens the WebSocket and prints phase progress to the terminal until the job completes.
MCP server
Deep Water ships an MCP (Model Context Protocol) server so agents and IDE integrations can drive the full API surface — create research, stream progress, manage keys, pull usage — as named tools. Drop-in config for Claude Code, Cursor, Windsurf, and any other MCP-aware client.
{
"mcpServers": {
"deepwater": {
"command": "npx",
"args": ["-y", "@unlikeotherai/deepwater", "mcp"],
"env": { "DEEPWATER_API_KEY": "dw_live_..." }
}
}
}
Cost model
You pay for the LLM tokens a job actually consumes plus a small fixed infrastructure cost per URL processed, with a 1.5× margin. A hard $0.10 minimum applies so the smallest jobs cover their fixed cost. The live calculator on deepwater.live reflects the exact formula we use.
Errors
Errors follow the RFC 7807 Problem Details format.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://deepwater.live/problems/invalid-request",
"title": "Invalid request",
"status": 400,
"detail": "depth must be one of light, standard, deep, heavy"
}