TypeScript SDK
@halvorai/ingest is the TypeScript and JavaScript client. It is open source (Apache-2.0) and works without a HalvorAI
account if you give it a sink. It needs Node.js 20 or later.
npm install @halvorai/ingestDesign rules
Section titled “Design rules”- Never in the payment path.
emit()returns at once with an event id, never throws, and never waits on the network. - Fail quietly, report loudly. Delivery problems are retried, optionally kept on disk, and reported to
onErrorandstats(). They never reach your payment code. - Nothing sensitive leaves the machine. Card numbers, CVCs, secret keys and private keys are stripped from text and JSON evidence, and instruction text is hashed locally.
Create a client
Section titled “Create a client”import { HalvorClient } from '@halvorai/ingest';
const halvor = new HalvorClient({ apiKey: process.env.HALVOR_API_KEY, // hk_test_... or hk_live_... sourceKey: process.env.HALVOR_SOURCE_KEY, // the private JWK that `init` created onError: (error) => logger.warn({ error }, 'halvorai'),});Create one client per process and reuse it. With no options, the client reads HALVOR_API_KEY and HALVOR_SOURCE_KEY
from the environment.
| Option | Default | Purpose |
|---|---|---|
apiKey | HALVOR_API_KEY | The bearer key. Its prefix sets the environment and the default endpoint. |
sourceKey | HALVOR_SOURCE_KEY | Private Ed25519 JWK, as a JSON string or an object. Its kid is your source id. |
environment | from the key | test or live. Needed when you use a sink without a key. |
endpoint | from the key | Override the API base URL. |
sink | none | Receive signed envelopes yourself instead of sending them. |
spoolDir | none | Directory for batches that could not be delivered. |
onError | none | Called with a structured error. See below. |
redact | none | { extraPatterns } to remove more text, for example employee ids. |
batchSize | 100 | Events per request. The API accepts at most 100. |
flushIntervalMs | 1000 | How long events wait to be batched. |
retryDelaysMs | [1000, 2000, 4000, 8000, 16000, 32000] | Backoff between retries. |
exitHook | true | Flush for up to two seconds when the process is about to exit. |
fetch | global fetch | A custom fetch, for proxies and tests. |
A missing or malformed key never throws. The client reports a config error once, logs a warning, and drops events
quietly so your application keeps running.
Emit an event
Section titled “Emit an event”const id = halvor.emit({ type: 'payment.settled', payment: { payment_id: 'pi_3QxAbCdEfGhIjKlM', rail: 'stripe', amount: { value: '41200', unit: 'minor', currency: 'USD' }, status: 'settled', rail_ref: { payment_intent: 'pi_3QxAbCdEfGhIjKlM' }, }, payee: { type: 'merchant', name: 'Example Airways' },});emit fills in ape_version, id, occurred_at, environment (from the key), source, and the agent, owner and
instruction from the agent context. Pass any of them to override. Then it copies your input,
and in the background redacts it, validates it against the schema, signs it, and queues it.
An invalid event is not sent. It is reported to onError as invalid_event with the failing paths, and counted in
droppedInvalid.
Agent context
Section titled “Agent context”import { withAgentContext, currentContext } from '@halvorai/ingest';
await withAgentContext({ agent: { id: 'agt_1' }, owner: { id: 'own_1', type: 'business' } }, async () => { halvor.emit({ /* ... */ });});See attribute payments to agents.
Delivery, retries and the spool
Section titled “Delivery, retries and the spool”The client sends events in batches of up to 100, at most once a second or as soon as a batch is full.
| Response | What happens |
|---|---|
Network error, 5xx, 408, 429 | Retried with backoff, up to six retries, which covers an outage of about a minute. |
413 | The batch is split in half and each half is sent. |
400, 401, 403, 422 | The batch is dropped and reported. Retrying cannot help. |
202 with a rejected item | That event is reported to onError with the server’s code. It is never retried. |
If delivery still fails after the retries, a client with spoolDir writes the signed batch to a file there and replays it
the next time it can, including after a restart. Without spoolDir, the batch is dropped and reported. Use a spool
directory in any service where losing a few events during a long outage matters.
Flushing
Section titled “Flushing”Call flush() before a short-lived process ends, such as a serverless function or a script. It resolves when the queue is
empty or the time cap passes, and tells you which.
const { drained } = await halvor.flush({ timeoutMs: 5000 });await halvor.close(); // stops timers; later emits are ignored and countedErrors and statistics
Section titled “Errors and statistics”onError receives { code, message, eventId?, serverCode?, details? }. Codes: invalid_event, rejected,
unauthorized, delivery_failed, spool and config.
halvor.stats() returns counters you can export to your metrics: emitted, queued, accepted, duplicates,
rejected, dropped, droppedInvalid, droppedDisabled, droppedClosed, retries, spooled and replayed.
Alert on dropped and rejected going up.
Self-hosting with a sink
Section titled “Self-hosting with a sink”const halvor = new HalvorClient({ sourceKey: process.env.HALVOR_SOURCE_KEY, environment: 'test', sink: async (envelopes) => { await myStore.insert(envelopes); // each is { event, sig } },});The sink gets the same signed envelopes the API would. Failures are retried like an outage. You can store APE events anywhere and still produce a verifiable record.
Subpath imports
Section titled “Subpath imports”Adapters live under their own paths, so you only load what you use:
@halvorai/ingest/stripe, /x402, /mpp, /mcp and /evidence.