Integration examples
Copy a complete TypeScript or Python workflow, or compose a pipeline with curl.
Choose an endpoint
Both examples use the REST API from a server or terminal. Supply a customer key with agents:read, query:write and query:read through your environment. List endpoints, select an id from the agents array, and set RECURSIFT_AGENT_ID to that value.
curl --fail-with-body "https://api.recursift.app/v1/agents?limit=20" \
-H "Authorization: Bearer $RECURSIFT_API_KEY"TypeScript: submit and poll
Use Node.js 22 or newer. Save the example as query_endpoint.ts. It uses built-in fetch, typed receipts and findings, bounded rate-limit retries, and a five-second polling interval. It reports partial results and unanswered endpoints explicitly.
// TypeScript + Node.js 22 or newer. Run server-side, never in a browser.
import { randomUUID } from "node:crypto";
import { setTimeout as sleep } from "node:timers/promises";
const BASE = "https://api.recursift.app/v1";
type Receipt = {
id: string;
status: "running" | "complete" | "partial" | "timed_out" | "cancelled";
agent_total: number;
deadline: string;
};
type Job = Receipt & {
findings: Array<{
agent_id: string;
status: string;
answer: string | null;
error: string | null;
answered_at: string | null;
}>;
unanswered_agents: string[];
};
async function main() {
const key = process.env.RECURSIFT_API_KEY;
const agent = process.env.RECURSIFT_AGENT_ID;
if (!key || !agent) {
throw new Error("Set RECURSIFT_API_KEY and RECURSIFT_AGENT_ID first");
}
async function call<T>(path: string, payload?: unknown, requestId?: string): Promise<T> {
for (let attempt = 0; attempt < 3; attempt++) {
const response = await fetch(BASE + path, {
method: payload === undefined ? "GET" : "POST",
headers: {
Authorization: "Bearer " + key,
...(payload === undefined ? {} : { "Content-Type": "application/json" }),
...(requestId ? { "Idempotency-Key": requestId } : {}),
},
body: payload === undefined ? undefined : JSON.stringify(payload),
signal: AbortSignal.timeout(20_000),
redirect: "error",
});
if (response.status === 429 && attempt < 2) {
const seconds = Number(response.headers.get("Retry-After") ?? "5");
await response.body?.cancel();
await sleep((Number.isFinite(seconds) ? Math.min(60, Math.max(5, seconds)) : 5) * 1_000);
continue;
}
if (!response.ok) {
await response.body?.cancel();
throw new Error("Recursift returned HTTP " + response.status);
}
return await response.json() as T;
}
throw new Error("Retry limit reached");
}
// Keep this ID unchanged if the submission is retried.
const requestId = randomUUID();
const receipt = await call<Receipt>("/queries", {
question: "What operating system are you running?",
target: { agent_ids: [agent] },
timeout_seconds: 120,
}, requestId);
console.log("Query:", receipt.id);
const end = performance.now() + 150_000;
while (performance.now() < end) {
const job = await call<Job>("/queries/" + encodeURIComponent(receipt.id));
if (["complete", "partial", "timed_out", "cancelled"].includes(job.status)) {
for (const finding of job.findings) {
console.log(finding.agent_id, finding.status, finding.answer ?? finding.error);
}
console.log("Status:", job.status, "Unanswered:", job.unanswered_agents);
return;
}
await sleep(5_000);
}
throw new Error("Stopped polling; retain the query ID to inspect it later");
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : "Query failed");
process.exitCode = 1;
});npm install --save-dev typescript @types/node
npx tsc query_endpoint.ts --target ES2022 --module nodenext --strict --skipLibCheck --outDir dist
node dist/query_endpoint.jsPython: submit and poll
Set RECURSIFT_API_KEY and RECURSIFT_AGENT_ID in your environment. This standard-library example submits one question, uses a stable request key for rate-limit retries, polls for a bounded period, and keeps endpoint attribution and failures visible.
# Python 3, standard library only. Run server-side or from your terminal.
import json
import os
import time
import urllib.error
import urllib.request
import uuid
BASE = "https://api.recursift.app/v1"
KEY = os.environ["RECURSIFT_API_KEY"]
AGENT = os.environ["RECURSIFT_AGENT_ID"]
request_id = str(uuid.uuid4())
def call(path, payload=None, request_id=None):
headers = {"Authorization": "Bearer " + KEY}
if request_id:
headers["Idempotency-Key"] = request_id
data = None
if payload is not None:
data = json.dumps(payload).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(BASE + path, data=data, headers=headers)
# Bound retries; retain the same idempotency key for POST retries.
for attempt in range(3):
try:
with urllib.request.urlopen(req, timeout=20) as response:
return json.load(response)
except urllib.error.HTTPError as error:
if error.code != 429 or attempt == 2:
raise
time.sleep(min(60, max(5, int(error.headers.get("Retry-After", "5")))))
job = call("/queries", {
"question": "What operating system are you running?",
"target": {"agent_ids": [AGENT]},
"timeout_seconds": 120,
}, request_id)
print("Query:", job["id"])
end = time.monotonic() + 150
while time.monotonic() < end:
job = call("/queries/" + job["id"])
if job["status"] in {"complete", "partial", "timed_out", "cancelled"}:
for finding in job["findings"]:
print(finding["agent_id"], finding["status"],
finding["answer"] or finding["error"])
print("Status:", job["status"], "Unanswered:", job["unanswered_agents"])
break
time.sleep(5)
else:
raise TimeoutError("Stopped polling; retain the query ID to inspect it later")python3 query_endpoint.pyFit it into your existing stack
- Inventory: page through /agents and store customer-scoped endpoint IDs in your integration.
- Investigation: submit a bounded /queries job with explicit targets, then poll its ID.
- Enrichment: attach each finding’s agent_id, answered_at, query_id and status to the corresponding investigation record.
- Stored lookup: use /findings for earlier answer summaries before issuing another query.
Use a separate secret and separate stored results per customer. Do not present unanswered endpoints as clean. Webhooks, official language SDK packages and cross-customer bulk jobs are not available yet.