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

@infloapi/node

v0.4.4

Published

Official Node.js SDK for the Inflo third-party API

Readme

@infloapi/node

Official Node.js SDK for the Inflo third-party API — combined user + group search, user discovery, identifier resolution, notifications, organization members, invitations, connections, and webhook signature verification, with typed responses and built-in auth/retry.

Node >= 18 (uses the built-in global fetch). For older Node versions, pass a fetch implementation via the fetch option.

Install

npm install @infloapi/node
# or
pnpm add @infloapi/node
# or
yarn add @infloapi/node

Quick start

import { InfloClient } from "@infloapi/node";

// OAuth2 client credentials (token is fetched and refreshed automatically):
const inflo = new InfloClient({
  clientId: process.env.INFLO_CLIENT_ID!,
  clientSecret: process.env.INFLO_CLIENT_SECRET!,
  scope: "users:search notifications:write", // optional, this is the default
});

// …or a Personal Access Token (PAT):
const inflo = new InfloClient({ pat: process.env.INFLO_PAT! });

// Combined search — returns both users and groups in one call:
// Pass userToken when the caller is acting on behalf of a specific Inflo user
// (e.g. an OIDC access token received after login). Omit for M2M server calls.
const accessToken = "oidc-access-token-from-your-sso-callback"; // from your auth flow
const { users, groups } = await inflo.search(
  { q: "jane" },
  { userToken: accessToken },
);
console.log(users[0].display_name, groups[0]?.name);

Configuration

new InfloClient({
  // exactly one of:
  pat: "infpat_…",
  // or:
  clientId: "…",
  clientSecret: "…",
  scope: "users:search notifications:write",

  baseUrl: "https://infloapp.com",   // override for staging/tests
  timeout: 30_000,                   // per-request timeout, ms
  retry: {
    maxRetries: 3,                   // 5xx/429/network errors only
    baseDelayMs: 300,
    maxDelayMs: 8_000,
  },
  defaultHeaders: { "X-My-Trace": "abc" },
  fetch,                             // optional custom fetch
});

Methods

search(params, opts) — combined users + groups

Search for Inflo users and groups in one call. Requires the signed-in user's OIDC access token. The server rejects requests without a delegated JWT with 403 app_context_required.

Users are scoped to those who have already authenticated with your app (presence gate). When Inflo staff enable Global Search for your app, all active Inflo users matching the query are returned. Groups are always searched across all public groups.

const { users, groups, total_users, has_more } = await inflo.search(
  { q: "jane", limit: 20, offset: 0 },
  { userToken: oidcAccessToken },
);
// users: [{ uid, display_name, username, avatar_url }]
// groups: [{ id, name, description, avatar_url }]

Each user's uid can be passed directly to inflo.invitations.send({ uid }).

users.search(params, opts)

Search for users only (no groups). Same auth and presence-gate rules as inflo.search().

const { users, total, has_more } = await inflo.users.search(
  { q: "jane", limit: 20, offset: 0 },
  { userToken: oidcAccessToken },
);

users.get(sub)

const user = await inflo.users.get("usr_abc123");

users.identifiers.resolve({ identifier })

const r = await inflo.users.identifiers.resolve({ identifier: "[email protected]" });
if (r.exists) console.log(r.user?.infloUserKey);

users.identifiers.lookup({ type, value })

const r = await inflo.users.identifiers.lookup({ type: "phone", value: "+15551234567" });
if (r.found) console.log(r.infloUserKey);

notifications.create({ sub, title, body, deepLink? })

const res = await inflo.notifications.create({
  sub: "usr_abc123",
  title: "Invoice ready",
  body: "Your March invoice is available.",
  deepLink: "https://yourapp.com/inv/123",
});

if (!res.delivered) {
  // Recipient has not opted in (API returned 202 with no body).
}

organizations.members.list(orgId)

const { members } = await inflo.organizations.members.list("org_42");

