Integration patterns
Three ways to put the ResearchAnything API behind your own product, when each fits, and the details worth getting right the first time: keeping results and staying inside your monthly allowance.
01Pattern A — server-side proxy
The default. Your backend holds the ra- key and your client never sees it. Your frontend calls your own API, which checks the user with your own auth and then calls ResearchAnything with the key.
client → your API → researchanything.ai/api/query
A minimal Express handler. It accepts POST /research from a signed-in user, forwards the question, records the run against the user, and returns what the client needs.
import express from "express";
const app = express();
app.use(express.json());
app.post("/research", requireUser, async (req, res) => {
const upstream = await fetch("https://researchanything.ai/api/query", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ESSARION_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: req.body.query }),
signal: AbortSignal.timeout(10 * 60 * 1000),
});
const data = await upstream.json();
// Keep your own record of who asked what.
await db.runs.insert({
user_id: req.user.id,
request_id: data.request_id,
status: data.status,
created_at: new Date(),
});
if (!upstream.ok || data.status !== "ok") {
return res.status(502).json({ error: data.errors?.[0]?.code ?? "research_failed" });
}
res.json({ answer: data.answer, sources: data.sources });
});
Pros. The key stays on your infrastructure. You have one place for audit logging, per-user limits and your own content policy, and you can refuse a question before it uses a research run.
Cons. One more hop, and your server holds a connection open for the whole run. Give both your outbound call and any proxy or platform in front of your API a timeout of several minutes.
02Pattern B — background jobs
For batch work, or anywhere you don't want a user's request to hang, run the call in a worker. The API has no asynchronous mode of its own: a call returns only when its run has finished, and there is no endpoint to poll with an API key. So the job queue is yours: accept the request, return your own job id, let a worker make the call, and have the client poll your API.
// 1. Accept the request and queue it.
app.post("/jobs", requireUser, async (req, res) => {
const job = await db.jobs.insert({ user_id: req.user.id, query: req.body.query, status: "queued" });
await queue.add("research", { job_id: job.id });
res.status(202).json({ job_id: job.id });
});
// 2. The worker makes the call and stores the result.
queue.process("research", async ({ job_id }) => {
const job = await db.jobs.get(job_id);
const r = await fetch("https://researchanything.ai/api/query", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ESSARION_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: job.query }),
signal: AbortSignal.timeout(10 * 60 * 1000),
});
const data = await r.json();
await db.jobs.update(job_id, { status: data.status ?? "error", result: data });
});
// 3. The client polls your API, not ResearchAnything.
app.get("/jobs/:id", requireUser, async (req, res) => {
res.json(await db.jobs.get(req.params.id));
});
If you want the request_id as soon as the run starts, have the worker use the stream: its first event, start, carries it.
Pros. Long runs don't block your UI, and a failure is contained in one job.
Cons. Your worker holds one connection per running job, and there is no way to resume a run: if the worker dies part-way, the job has to be run again, and that uses another research run. Keep concurrency modest; each job draws on the same monthly allowance.
03Pattern C — streaming relay
To show a run live in someone's browser without sending them your key, relay the stream. The browser opens a streaming request to your server; your server opens one to /api/query/stream and passes each chunk on as it arrives.
app.post("/stream", requireUser, async (req, res) => {
const controller = new AbortController();
res.on("close", () => controller.abort());
const upstream = await fetch("https://researchanything.ai/api/query/stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ESSARION_KEY}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
body: JSON.stringify({ query: req.body.query }),
signal: controller.signal,
});
res.status(upstream.status);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders();
try {
for await (const chunk of upstream.body) res.write(chunk);
} catch {
// The browser went away and the upstream request was aborted.
}
res.end();
});
Aborting the upstream request when the browser leaves matters: closing the stream stops the run on ResearchAnything's side, so a user who navigates away doesn't keep a run going for nobody. The research run is still used.
Pros. Live progress, sources and reasoning in your UI, with the key kept on your server.
Cons. Long-lived connections through your whole stack. Some hosts cap request duration, and some proxies buffer event streams; see Streaming · Common pitfalls.
04Keeping results
Store what you need from each call in your own database. The API gives your key no way to fetch a past run again: run timelines live in the API Console, behind your signed-in session, and their step-by-step part is kept only for a few hours.
- The answer. Keep the whole envelope:
request_id,answer,sources(with their citations) anderrors. It is small, and it is the record of what your user was shown. - The process. If you want to show or audit how a run got to its answer, stream it and keep the
step,sourceandreasoningevents as they arrive. - The id. Keep
request_idwith your user's record. It lets you find the call in the Console's usage list if you need to look into it.
05Staying within your allowance
API calls are not billed per token. Each accepted call uses one research run from your plan's monthly allowance (3 on Free, 40 on Pro, 100 on Researcher), shared with runs you start in the app. When it runs out, every call fails with 429 and quota_exceeded until the next calendar month. If several of your users share one key, they share that allowance.
- Count calls per user in your own database, and refuse a request before forwarding it when a user is over the limit you set.
- Don't retry failed runs automatically. Each retry is a new run and uses another research run.
- When you see
quota_exceeded, stop calling until the next month and tell your users; the error'slimitandusedfields say where you stand. - Watch the monthly count on your ResearchAnything profile page, and move to a larger plan before you run out if you need to.
06Where to go next
- How the stream works on the wire: Streaming with SSE
- Using the citations in each source: Working with citations
- Every field and status code: API · Queries
- Keys and sessions: API · Authentication