DocsGuidesIntegration patterns

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.

NoteThe patterns combine. A common setup is a server-side proxy for everything, with a streaming relay for the screens where people watch a run.

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.

Node — Express
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.

Node
// 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.

Node — Express
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.

CautionWrite each chunk as it arrives and flush headers first. A framework that buffers the response will deliver the whole stream at the end.

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.

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.

  1. Count calls per user in your own database, and refuse a request before forwarding it when a user is over the limit you set.
  2. Don't retry failed runs automatically. Each retry is a new run and uses another research run.
  3. When you see quota_exceeded, stop calling until the next month and tell your users; the error's limit and used fields say where you stand.
  4. Watch the monthly count on your ResearchAnything profile page, and move to a larger plan before you run out if you need to.
CautionDon't show your customers the numbers from the Console's usage page. They cover your whole account, across all your users. Count per user yourself.

06Where to go next

Can’t find what you need?

Write to us and a person will answer.