invitations.send(body, opts?)

Send a single invitation. At least one of uid, email, or phone is required.

The server may respond with one of two outcomes — check result.outcome:

  • "invited" — an invitation was created and the recipient will be notified.
  • "connected" — the target was already on Inflo and a direct connection was established (no email sent).
// By Inflo UID — no email address needed (recommended)
const result = await inflo.invitations.send({ uid: "uid_abc123", message: "Come join us!" });

if (result.outcome === "invited") {
  console.log("Invitation id:", result.invitation.id);
  console.log("Email status:", result.invitation.delivery.email.status);
} else {
  // result.outcome === "connected"
  console.log("Connected directly:", result.user.uid, "state:", result.state);
}

// By email
const result = await inflo.invitations.send({
  email: "[email protected]",
  expires_in_days: 7,
  notify: { sms: false },
});

// SSO-delegated (send on behalf of a signed-in user)
const result = await inflo.invitations.send(
  { email: "[email protected]" },
  { userToken: oidcAccessToken },
);

End-to-end: search → send invitation by UID → handle webhook

// 1. Combined search — returns users AND groups in one call
//    Response: { users, groups, total_users, has_more }
const { users } = await inflo.search(
  { q: "jane", limit: 20 },
  { userToken: oidcAccessToken },
);
const target = users[0];

// 2. Send by UID — no email lookup required
const result = await inflo.invitations.send({
  uid: target.uid,
  message: "Join my workspace on AwesomeApp!",
  redirect_uri: "https://yourapp.com/welcome",
});

if (result.outcome === "invited") {
  console.log("Invitation sent:", result.invitation.id);
} else {
  // outcome === "connected" — target was already on Inflo, connected directly
  console.log("Connected:", result.user.uid);
}
// 3. When the recipient accepts, Inflo fires an `invitation.accepted` webhook
//    and the connection goes live — use verifyWebhookSignature to validate it.

invitations.list(opts?)

// Default: returns both sent and received, page 1, limit 20
const { invitations, pagination } = await inflo.invitations.list();

// Filter to sent only, page 2
const { invitations, pagination } = await inflo.invitations.list({
  direction: "sent",
  status: "pending",
  page: 2,
  limit: 50,
});

console.log(invitations[0].direction);  // "sent" or "received"
console.log(invitations[0].inviter.uid);
console.log(pagination.total, pagination.has_more);

invitations.get(id, opts?)

const inv = await inflo.invitations.get(42);
console.log(inv.status, inv.invitee_email, inv.delivery.email.status);

invitations.accept(id, opts?)

// Email invitation — no token required
const result = await inflo.invitations.accept(42, { userToken: oidcAccessToken });
console.log(result.connection.state);      // "accepted"
console.log(result.connection.inviter.uid);
console.log(result.redirect_uri);          // redirect URL if set by the inviter

// SMS-only invitation — must supply the opaque token from the invite link.
// The server uses this to prove the caller possessed the original SMS URL.
const result = await inflo.invitations.accept(42, {
  userToken: oidcAccessToken,
  token: smsTokenFromInviteUrl,  // hex string from /invite/<token>
});

invitations.decline(id, opts?)

const result = await inflo.invitations.decline(42);
console.log(result.invitation.status); // "declined"
console.log(result.message);

invitations.cancel(id, opts?)

Cancel a pending sent invitation (inviter only). Returns void on success (204).

await inflo.invitations.cancel(42);

invitations.resend(id, opts?)

Resend a pending invitation (rotates the token and re-sends the notification).

const result = await inflo.invitations.resend(42);
console.log(result.invitation.delivery.email.status); // "sent" or "failed"
console.log(result.message);

// With a per-channel delivery override:
await inflo.invitations.resend(42, { notify: { email: false, sms: true } });

invitations.sendBulk(request, opts?)

