npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@microsoft/webiq

v0.1.6

Published

Web IQ API TypeScript SDK

Readme

Web IQ TypeScript SDK

Official TypeScript SDK for Web IQ APIs.

Node.js 22+ License: MIT

Installation

npm install @microsoft/webiq

Quick Start

import { WebIQClient, BrowseContentFormat, ContentFormat } from '@microsoft/webiq';

const client = new WebIQClient({
  apiKey: process.env.WEBIQ_API_KEY,
});

// Web search with content format
const web = await client.web.search('TypeScript programming', {
  maxResults: 5,
  contentFormat: ContentFormat.HTML, // Use enum, not the string "html"
});
for (const result of web.webResults ?? []) {
  console.log(`${result.title}: ${result.url}`);
}

// News search
const news = await client.news.search('technology', { maxResults: 5 });
for (const item of news.newsResults ?? []) {
  console.log(`${item.title} - ${item.source}`);
}

// Video search
const videos = await client.videos.search('machine learning tutorial');
for (const video of videos.videoResults ?? []) {
  console.log(`${video.title} (${video.length})`);
}

// Browse a URL in markdown format
const page = await client.browse.fetch('https://www.microsoft.com', {
  contentFormat: BrowseContentFormat.MARKDOWN,
});
console.log(page.content);

// Classic search with multiple answer types
const classic = await client.classic.search('Artificial intelligence trends', {
  responseFilter: ['webResults'],
});
// responseFilter selects which answer types come back; the matching
// fields (e.g. classic.webResults) are populated dynamically and typed as `any`.
for (const r of classic.webResults ?? []) {
  console.log(`  - ${r.title}: ${r.url}`);
}

await client.close();

Authentication

API Key

Get an API key at webiq.microsoft.ai.

import { WebIQClient } from '@microsoft/webiq';

const client = new WebIQClient({
  apiKey: process.env.WEBIQ_API_KEY,
});

EntraID (Azure AD)

Pass any @azure/identity TokenCredential:

import { DefaultAzureCredential } from '@azure/identity';
import { WebIQClient } from '@microsoft/webiq';

const client = new WebIQClient({
  credential: new DefaultAzureCredential(),
});

API Reference

Web Search

import { ContentFormat, SafeSearch } from '@microsoft/webiq';

const results = await client.web.search(query, {
  maxResults: 10, // 1-50, default 10
  language: 'en', // ISO 639-1 code
  region: 'US', // Country/region code
  location: 'lat:40.7;long:-74.0', // Optional
  contentFormat: ContentFormat.HTML, // PASSAGE, TEXT, HTML, MARKDOWN
  maxLength: 10000, // 1-500000
  safeSearch: SafeSearch.STRICT, // OFF, STRICT
  customSearchConfigId: 'my-config', // Optional custom search config
  includeDomains: ['example.com'], // Optional allowlist (max 250)
  excludeDomains: ['spam.example'], // Optional blocklist (max 250)
});
// results.webResults → [{title, url, content, lastUpdatedAt, crawledAt, language, isAdult, contentTier}]

News Search

const results = await client.news.search(query, {
  maxResults: 10, // 1-20, default 10
  language: 'en',
  region: 'US',
  location: 'lat:40.7;long:-74.0', // Optional
  contentFormat: ContentFormat.TEXT,
  maxLength: 10000,
});
// results.newsResults → [{title, url, content, source, lastUpdatedAt, crawledAt, thumbnail, isAdult}]

Video Search

const results = await client.videos.search(query, {
  maxResults: 30, // 1-30, default 30
  language: 'en',
  region: 'US',
  enablePlaylist: true,
  freshness: 'month', // week, month, year
});
// results.videoResults → [{title, url, length, viewCount, publishedBy, moments, ...}]
// results.playlists → [{title, videos}]

Browse

import { BrowseContentFormat, LiveCrawlMode } from '@microsoft/webiq';

const page = await client.browse.fetch(url, {
  maxLength: 10000, // 1-500000
  liveCrawl: LiveCrawlMode.FALLBACK, // NONE (default), FALLBACK, FORCE
  includeWebLinks: true,
  includeImageLinks: true,
  renderDynamicPages: false,
  contentFormat: BrowseContentFormat.MARKDOWN,
});
// page → {url, title, content, isAdult, retryAfter, traceId, ...}

Image Search

import { ImageAspectRatio, ImageSize, SafeSearch } from '@microsoft/webiq';

