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

@evinor/sdk

v0.1.0

Published

Official TypeScript SDK for the Evinor API: event search, sensors, structured reports, and webhook verification.

Readme

@evinor/sdk

The official TypeScript SDK for the Evinor public API: search structured events, manage sensors, file structured reports, and verify webhook deliveries.

Full API documentation: https://docs.evinor.ai

Reports cannot yet be retracted. Once reports.submit succeeds, the report is filed and there is no API to withdraw it. Validate carefully before you submit.

Install

npm install @evinor/sdk

Requires Node.js 20 or later (ESM only). The SDK is for server-side use only: an Evinor API key is a secret and must never be shipped to a browser.

Configure

import { Evinor } from '@evinor/sdk';

// Reads EVINOR_API_KEY from the environment when `apiKey` is omitted.
const client = new Evinor();

// Or pass the key explicitly (e.g. from a secrets manager).
const explicit = new Evinor({ apiKey: mySecrets.evinorApiKey });

API keys start with evnr_live_. The constructor throws an EvinorError when no key is found or the key is malformed; the error message never contains the key.

Optional settings: baseUrl, fetch (a custom fetch implementation), maxRetries (default 2, 0 disables retries), maxRetryDelayMs (default 60 000), and timeoutMs (per attempt, default 60 000).

Every method resolves to { data, requestId, status, headers }. Include requestId when you contact support.

Quickstart 1 — submit a structured report

A report is filed against an event type. The builder checks your report against the event type's roles locally, before any request is sent.

import { Evinor, ReportValidationError } from '@evinor/sdk';

const client = new Evinor();

// 1. Pick an event type and look at its roles.
const { data: eventTypes } = await client.eventTypes.list();
const eventType = eventTypes.data.find((t) => t.code_name === 'ACQUISITION');
if (!eventType) throw new Error('event type not available');
for (const role of eventType.roles) {
  console.log(role.code_name, role.expected_type);
}

// 2. Resolve named entities to ids.
const acquirer = await client.entities.search({ query: 'Acme Corp' }).firstPage();
const acquirerId = acquirer.data.data[0]?.id;
if (!acquirerId) throw new Error('entity not found');

// 3. Build the report: one value per role, of the kind its expected_type calls for.
const builder = client.reports
  .builder(eventType)
  .happenedAt('2026-09-01')
  .role('ACQUIRER', { entityId: acquirerId })
  .role('ANNOUNCED_ON', { date: '2026-09-01' })
  .role('DEAL_VALUE', { amount: { value: 250_000_000, unit: 'USD' } })
  .description('Acme Corp agreed to acquire Example Ltd.');

// 4. Submit.
try {
  const result = await builder.submit();
  console.log('filed', result.data.id, 'key', result.idempotencyKey);
} catch (err) {
  if (err instanceof ReportValidationError) {
    console.error(err.code, err.role, err.message); // nothing was sent
  }
  throw err;
}

Role values: NAMED_ENTITY → { entityId }, DATE → { date: 'YYYY-MM-DD' }, amount types → { amount: { value, unit } }, anything else → { text }. To file under a reporting grant, pass a grant id from client.reportingGrants.list() to .grant(id). You can also pass an event type id to client.reports.builder(id), which fetches the event type list first (and returns a promise).

The role code names above are illustrative; always read them from eventType.roles.

Exactly-once filing: persist the idempotency key

Every submit is sent with an Idempotency-Key: yours, or a freshly generated one. It is returned as result.idempotencyKey and attached to every thrown error as err.idempotencyKey.

If a submit fails for any reason — a timeout, a network error, a crash, an abort — the report may or may not have been filed. Resubmit with the same key, never a new one. A new key files a duplicate report.

import { EvinorError, ReportAlreadySubmittedError } from '@evinor/sdk';

// Mint the key yourself and store it with your job, so a restart can reuse it.
const idempotencyKey = crypto.randomUUID();
await saveJob({ idempotencyKey });

try {
  await builder.submit({ idempotencyKey });
} catch (err) {
  if (err instanceof ReportAlreadySubmittedError) {
    // The earlier attempt was filed. Nothing to do.
  } else if (err instanceof EvinorError && err.idempotencyKey) {
    // Retry later with err.idempotencyKey — never a new key.
  }
}

Retryable failures are retried automatically, always under the same key.

Quickstart 2 — search events

// A new search is BILLED. It is never retried automatically.
const { data: page } = await client.events.search({
  event_type_id: eventType.id,
  lookback_days: 30,
  limit: 50,
});

// Further pages of the same execution are FREE, and retried automatically.
if (page.has_more && page.next_cursor) {
  const { data: next } = await client.events.continue(page.next_cursor, { limit: 50 });
}

Or walk every result with one billed execution:

for await (const event of client.events.searchAll({ lookback_days: 7 })) {
  console.log(event.id, event.sentence);
}

searchAll bills exactly once and pages through the free continuations. If the execution expires mid-walk it throws a search-execution-expired error rather than re-running (and re-billing) the search. Because events.search is billed, retrying a failed search is always your decision: check err.retryable.

Quickstart 3 — verify a webhook

Sensors deliver matching events to your webhook, signed with the sensor's signing_secret (returned once, when the sensor is created or its secret is rotated).

import { verifyWebhook } from '@evinor/sdk';

// Use the RAW request body string, before any JSON middleware touches it.
app.post('/evinor-webhook', express.text({ type: '*/*' }), async (req, res) => {
  const ok = await verifyWebhook({
    payload: req.body,
    signature: req.get('X-Evinor-Signature') ?? '',
    secret: process.env.EVINOR_SIGNING_SECRET ?? '',
    toleranceSeconds: 300, // optional replay guard on the envelope timestamp
  });
  if (!ok) return res.status(401).end();
  // ... handle JSON.parse(req.body)
  res.status(204).end();
});

verifyWebhook resolves false (it never throws) for a bad signature, a malformed body, or a timestamp outside toleranceSeconds.

Pagination

List methods (sensors.list, reports.list, entities.search) return a page walker: iterate it with for await to get every item, or call .firstPage() for a single page.

Errors

Every error thrown by the SDK extends EvinorError, which carries:

  • retryable — whether sending the same request again can succeed;
  • requestId — the server's request id, when there was a response;
  • idempotencyKey — the key the request was sent with, if any.

| Class | When | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | | EvinorApiError | The API returned a problem response. Has status, problemKey (e.g. rate-limited), title, detail, errors. | | ReportAlreadySubmittedError | A resubmit under an idempotency key whose report was already filed. | | ReportValidationError | The report builder rejected the report locally. Has code and role. Nothing was sent. | | EvinorConnectionError | A network failure, a timeout, or an error response without a problem body. Has kind. |

Retry policy

Retries are decided per operation, not per HTTP method:

  • Reads and events.continue are retried on retryable failures.
  • reports.submit is always retried under its same idempotency key.
  • Sensor writes (create, enable, disable, rotateSigningSecret) are retried only when you pass an idempotencyKey. sensors.update and sensors.delete are never retried.
  • events.search is billed and is never retried.

Waits honor Retry-After / RateLimit-Reset, otherwise use jittered exponential backoff, capped by maxRetryDelayMs. Pass an AbortSignal as signal to cancel a call, including any wait between retries.

Versioning

The Evinor /v1 API is in preview, so this package stays at 0.x. While it does, a minor version bump may contain breaking changes; patch releases do not. The SDK ignores unknown response fields, so additive API changes do not break it. See docs.evinor.ai for the API's versioning policy.

License

MIT