Build on Shruwd

TypeScript SDK

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.

bash
npm install @shruwd/sdk

Getting started

ts
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

OptionDefault
apiKeyRequired
baseUrlhttps://shruwd.io/api/v1Point at a preview deployment
fetchglobal fetchSupply your own
maxRetries3See below
timeoutMsPer request
clientYour 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

NamespaceMethods
workspaceget()
brandslist() · create() · get() · update() · archive()
promptslist() · add() · update() · remove()
entitieslist() · add() · set() · remove()
cycleslist() · run()
visibilityget() · series()
crawlersget()
findingslist() · get() · transition()
suggestionslist() · accept() · dismiss()
connectionscreateIngestToken()
setupget() · suggest()
answerslist()

List calls unwrap their envelope, so brands.list() returns an array rather than { brands: [...] }.

Reading a metric

ts
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.

ts
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
statusHTTP status
codeThe stable code. Branch on this
messageHuman-readable
retryableWhether retrying could succeed
detailsStructured context, when there is any
retryAfterSecondsSet 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:

ResponseRetried
429Yes, 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 errorsOnly on GET, PUT and DELETE
4xx other than 429Never

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