const results = await client.images.search(query, {
  maxResults: 30, // 1-30, default 30
  language: 'en',
  region: 'US',
  aspectRatio: ImageAspectRatio.WIDE, // SQUARE, WIDE, TALL
  imageSize: ImageSize.LARGE, // SMALL, MEDIUM, LARGE, EXTRA_LARGE
  safeSearch: SafeSearch.STRICT, // OFF, STRICT
  watermarkFree: true,
});
// results.imageResults → [{title, url, hostPageUrl, caption, width, height, thumbnailUrl, ...}]

Classic Search

Given a query, classic search will search and retrieve content from webpages, images, videos, news, weather, sports, etc.

import { ContentFormat, SafeSearch } from '@microsoft/webiq';

const response = await client.classic.search(query, {
  maxAnswerTypes: 6, // 1-6, default 6
  language: 'en', // ISO 639-1/2 or BCP 47
  region: 'US', // Country/region code
  location: 'lat:40.7;long:-74.0', // Optional
  maxResultsWeb: 10, // 1-50, default 10
  maxLength: 10000, // 1-500000
  contentFormat: ContentFormat.HTML,
  freshness: 'month', // day, week, month, year, or date range
  responseFilter: ['webResults', 'newsResults'], // Answer types to include
  safeSearch: SafeSearch.STRICT, // OFF, STRICT
});
// response.querySignals → {originalQuery, normalizedQuery, isDefensive, isAdult, isNav, isFresh}
// response.traceId → string
// Additional dynamic fields for different answer types

Enum Types

Some parameters require enum values instead of plain strings. Import them from @microsoft/webiq:

import { BrowseContentFormat, ContentFormat } from '@microsoft/webiq';

// ContentFormat: format of returned content for web, news, and classic search
ContentFormat.PASSAGE; // Selected passages only (plain text)
ContentFormat.TEXT; // Full page text (plain text)
ContentFormat.HTML; // HTML format (default for web search)
ContentFormat.MARKDOWN; // Markdown format

// BrowseContentFormat: format for browse (no PASSAGE option)
BrowseContentFormat.TEXT; // Full page text
BrowseContentFormat.HTML; // HTML format (default)
BrowseContentFormat.MARKDOWN; // Markdown format

// Usage in web search
const results = await client.web.search('query', {
  contentFormat: ContentFormat.MARKDOWN,
});

// Usage in browse
const page = await client.browse.fetch('https://www.microsoft.com', {
  contentFormat: BrowseContentFormat.HTML,
});

Configuration

Timeout and Retry

import { WebIQClient, RetryPolicy } from '@microsoft/webiq';

const client = new WebIQClient({
  apiKey: 'your-api-key',
  timeout: 10000, // milliseconds (default: 10000)
  retry: new RetryPolicy({
    maxRetries: 2, // defaults shown
    baseDelaySeconds: 0.25,
    maxDelaySeconds: 4.0,
  }),
});

Cancellation

Every search and browse method accepts an AbortSignal via options.signal. When the signal aborts, the in-flight request is cancelled, retries stop, and the signal's reason is re-thrown unchanged (no APIConnectionError wrapping).

Propagate a client disconnect from an HTTP handler:

import express from 'express';

app.get('/search', async (req, res) => {
  try {
    const result = await client.web.search(req.query.q as string, {
      signal: req.signal, // express/fastify expose the request's AbortSignal
    });
    res.json(result);
  } catch (err) {
    if (err instanceof DOMException && err.name === 'AbortError') return; // client gone
    throw err;
  }
});

Apply a wall-clock budget that includes retries (the timeout option is per attempt only):

const result = await client.web.search('latest news', {
  signal: AbortSignal.timeout(5_000),
});

Combine multiple signals (e.g. a request signal plus a wall-clock cap):

const result = await client.web.search('latest news', {
  signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]),
});

Custom fetch (proxy, TLS, ...)

For advanced HTTP settings — proxy, custom TLS, mTLS, etc. — pass a custom fetchFn that wraps the global fetch. Spread init in your wrapper so headers, body, and the internal AbortSignal are preserved.

import { ProxyAgent } from 'undici';
import { WebIQClient } from '@microsoft/webiq';

const proxyAgent = new ProxyAgent('http://my-proxy:8080');

const client = new WebIQClient({
  apiKey: 'your-api-key',
  // `dispatcher` is a Node/undici-specific init field; the `FetchFunction`
  // type accepts extra fields so no type assertion is needed.
  fetchFn: (url, init) => fetch(url, { ...init, dispatcher: proxyAgent }),
});

Telemetry

import { WebIQClient, TelemetryEvent } from '@microsoft/webiq';

