Skip to main content
Rezolve Ai

Rezolve Ai SDK

The official JavaScript and TypeScript client for the Rezolve Ai v1 API.

The Rezolve Ai SDK is the official JavaScript and TypeScript client for Rezolve Ai. Use it in Node.js apps, scripts, or serverless functions to run the Contextual Enrichment Engine, manage durable jobs, and read your account usage.

Who this is for

Developers calling Rezolve Ai from their own code. If you would rather drive Rezolve Ai from a terminal, see the Rezolve Ai CLI. If you want an AI assistant to call Rezolve Ai for you, see the MCP server.

Install

npm install @rezolve-ai/sdk

Renamed from @authoritas-ace/ace-sdk. The old package still resolves but is no longer updated. The exported types moved from the Ace prefix to RezolveAi, so AceClient is now RezolveAiClient.

Create a client

import { createClient } from "@rezolve-ai/sdk";

const ace = createClient({
  baseUrl: "https://ace.authoritas.com",
  apiKey: process.env.ACE_API_KEY, // ace_live_... or ace_test_...
});

Create a key under Settings, Developer console, Keys. See API Keys for scopes and environments.

Your first call

health() needs no key, so it is the quickest way to confirm your base URL is right.

const res = await ace.health();
console.log(res.status, res.data); // 200 { status: "ok", service: "enrichment-api", version: "v1" }

How results are shaped

Every method resolves to an ApiResult<T>. Nothing throws on an API error: you get error instead of data, plus the HTTP status.

interface ApiResult<T> {
  data?: T;
  error?: { code: string; message: string; details?: unknown };
  status: number;
  meta?: { pagination?: { page: number; pageSize: number; total: number; totalPages: number } };
}

meta is set when the API sends more than the data itself: pagination for a list, and for job results the run's totals.

Check error yourself, or use assertOk to turn a failure into a thrown RezolveAiApiError:

import { assertOk, RezolveAiApiError } from "@rezolve-ai/sdk";

try {
  const res = await ace.usage();
  assertOk(res);
  console.log(res.data.credits.balance); // res.data is defined past this point
} catch (err) {
  if (err instanceof RezolveAiApiError) {
    console.error(err.code, err.status, err.message);
  }
}

Contextual Enrichment Engine

Three methods, matching the three engine endpoints. Each takes a source, which is either inline products or a connected store.

// 1. Generate the eight section contextual rules for a product set.
const rules = await ace.enrichment.rules({
  source: {
    type: "inline",
    products: [
      { id: "sku-1", title: "Merino base layer", description: "...", category: "Outdoor" },
    ],
  },
  creativityLevel: 3,
});

// 2. Generate content grounded in those rules. The rules live under `rules`
//    in the response, keyed by product category.
const { rules: categoryRules } = rules.data as {
  rules: Record<string, Record<string, unknown>>;
};
const content = await ace.enrichment.content({
  source: { type: "inline", products: [{ id: "sku-1", title: "Merino base layer" }] },
  contentTypes: ["product-description", "meta-tags", "jsonld-schema"],
  rules: categoryRules,
});

// 3. Or run both steps in one call. This always returns a job.
const pipeline = await ace.enrichment.pipeline({
  source: { type: "inline", products: [{ id: "sku-1", title: "Merino base layer" }] },
  contentTypes: ["product-description", "meta-tags"],
});

A product needs only id and title. Extra keys you pass through (brand, price, tags, attributes) become grounding signal.

rules answers inline for up to 5 products, and content for up to 5 products and 10 units (products multiplied by content types). Past that they return a job envelope instead of the result, so check which you got, or pass mode: "sync" or mode: "async" to decide yourself. content does not derive rules: leave rules out and the copy is written from the product data alone.

Look products up first

content and pipeline take lookup. Each product is looked up by its gtin, isbn, asin, mpn with brand, or url before its content is written, and what a published source states about it is used alongside your own data.

const content = await ace.enrichment.content({
  source: { type: "inline", products: [{ id: "sku-1", title: "Fineliner pen, azure", gtin: "4006381333931" }] },
  contentTypes: ["product-description"],
  lookup: true, // free sources. Use { sources: ["open_data", "web_search"] } to also search the web.
});

Product lookup

Find what published sources say about a product from whatever you know about it. Every fact comes back with the source that states it. See Product lookup for the full picture.

