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

@outboundiq/client

v0.2.0

Published

Official TypeScript SDK for the outboundIQ platform: ANI assignment, dial ingestion, custom dialer integration, NRM, live feed, and webhook verification.

Readme

@outboundiq/client

The official TypeScript SDK for the outboundIQ platform.

npm version CI

  • Typed coverage of the platform APIs: Assignment, Dials, Custom Dialer Integration, ANI Planner, NRM, and Live Feed
  • Webhook signature verification with typed dial.batch payloads
  • Zero runtime dependencies
  • Works in Node 20+, Cloudflare Workers, Deno, and Bun
  • Automatic retries with exponential backoff, safe by default

The API reference remains the source of truth. Request and response fields in this SDK mirror the HTTP API exactly, so anything you read in the docs maps 1:1 to the types here.

Install

npm install @outboundiq/client

Quick start

import { outboundiq } from "@outboundiq/client";

const oiq = outboundiq({ apiKey: process.env.OUTBOUNDIQ_API_KEY });

const result = await oiq.assignment.next({
  prospect_phone: "5559876543",
  prospect_zip: "90210",
});

if (result.success) {
  console.log(`Dial from ${result.ani}`);
}

Authentication

Every request needs a universal API key, generated in the outboundIQ workspace dashboard. Pass it explicitly or set the OUTBOUNDIQ_API_KEY environment variable:

const oiq = outboundiq({ apiKey: "oiq_..." });
// or, with OUTBOUNDIQ_API_KEY set in the environment:
const oiq = outboundiq();

Keep the key server-side. Do not ship it in browser bundles.

Assignment API

Get the next ANI to dial from, one prospect at a time or in batch.

// Single prospect
const next = await oiq.assignment.next({
  prospect_phone: "5559876543",
  prospect_zip: "90210",
  dialer_campaign: "q3-outbound",
  e164: true,
});

// Batch, one call for a whole lead list
const batch = await oiq.assignment.batch({
  leads: [
    { row_id: "lead-1", prospect_phone: "5559876543", prospect_zip: "90210" },
    { row_id: "lead-2", prospect_phone: "5551112222" },
  ],
});

for (const row of batch.results) {
  if (row.error) console.warn(`${row.row_id}: ${row.error}`);
  else console.log(`${row.row_id} -> ${row.outboundani}`);
}

Dials API

Post dial records for processing and enrichment.

await oiq.dials.create({
  campaign_id: "abc-123",
  campaign_name: "Q2 Outbound Push",
  agent_name: "Jane Doe",
  from_number: "5551234567",
  to_number: "5559876543",
  disposition_name: "Sale",
  datetime: "2026-04-10 14:32:15",
  call_direction: "Outbound",
  zip: "90210",
  sys_created_date_original: "2026-04-01",
  total_dial_attempts: 3,
  skill_name: "Sales Tier 1",
  lead_source: "facebook-ads",
  dial_id: "550e8400-e29b-41d4-a716-446655440000",
});

For outbound calls from_number is the caller ID and to_number is the prospect. For inbound calls the two are reversed.

Custom Dialer Integration API

Sync campaigns, dispositions, and ANIs from any dialer into outboundIQ.

await oiq.custom.campaigns.create({
  id: "c-1",
  name: "My Campaign",
  type: "Outbound",
});
await oiq.custom.campaigns.update({ id: "c-1", name: "Renamed" });
await oiq.custom.campaigns.get("c-1");
await oiq.custom.campaigns.delete("c-1");

await oiq.custom.dispos.create({
  id: "d-1",
  name: "Sale",
  type: "Agent",
  contact: true,
  success: true,
});

await oiq.custom.anis.create({
  country_code: "+1",
  number: "5551234567",
  inbound_group_id: "ig-1",
});
const { ani } = await oiq.custom.anis.get("5551234567");

ANI Planner API

Analyze recent dial volume against your current ANI inventory and get a recommended number of ANIs per region.

const plan = await oiq.aniPlanner.generate({
  dateStart: "2026-05-01",
  dateEnd: "2026-05-28",
  dailyDialsTarget: "BETTER", // 50 (BEST), 75 (BETTER), or 100 (GOOD) dials per ANI per day
  groupBy: "area_code",
  inventoryMode: "managed",
  campaigns: ["west-coast-outbound"],
});

if (plan.success) {
  const { regionStats, totalCurrentAnis, totalProposedAnis } = plan.data;
  console.log(`${totalCurrentAnis} ANIs today, ${totalProposedAnis} recommended`);

  for (const region of regionStats) {
    if (region.difference > 0) {
      console.log(`${region.region}, ${region.state}: add ${region.difference}`);
    }
  }
}

Every field is optional; calling generate() with no arguments analyzes the start of the current month through yesterday, across every campaign. The date range defaults, the resolved numeric dailyDialsTarget, and the effective range are all echoed back in data.

