@evinor/sdk
v0.1.0
Published
Official TypeScript SDK for the Evinor API: event search, sensors, structured reports, and webhook verification.
Maintainers
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.submitsucceeds, the report is filed and there is no API to withdraw it. Validate carefully before you submit.
Install
npm install @evinor/sdkRequires 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.continueare retried on retryable failures. reports.submitis always retried under its same idempotency key.- Sensor writes (
create,enable,disable,rotateSigningSecret) are retried only when you pass anidempotencyKey.sensors.updateandsensors.deleteare never retried. events.searchis 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
