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

@hookticon/client

v0.1.0

Published

Typed Node.js client for the Hookticon webhook relay API.

Readme

@hookticon/client

Typed Node.js client for the Hookticon webhook relay — hand it an HTTP request and it delivers that request to your target, retrying on a backoff curve until the target answers 2xx or the attempts run out.

Types are generated from the API's own OpenAPI document, so a change to the relay's contract shows up here as a compile error rather than a runtime 400.

Installation

npm install @hookticon/client

Requires Node.js >= 18 (for the global fetch).

This is an ESM-only package — it publishes no CommonJS build. From a CJS file, reach it with a dynamic import: const { createHookticonClient } = await import('@hookticon/client').

Entry points

| Import | Environment | Notes | | --- | --- | --- | | @hookticon/client | Node.js | The only entry point |

There is no browser build, deliberately. The API key authenticates every relay call, and a key shipped in a browser bundle is a key handed to every visitor. Call this from your server.

Quick start

import { createHookticonClient } from '@hookticon/client';

const hookticon = createHookticonClient({
  baseUrl: 'https://hookticon.example.com',
  apiKey: process.env.HOOKTICON_API_KEY!,
});

// Hand over a request to be delivered elsewhere. Returns as soon as it is accepted.
const { id } = await hookticon.registerWebhook({
  url: 'https://partner.example.com/webhooks/orders',
  body: JSON.stringify({ orderId: 'A-1' }),
  headers: { 'content-type': 'application/json' },
  timeoutStrategy: 'SDF3',
  failNotifyUrl: 'https://my-app.example.com/hookticon/failed',
});

// Later: how did it go?
const page = await hookticon.listWebhooks({ status: 'FAIL', limit: 100 });

A successful registerWebhook means accepted for delivery, not delivered. Delivery is asynchronous; poll listWebhooks or supply a failNotifyUrl to learn the outcome.

Configuration

type HookticonClientOptions = {
  /** Where the relay lives, e.g. `https://hookticon.example.com`. Required — there is no default. */
  baseUrl: string;

  /** Sent as `x-hookticon-api-key` on every authenticated call. A server-side secret. */
  apiKey: string;

  /** Optional. When given, every response is logged at debug level. Pino/winston/console-shaped. */
  logger?: {
    debug(obj: unknown, msg?: string): void;
    info(obj: unknown, msg?: string): void;
    warn(obj: unknown, msg?: string): void;
    error(obj: unknown, msg?: string): void;
  };

  /** Optional. Override the global `fetch` — a proxy agent, an instrumented client, a test double. */
  fetch?: typeof fetch;
};

baseUrl has no default on purpose: the relay's production host is deployment-specific, so any value baked into this package would be wrong for someone, and silently relaying production traffic at the wrong host is worse than a compile error. HOOKTICON_DEV_BASE_URL is exported for local experiments.

API

| Method | Description | | --- | --- | | registerWebhook(params) | Hand the relay a request to deliver. Returns { id }. | | listWebhooks(params?) | One page of delivery records, newest first, plus the total. | | health() | Liveness probe. Unauthenticated. |

registerWebhook(params)

| Parameter | Type | Default | Description | | --- | --- | --- | --- | | url | string | — | Required. Absolute URL the request is delivered to. | | method | string | post (server-side) | HTTP method used against the target. | | timeoutStrategy | 'DEFAULT' \| 'SDF3' \| 'NO_TIMEOUT' | DEFAULT (server-side) | Retry curve. | | failNotifyUrl | string | none | POSTed once, with the webhook record as JSON, when retries are exhausted. | | body | BodyInit | none | Relayed byte for byte — never parsed or re-encoded. | | headers | Record<string, string> | none | Forwarded verbatim to the target. |

Retry curves:

| Strategy | Behaviour | | --- | --- | | DEFAULT | Up to 37 attempts, tapering 1min → 3min → 10min → 1h → 1 day → 2 days | | SDF3 | 4 attempts: after 6h, then 1 day, then 1 day | | NO_TIMEOUT | A single attempt, never retried |

Defaults for method and timeoutStrategy are applied by the relay, not by this package — an unset option is omitted from the request rather than filled in here, so the two can never disagree.

Everything in headers travels on to the target, including any credential it expects. The relay strips the x-hookticon-* control headers and host; a caller header can never displace a control header or the API key.