Regions averaging fewer than 20 dials per business day come back with belowThreshold: true, are recommended 0 ANIs, and are excluded from totalProposedAnis — so any numbers already provisioned there surface as surplus in aniDifference.

The shape of regionStats follows the company's country, which is detected from the dialer. US and CA group into state/region rows with toll-free reported as a state: "TF" row; UK groups per area code and returns a non-zero tollFreeRecommendation plus a "Mobile" row for 07x volume.

NRM API

Number reputation management: inventory, remediation, pausing, and activation.

// Page through the ANI inventory with reputation stats
const inventory = await oiq.nrm.listAnis({ page: 1, page_size: 500 });
for (const ani of inventory.data) {
  console.log(ani.phone, ani.statusLabel, ani.blockRate);
}

// Request remediation for a flagged number
await oiq.nrm.remediate({ ani: "5551234567", carrier: "130077", note: "flagged as spam" });

// Rest a number, then bring it back
await oiq.nrm.pause({ ani: "5551234567" });
await oiq.nrm.activate({ ani: "5551234567", date: "2026-09-01" });

Carrier IDs for remediate and pause are listed in the carriers reference.

Live Feed

Push leads into a running dialer campaign in real time. RingCX is supported today.

await oiq.liveFeed.ringcx.upload({
  campaignId: "rc-1",
  lead: { leadPhone: "5559876543", firstName: "Ada", zip: "90210" },
  options: { dialPriority: "IMMEDIATE", duplicateHandling: "REMOVE_FROM_LIST" },
});

Webhooks

outboundIQ signs every webhook delivery with HMAC-SHA256. Always verify against the raw request body, before any JSON parsing.

import { constructWebhookEvent, WEBHOOK_SIGNATURE_HEADER } from "@outboundiq/client";

Node with Express:

import express from "express";

const app = express();

app.post(
  "/webhooks/outboundiq",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      const event = await constructWebhookEvent(
        req.body,
        req.get(WEBHOOK_SIGNATURE_HEADER) ?? "",
        process.env.OUTBOUNDIQ_WEBHOOK_SECRET!,
      );
      for (const dial of event.dials) {
        console.log(dial.campaign, dial.disposition, dial.success);
      }
      res.sendStatus(200);
    } catch {
      res.sendStatus(400);
    }
  },
);

Cloudflare Workers:

import { constructWebhookEvent } from "@outboundiq/client";

export default {
  async fetch(request: Request, env: { OUTBOUNDIQ_WEBHOOK_SECRET: string }) {
    const body = await request.text();
    const signature = request.headers.get("x-outboundiq-signature") ?? "";
    try {
      const event = await constructWebhookEvent(body, signature, env.OUTBOUNDIQ_WEBHOOK_SECRET);
      // handle event.dials
      return new Response("ok");
    } catch {
      return new Response("bad signature", { status: 400 });
    }
  },
};

Notes:

  • Deliveries are batched: each dial.batch payload carries 1 to 100 dial events.
  • Use the x-outboundiq-delivery-id header as an idempotency key. Failed deliveries are retried once; reconcile longer gaps via the Dials API.
  • verifyWebhookSignature(payload, signature, secret) is also exported if you only want the boolean check.

Error handling

All errors extend OutboundIQError. Non-2xx responses carry status and the parsed body.

import { AuthenticationError, RateLimitError, OutboundIQError } from "@outboundiq/client";

try {
  await oiq.dials.create(dial);
} catch (error) {
  if (error instanceof AuthenticationError) {
    // 401 or 403: bad, inactive, or wrong-type API key
  } else if (error instanceof RateLimitError) {
    console.log(`retry after ${error.retryAfter ?? "?"}s`);
  } else if (error instanceof OutboundIQError) {
    console.error(error.status, error.body);
  }
}

| Error | Meaning | | --- | --- | | AuthenticationError | 401 or 403. Check that the key is valid, active, and of type universal. | | RateLimitError | 429, after retries were exhausted. retryAfter is set when the API sent it. | | APIError | Any other non-2xx response. | | ConnectionError | Network failure or timeout. No response was received. | | WebhookVerificationError | A webhook signature did not match the payload. |

Retries and timeouts

The client retries with exponential backoff and jitter, up to maxRetries times (default 2):

  • GET requests retry on network errors, 429, and 5xx responses.
  • POST, PUT, and DELETE retry only on 429, where the request is known not to have been processed. This avoids duplicating writes like dial records when the outcome of a failed request is unknown.

Set maxRetries: 0 to disable retries. Every request times out after timeoutMs (default 30000) and surfaces as a ConnectionError.

Configuration

const oiq = outboundiq({
  apiKey: "oiq_...",        // or OUTBOUNDIQ_API_KEY env var
  baseUrl: "https://api.outboundiq.cloud",
  timeoutMs: 30_000,
  maxRetries: 2,
  fetch: customFetch,        // bring your own fetch if you need to
});

Requirements

Node 20 or newer, or any runtime with the fetch and Web Crypto APIs (Cloudflare Workers, Deno, Bun).

License

MIT