@microsoft/webiq
v0.1.6
Published
Web IQ API TypeScript SDK
Readme
Web IQ TypeScript SDK
Official TypeScript SDK for Web IQ APIs.
Installation
npm install @microsoft/webiqQuick 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 typesEnum 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