Send up to 100 invitations in one request (HTTP 207). The server returns a per-entry results array and a summary. An Idempotency-Key header is automatically attached by the SDK.

const result = await inflo.invitations.sendBulk({
  invitations: [
    { uid: "uid_abc123" },
    { email: "[email protected]", message: "Come join!" },
    { phone: "+15551234567" },
  ],
  expires_in_days: 14,
  notify: { sms: false },
});

console.log(result.summary); // { created: 2, skipped: 1, failed: 0 }

for (const entry of result.results) {
  if (entry.status === "created") {
    console.log(`Entry ${entry.index}: invitation #${entry.invitation_id} sent`);
  } else if (entry.status === "already_connected") {
    console.log(`Entry ${entry.index}: already connected`);
  } else if (entry.status === "already_pending") {
    console.log(`Entry ${entry.index}: invitation #${entry.invitation_id} already pending`);
  } else {
    console.warn(`Entry ${entry.index} failed: ${entry.code}`);
  }
}

connections.list(opts?)

const { connections, total, not_yet_visible_count } = await inflo.connections.list({
  userToken: oidcAccessToken,
});

connections.remove(uid, opts?)

Remove an accepted Inflo connection.

await inflo.connections.remove("uid_abc123");

// SSO-delegated
await inflo.connections.remove("uid_abc123", { userToken: oidcAccessToken });

verifyWebhookSignature({ signatureV2, body, secret })

As of 2026-11-03 Inflo emits only X-Inflo-Signature-V2. Pass it as signatureV2 — the SDK checks the embedded timestamp (replay protection, 300-second window) and the HMAC together:

import { verifyWebhookSignature } from "@infloapi/node";

app.post("/webhooks/inflo", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyWebhookSignature({
    signatureV2: req.header("X-Inflo-Signature-V2") ?? "",
    body: req.body, // Buffer — must be raw bytes before JSON.parse
    secret: process.env.INFLO_WEBHOOK_SECRET!,
  });
  if (!ok) return res.status(401).end();
  // process JSON.parse(req.body.toString("utf8"))
  res.status(204).end();
});

The v2 format signs "<unixTimestamp>.<rawBody>" with HMAC-SHA256 and encodes the result as t=<unix>,v1=<hex> in the X-Inflo-Signature-V2 header. Pass an array to secret during a key rotation — each secret is tried in order.

Errors

All HTTP failures throw InfloApiError:

import { InfloApiError } from "@infloapi/node";

try {
  await inflo.users.search({ q: "x" });
} catch (err) {
  if (err instanceof InfloApiError) {
    console.error(err.status, err.code, err.hint, err.requestId);
  }
}

Transient failures (HTTP 408/425/429/5xx and network errors) are retried with exponential backoff + jitter, capped by retry.maxRetries.

TypeScript types

Domain types like PublicUser, IdentifierResolveResult, NotificationCreateResult, InAppSearchResponse, SearchedUser, and SearchedGroup are exported from the package root. Raw OpenAPI types can be regenerated from docs/third-party-api/openapi.yaml:

npm run generate

This writes src/generated/api.d.ts using openapi-typescript.

Versioning

This package follows semver:

  • Patch — bug fixes, doc tweaks.
  • Minor — additive: new methods, new optional fields, retry tweaks.
  • Major — breaking changes to method signatures or constructor shape.

We will only release a major bump when a request/response field is removed or renamed in a breaking way, or when an existing argument changes meaning.

See CHANGELOG.md for the release log.

Examples

See the examples/ directory for runnable scripts.

Related packages

| Package | Purpose | |---------|---------| | @infloapi/react | React auth layer — InfloProvider, AuthGuard, PKCE SSO, useInfloAuth. Use alongside this SDK (server calls) when building a React frontend. | | @infloapi/react-social | React UI for connection/group selection — CirclesPickerDialog, ConnectionCard. Wires to this SDK's connections.list() via your server proxy. |

License

MIT © Inflo