// Binary payloads are relayed unchanged — set a content-type so the target knows what it got.
await hookticon.registerWebhook({
  url: 'https://partner.example.com/ingest',
  body: new Uint8Array(pdfBytes),
  headers: { 'content-type': 'application/pdf', authorization: 'Bearer partner-token' },
});

listWebhooks(params?)

All filters are optional and combine with AND.

| Parameter | Type | Default | Description | | --- | --- | --- | --- | | status | 'PENDING' \| 'SUCCESS' \| 'FAIL' | none | Only webhooks in this delivery state. | | timeoutStrategy | 'DEFAULT' \| 'SDF3' \| 'NO_TIMEOUT' | none | Only webhooks on this retry curve. | | url | string | none | Case-insensitive substring of the target URL. | | createdFrom | string (ISO 8601) | none | Registered at or after this instant, inclusive. | | createdTo | string (ISO 8601) | none | Registered at or before this instant, inclusive. | | limit | number (1–200) | 50 | Page size. | | offset | number | 0 | Rows skipped, for paging. |

const { items, total, limit, offset } = await hookticon.listWebhooks({
  status: 'FAIL',
  url: 'partner.example.com',
  createdFrom: '2026-01-01T00:00:00.000Z',
  limit: 100,
});

for (const webhook of items) {
  console.log(webhook.id, webhook.tries, webhook.nextTryAt);
}

The relayed request itself — body and forwarded headers — is deliberately not returned: it is your own payload and may carry your credentials. It is visible in the relay's admin panel, which is guarded by a separate password.

health()

const { status } = await hookticon.health(); // { status: 'ok' }

Answers as soon as the relay is serving and checks no dependencies — a liveness signal, not a readiness one. Unauthenticated, so it works before an API key is configured.

Error handling

Every failure is an Error subclass from this package, so one instanceof catches them all.

| Class | Meaning | | --- | --- | | HookticonError | Base class. Everything below extends it. | | HookticonApiError | The relay answered, and the answer was not 2xx. Carries statusCode, code, response. | | HookticonTransportError | The relay could not be reached — DNS, TLS, refused connection, abort. Carries cause. |

The split matters: a HookticonApiError is usually a bug in the call, a HookticonTransportError is usually worth retrying.

| Type guard | Narrows to | | --- | --- | | isHookticonError(error) | HookticonError | | isHookticonApiError(error, code?) | HookticonApiError, optionally with that code | | isHookticonApiCodeError(error, 'UNAUTHORIZED') | HookticonApiError & { code: 'UNAUTHORIZED' }code is the literal inside the branch | | isHookticonTransportError(error) | HookticonTransportError |

| code | Status | Meaning | | --- | --- | --- | | INVALID_HEADERS | 400 | A control header is missing or malformed (registerWebhook). | | INVALID_QUERY | 400 | A query parameter is malformed or out of range (listWebhooks). | | UNAUTHORIZED | 401 | Missing or unknown API key. | | INTERNAL_ERROR | 500 | Unexpected server-side failure. |

code is undefined when something between you and the relay — a proxy, a load balancer — answered instead of the relay itself. statusCode is always set on a HookticonApiError.

import { isHookticonApiCodeError, isHookticonTransportError } from '@hookticon/client';

try {
  await hookticon.registerWebhook({ url: target, body });
} catch (error) {
  if (isHookticonApiCodeError(error, 'UNAUTHORIZED')) {
    throw new Error('Rotate HOOKTICON_API_KEY');
  }
  if (isHookticonTransportError(error)) {
    return scheduleRetry(); // the relay is unreachable, not unhappy
  }
  throw error;
}

Advanced

A custom fetch

Anything fetch-shaped works — a proxy agent, an OpenTelemetry-instrumented client, or a test double:

const hookticon = createHookticonClient({
  baseUrl,
  apiKey,
  fetch: (request) => instrumentedFetch(request),
});

Regenerating from the spec

src/clients/generated and openapi.json are both committed, so a fresh clone builds without the API being reachable, and spec drift is visible in the diff. Never hand-edit src/clients/generated — change the spec in the backend repo and regenerate:

npm run codegen:dump    # dump the spec from ../backend's working tree, then generate
npm run codegen:fetch   # or: pull it from a deployment (OPENAPI_URL overrides the default)
npm run codegen         # regenerate from the openapi.json already committed here

CI re-runs npm run codegen and fails on a non-empty diff, so committed types cannot quietly describe an older spec.

License

MIT