import { isJobEnvelope } from "@rezolve-ai/sdk";

// One product. `q` works out what kind of value it is.
const found = await ace.enrichment.lookup({ q: "4006381333931", market: "GB" });
assertOk(found);

if (!isJobEnvelope(found.data) && "items" in found.data && !("dryRun" in found.data)) {
  const [item] = found.data.items;
  console.log(item.match?.status, item.product?.title);
  for (const fact of item.facts ?? []) console.log(fact.field, fact.value, fact.url);
}

A small request answers inline. A large batch returns a job, which you wait for like any other:

const batch = await ace.enrichment.lookup(
  { items: rows.map((row) => ({ ref: row.id, identifiers: { gtin: row.barcode } })) },
  { idempotencyKey: "catalogue-2026-10-09" },
);
assertOk(batch);

if (isJobEnvelope(batch.data)) {
  await ace.jobs.wait(batch.data.id, { timeoutMs: 30 * 60_000 });
  const page = await ace.jobs.results(batch.data.id, { pageSize: 200 });
  console.log(page.meta?.summary); // matched, ambiguous, notFound, searchCalls, cacheHits
}
MethodWhat it does
ace.enrichment.lookup(body, { idempotencyKey })One product or a batch of up to 500. Add enrich to also write content, or dryRun: true to validate and estimate without looking anything up.
ace.enrichment.lookupOne({ gtin, asin, mpn, brand, ... })One product, free. Reads the free sources and anything already found on the web. Never runs a search.
ace.enrichment.lookupSources({ projectId })Which sources this key can use, with the reason for any it cannot.

Only a web search is charged. Leave it out with excludeSources: ["web_search"], or use lookupOne, for a lookup that costs nothing.

Idempotency

Write methods accept an idempotency key, so a retried request reuses the original job instead of starting a second one.

await ace.enrichment.pipeline(body, { idempotencyKey: "nightly-2026-07-29" });

Jobs

pipeline always runs as a job, and a large rules or content request does too. Each returns a job envelope. Poll it, or let the SDK poll for you.

const started = await ace.enrichment.pipeline({ source, contentTypes: ["product-description"] });
assertOk(started);

// Poll until the job reaches a terminal status (default: every 2s, 5 minute cap).
const finished = await ace.jobs.wait(started.data.id, { pollMs: 2000, timeoutMs: 300_000 });
assertOk(finished);

// Then page through the per-product output.
const results = await ace.jobs.results(started.data.id, { page: 1, pageSize: 50 });

The full set:

MethodWhat it does
ace.jobs.list({ kind, status, storeId, page, pageSize })List jobs, newest first.
ace.jobs.get(id)Fetch one job with its status and result.
ace.jobs.results(id, { page, pageSize })Paginated per-product results.
ace.jobs.cancel(id)Cancel a pending or running job.
ace.jobs.wait(id, { pollMs, timeoutMs })Poll until terminal. Returns a TIMEOUT error if the cap elapses.

Utilities

Fast helpers. Language detection is unbilled and the scorers are pure functions, so none of these consume credits.

await ace.utils.language({ products });          // dominant locale + confidence
await ace.utils.agenticReadiness({ products });  // per-product readiness + summary
await ace.utils.reviewQuality({ items });        // weighted overall score per item

Usage and feeds

const usage = await ace.usage();
// { env, credits: { balance, limit, periodStart, periodEnd },
//   rateLimit: { limitPerMinute }, jobs: { total }, testQuota? }

await ace.feeds.list({ projectId });
await ace.feeds.get(feedId, { projectId });

testQuota is present only for ace_test_ keys. Both feed calls need the feeds:read scope, and projectId unless the key is pinned to a project. See Project scope.

Escape hatch

Any GET or POST endpoint without a typed wrapper is still reachable, with auth and error handling applied:

await ace.get("/api/v1/webhooks");
await ace.post("/api/v1/webhooks", {
  url: "https://example.com/hook",
  events: ["enrichment.job.succeeded"],
});

Next steps

  • API Reference for the full endpoint list with request and response schemas
  • Contextual Enrichment Engine for what the engine produces and how grounding works
  • Webhooks to be notified when a job finishes instead of polling
  • Feeds API for creating feeds, pushing products, and exporting channel files

On this page