A typed, dependency-free TypeScript client for the Shruwd API, with retries and structured errors built in.
@shruwd/sdk is a typed wrapper over the API. ESM, and no
runtime dependencies beyond fetch.
Its types are generated from the OpenAPI document, so the client and the contract cannot drift apart.
npm install @shruwd/sdk
Getting started
import { Shruwd } from '@shruwd/sdk';
const shruwd = new Shruwd({ apiKey: process.env.SHRUWD_API_KEY! });
const brand = await shruwd.brands.create({ name: 'Waitlister', domain: 'waitlister.me' });
// Competitors first: the first measurement starts as soon as prompts exist.
await shruwd.entities.add(brand.brandId, {
name: 'LaunchList',
domains: ['getlaunchlist.com'],
});
await shruwd.prompts.add(brand.brandId, [
{ text: 'best waitlist software for a product launch', intent: 'commercial' },
{ text: 'launchlist alternatives', intent: 'comparison' },
]);
Constructing without a key throws immediately rather than failing on the first request.
Options
| Option | Default | |
|---|---|---|
apiKey | — | Required |
baseUrl | https://shruwd.io/api/v1 | Point at a preview deployment |
fetch | global fetch | Supply your own |
maxRetries | 3 | See below |
timeoutMs | — | Per request |
client | — | Your app’s name and version, e.g. acme-reporter/2.1.0. Sent with the SDK’s own so your usage is attributable. Never a person, an account or a secret |
Methods
| Namespace | Methods |
|---|---|
workspace | get() |
brands | list() · create() · get() · update() · archive() |
prompts | list() · add() · update() · remove() |
entities | list() · add() · set() · remove() |
cycles | list() · run() |
visibility | get() · series() |
crawlers | get() |
findings | list() · get() · transition() |
suggestions | list() · accept() · dismiss() |
connections | createIngestToken() |
setup | get() · suggest() |
answers | list() |
List calls unwrap their envelope, so brands.list() returns an array rather than
{ brands: [...] }.
Reading a metric
const visibility = await shruwd.visibility.get(brand.brandId, { engine: 'google_aio' });
// point, lo and hi are proportions from 0 to 1.
const pct = (x: number) => (x * 100).toFixed(1);
for (const entity of visibility.entities) {
const rate = entity.mentionRate;
if (rate.state === 'ok') {
console.log(`${entity.canonicalName}: ${pct(rate.point)}% (${pct(rate.lo)}–${pct(rate.hi)}%, n=${rate.n})`);
} else {
console.log(`${entity.canonicalName}: not enough data yet`);
}
}
Discriminating on state is not optional politeness — there is no point to read when
the state is insufficient_data, and treating it as 0 is the single most common way to
misreport these numbers.
Errors
Every failure throws a ShruwdError.
import { Shruwd, ShruwdError } from '@shruwd/sdk';
try {
await shruwd.entities.add(brandId, { name: 'Arc' });
} catch (error) {
if (error instanceof ShruwdError && error.code === 'context_terms_required') {
await shruwd.entities.add(brandId, { name: 'Arc', contextTerms: ['browser', 'The Browser Company'] });
} else {
throw error;
}
}
| Property | |
|---|---|
status | HTTP status |
code | The stable code. Branch on this |
message | Human-readable |
retryable | Whether retrying could succeed |
details | Structured context, when there is any |
retryAfterSeconds | Set on a 429 |
A failure with no JSON body becomes http_<status>, so you always get a code to branch on.
Retries
Handled for you, with backoff:
| Response | Retried |
|---|---|
429 | Yes, on every method, honouring Retry-After — unless it asks for more than 30 seconds, which is thrown at once for you to schedule |
5xx and network errors | Only on GET, PUT and DELETE |
4xx other than 429 | Never |
POST is not retried on 5xx because it is not idempotent — a brand or a batch of prompts
might have been created before the failure. Handle those yourself if you need to.
Up to maxRetries attempts, then the error is thrown.
Next steps
- API overview — the underlying contract
- MCP server — the same client as agent tools