const client = new WebIQClient({
  apiKey: 'your-api-key',
  telemetryHook: (event: TelemetryEvent) => {
    console.log(`${event.method} ${event.path} → ${event.statusCode} (${event.elapsedMs}ms)`);
  },
});

Error Handling

The SDK throws specific error types for different error conditions. All errors extend WebIQError.

Error Hierarchy

| Error Class | HTTP Status | When | | ----------------------- | ---------------------------- | ------------------------------------------------- | | AuthenticationError | 401 | Invalid or missing API key | | PermissionDeniedError | 403 | Authenticated but not authorized for the resource | | RateLimitError | 429, 430 | Rate limit / concurrent-request limit exceeded | | APIStatusError | 400, 404, 500, 503, 504, ... | All other HTTP errors | | APIConnectionError | — | Network issues, DNS failures, timeouts | | WebIQError | — | Base class for all SDK errors |

PermissionDeniedError and RateLimitError both extend APIStatusError, so a broader instanceof APIStatusError check still matches them.

Rate limits are never auto-retried

The SDK does not automatically retry 429 (rate limit) or 430 (concurrent-request limit) responses. As soon as the API returns one, the transport throws RateLimitError so your application can decide what to do — back off, queue the request, surface it to the user, etc. Generic retry settings (RetryPolicy.retryOnStatus, maxRetries) do not apply to rate limits.

The server reports the back-off hint in the response body as the retryAfter field — typically a duration with an s suffix (e.g. "30s", "60s"). The SDK surfaces that value unchanged on error.retryAfter.

import { RateLimitError } from '@microsoft/webiq';

try {
  const results = await client.web.search('test');
} catch (error) {
  if (error instanceof RateLimitError) {
    // error.retryAfter is the server-provided value from the response body
    // (e.g. "60s"). The SDK does not parse or normalize it for you.
    console.warn(`Rate limited. Retry after: ${error.retryAfter}`);
    // Your retry strategy lives here — the SDK will not retry for you.
  }
}

Basic Error Handling

import {
  WebIQError,
  APIConnectionError,
  APIStatusError,
  AuthenticationError,
  PermissionDeniedError,
  RateLimitError,
} from '@microsoft/webiq';

try {
  const results = await client.web.search('test');
} catch (error) {
  if (error instanceof PermissionDeniedError) {
    // 403 — authenticated, but not allowed to call this resource
    console.error(`Forbidden (HTTP ${error.statusCode}):`, error.message);
  } else if (error instanceof AuthenticationError) {
    // 401 — invalid or missing API key
    console.error(`Auth failed (HTTP ${error.statusCode}):`, error.message);
  } else if (error instanceof RateLimitError) {
    // 429 or 430 — rate limit / concurrent-request limit (never auto-retried)
    console.error(`Rate limited. Retry after: ${error.retryAfter}`);
  } else if (error instanceof APIStatusError) {
    // Other HTTP errors (400, 404, 500, 503, 504, etc.)
    console.error(`API error (HTTP ${error.statusCode}):`, error.message);
  } else if (error instanceof APIConnectionError) {
    // Network issues
    console.error('Connection failed:', error.message);
  }
}

Inspecting Error Details

All APIStatusError instances (including PermissionDeniedError and RateLimitError) expose the full error response body. The pattern below applies when the server rejects a request (e.g. a 4xx the SDK couldn't pre-validate, or a transient 5xx). browse.fetch is a convenient way to trigger one — a missing or filtered URL surfaces as a structured error:

import { WebIQClient, APIStatusError } from '@microsoft/webiq';

async function main() {
  const client = new WebIQClient({ apiKey: 'your-api-key' });

  try {
    await client.browse.fetch('https://www.not-microsoft.com');
  } catch (error) {
    if (error instanceof APIStatusError) {
      console.error(`Status: ${error.statusCode}`); // e.g. 404
      console.error(`Message: ${error.message}`); // e.g. "No result is found"

      // Full error body from the API response
      if (error.body && typeof error.body === 'object') {
        const body = error.body as Record<string, unknown>;
        console.error(`Error code: ${body.errorCode}`); // e.g. "BrowseApiDocNotFound"
        console.error(`Category: ${body.errorCategory}`); // e.g. "UserError"
        console.error(`Details: ${body.technicalDetails}`); // e.g. "NotFound"
        console.error(`Trace ID: ${body.traceId}`); // for debugging with support
        console.error(`Retry after: ${body.retryAfter}`); // for retryable errors
      }
    }
  }
}

main().catch(console.error);

Concurrent Requests

const [web, news, videos] = await Promise.all([
  client.web.search('TypeScript'),
  client.news.search('programming'),
  client.videos.search('tutorial'),
]);

License

MIT