@authoritas-ace/ace-sdk
v2.0.0
Published
TypeScript client for the ACE (Agentic Commerce Engine) v1 API: contextual enrichment, durable jobs, projects, experiments, webhooks
Maintainers
Readme
ACE SDK
The official JavaScript and TypeScript client for ACE (Agentic Commerce Engine). Use it in Node.js apps, scripts, or serverless functions to run the Contextual Enrichment Engine, track durable jobs, and manage your projects, experiments and webhooks.
New to ACE? Start at ace.authoritas.com, or read the SDK guide for the long-form version of this page.
Install
npm install @authoritas-ace/ace-sdkNode 18 or newer. ESM only.
Create a client
import { createClient } from "@authoritas-ace/ace-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. health() is the only method that works without one.
How results are shaped
Every method resolves to an ApiResult<T>. Nothing throws on an API error, so 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 } };
}Check error yourself, or use assertOk to turn a failure into a thrown AceApiError:
import { assertOk, AceApiError } from "@authoritas-ace/ace-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 AceApiError) 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 contextual rules for a product set.
const rules = await ace.enrichment.rules({
source: {
type: "inline",
products: [{ id: "sku-1", title: "Merino base layer", category: "Outdoor" }],
},
creativityLevel: 3,
});
// 2. Generate content grounded in those rules.
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: rules.data,
});
// 3. Or run both steps in one call.
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.
Write methods take 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
A large content or pipeline request runs asynchronously and 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);
const finished = await ace.jobs.wait(started.data.id, { pollMs: 2000, timeoutMs: 300_000 });
const results = await ace.jobs.results(started.data.id, { page: 1, pageSize: 50 });| Method | What it does |
|--------|--------------|
| ace.jobs.list({ kind, status, storeId, page, pageSize }) | List jobs, newest first |
| ace.jobs.get(id) | One job with its status and result |
| ace.jobs.results(id, { page, pageSize }) | Paginated per-product results |
| ace.jobs.cancel(id) | Cancel a queued or running job |
| ace.jobs.wait(id, { pollMs, timeoutMs }) | Poll until terminal. Returns a TIMEOUT error if the cap elapses |
Projects
await ace.projects.list(); // scoped keys see only their project
await ace.projects.get(id);
await ace.projects.create({ name: "Autumn catalogue" }); // integration_type defaults to "feeds"
await ace.projects.update(id, { description: "Q4 push" });
await ace.projects.remove(id);Experiments
Every experiments call is project-scoped, so projectId is required throughout. On create it travels in the body; everywhere else it is a scope argument.
await ace.experiments.list({ projectId });
await ace.experiments.get(id, { projectId });
await ace.experiments.create({ projectId, name: "PDP copy A/B" });
await ace.experiments.update(id, { status: "running" }, { projectId });
await ace.experiments.remove(id, { projectId });Webhooks
const hook = await ace.webhooks.create({
url: "https://example.com/hooks/ace", // https is required
events: ["enrichment.job.succeeded", "enrichment.job.failed"],
});
await ace.webhooks.list();
await ace.webhooks.update(id, { is_active: false });
await ace.webhooks.remove(id);
// Delivery log. Page counts arrive in result.meta.pagination.
const log = await ace.webhooks.deliveries(id, { status: "failed", pageSize: 50 });Each subscription carries a secret. Verify the signature header on your endpoint against it before trusting a payload.
Utilities, usage and feeds
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
await ace.usage(); // credits, rate limit, job counts, test quota
await ace.feeds.list({ projectId });
await ace.feeds.get(feedId);testQuota is present only for ace_test_ keys.
Escape hatch
Any endpoint without a typed wrapper is still reachable, with auth and error handling applied:
await ace.get("/api/v1/openapi");
await ace.post("/api/v1/some/endpoint", body);
await ace.put("/api/v1/some/endpoint/id", body);
await ace.del("/api/v1/some/endpoint/id");Also available
- ACE CLI drives the same API from your terminal.
- The MCP server lets an AI assistant call ACE for you.
License
MIT
