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

@epostak/sdk

v3.3.2

Published

Official Node.js SDK for the ePošťák API — Peppol e-invoicing for Slovakia

Readme

@epostak/sdk

Official Node.js / TypeScript SDK for the ePošťák API — Peppol e-invoicing for Slovakia and the EU.

Zero runtime dependencies. Requires Node.js 18+.

Recent changes

v3.2.0 — 2026-05-12

  • New: Pull API — client.inbound (list, get, getUbl, ack) and client.outbound (list, get, getUbl, events) resources with full TypeScript types (InboundDocument, OutboundDocument, OutboundEvent, etc.).
  • New: UblValidationError class — thrown on 422 UBL_VALIDATION_ERROR; carries .rule (e.g. "BR-06") and UblRule exported union type for the 7 known rule codes.
  • New: client.webhooks.test(id, { event? })event is now passed as ?event= query parameter (server precedence over body).
  • New: client.lastRateLimit: { limit, remaining, resetAt: Date } | null — updated after every request that includes X-RateLimit-* response headers.
  • Improved: WebhookDelivery type adds optional idempotency_key?: string — SHA-256 hex stable across retry attempts.
  • Improved: WebhookDeliveriesParams adds includeResponseBody?: boolean (opt-in response body in delivery history).
  • Improved: WebhookEvent union adds "document.failed" variant.
  • Resolved doc drifts surfaced by 2026-05-12 endpoint consistency audit.

v3.0 — OAuth-only auth. The SDK now auto-mints a JWT on the first API call and refreshes it before expiry. Constructor takes clientId + clientSecret instead of apiKey. Raw sk_live_* bearer is no longer accepted by the server. See CHANGELOG.md.


Installation

npm install @epostak/sdk

Quick Start

import { EPostak } from "@epostak/sdk";

const client = new EPostak({
  clientId: "sk_live_xxxxx",
  clientSecret: "sk_live_xxxxx",
});

const result = await client.documents.send({
  receiverPeppolId: "0245:1234567890",
  invoiceNumber: "FV-2026-001",
  issueDate: "2026-04-04",
  dueDate: "2026-04-18",
  items: [
    { description: "Konzultácia", quantity: 10, unitPrice: 50, vatRate: 23 },
  ],
});
console.log(result.documentId, result.messageId, result.payloadSha256);

Peppol ID Format (Slovakia)

| Scheme | Identifier | Format | Example | | ------ | ---------- | ----------------- | ----------------- | | 0245 | DIČ | 0245:XXXXXXXXXX | 0245:1234567890 |

Per Slovak PASR, only 0245:DIČ is used. The 9950:SK... VAT form is not supported.


Authentication

| Key prefix | Use case | | ----------- | -------------------------------------------------- | | sk_live_* | Direct access — acts on behalf of your own firm | | sk_int_* | Integrator access — acts on behalf of client firms |

const client = new EPostak({
  clientId: "sk_live_xxxxx",
  clientSecret: "sk_live_xxxxx",
  baseUrl: "https://...", // optional, defaults to https://epostak.sk/api/v1
  firmId: "uuid", // optional, required for integrator keys
});

OAuth client_credentials (automatic)

The SDK automatically mints a JWT on the first request and refreshes it before expiry. You never handle tokens directly. For manual token management:

const tokens = await client.auth.token({
  clientId: "sk_live_xxxxx",
  clientSecret: "sk_live_xxxxx",
});
console.log(tokens.access_token, tokens.expires_in); // 900s

const renewed = await client.auth.renew({
  refreshToken: tokens.refresh_token,
});

await client.auth.revoke({
  token: tokens.refresh_token,
  tokenTypeHint: "refresh_token",
});

Key introspection, rotation, IP allowlist

const status = await client.auth.status();
console.log(status.key.prefix, status.plan.name, status.firm.peppolStatus);

const rotated = await client.auth.rotateSecret(); // sk_live_* only
console.log(rotated.key); // store immediately — only returned once

await client.auth.ipAllowlist.update({
  cidrs: ["192.168.1.0/24", "203.0.113.42"],
});
const { ip_allowlist } = await client.auth.ipAllowlist.get();

API Reference

Documents

// Send a document (JSON mode — UBL auto-generated)
const result = await client.documents.send(
  {
    receiverPeppolId: "0245:1234567890",
    receiverName: "Firma s.r.o.",
    invoiceNumber: "FV-2026-001",
    issueDate: "2026-04-04",
    dueDate: "2026-04-18",
    currency: "EUR",
    items: [{ description: "Konzultácia", quantity: 10, unitPrice: 50, vatRate: 23 }],
  },
  // Optional: replay-safe send. Server returns 409 (idempotency_conflict)
  // if the same key is replayed before the original request finishes.
  { idempotencyKey: "fv-2026-001-send" },
);

// Send pre-built UBL XML
await client.documents.send({
  receiverPeppolId: "0245:1234567890",
  xml: '<?xml version="1.0"?>...',
});

// Get document by ID
const doc = await client.documents.get("doc-uuid");

// Update a draft document
await client.documents.update("doc-uuid", { invoiceNumber: "FV-2026-002", dueDate: "2026-05-01" });

// Status with full history
const status = await client.documents.status("doc-uuid");

// Delivery evidence (AS4, MLR, invoice response)
const evidence = await client.documents.evidence("doc-uuid");

// Download PDF / UBL XML
const pdf = await client.documents.pdf("doc-uuid");
const ubl = await client.documents.ubl("doc-uuid");

// Respond to received invoice (AP=accept, RE=reject, UQ=query)
await client.documents.respond("doc-uuid", { status: "AP", note: "Akceptované" });

// Validate without sending — pass the JSON invoice or raw UBL XML
const validation = await client.documents.validate({
  format: "json",
  document: { receiverPeppolId: "0245:1234567890", items: [/* ... */] },
});

// Check receiver capability
const check = await client.documents.preflight({ receiverPeppolId: "0245:1234567890" });

// Convert between JSON and UBL
const converted = await client.documents.convert({
  input_format: "json",
  output_format: "ubl",
  document: { ... },
});

Inbox

// List received documents
const inbox = await client.documents.inbox.list({
  limit: 20,
  status: "RECEIVED",
  since: "2026-04-01T00:00:00Z",
});

// Get full detail with UBL XML payload
const detail = await client.documents.inbox.get("doc-uuid");
console.log(detail.document, detail.payload);

// Acknowledge (mark as processed)
await client.documents.inbox.acknowledge("doc-uuid");

// Cross-firm inbox (integrator only)
const all = await client.documents.inbox.listAll({
  limit: 50,
  firm_id: "firm-uuid",
});

Audit (per-firm security feed)

Cursor-paginated walk over (occurred_at DESC, id DESC).

let cursor: string | null = null;
do {
  const page = await client.audit.list({
    event: "jwt.issued",
    since: "2026-04-01T00:00:00Z",
    cursor,
    limit: 50,
  });
  for (const ev of page.items) {
    console.log(ev.occurred_at, ev.event, ev.actor_id);
  }
  cursor = page.next_cursor;
} while (cursor);

Peppol

const participant = await client.peppol.lookup("0245", "1234567890");

const results = await client.peppol.directory.search({
  q: "Telekom",
  country: "SK",
});

const company = await client.peppol.companyLookup("12345678");

Firms (integrator)

const firms = await client.firms.list();
const firm = await client.firms.get("firm-uuid");
const docs = await client.firms.documents("firm-uuid", {
  limit: 20,
  direction: "inbound",
});
await client.firms.registerPeppolId("firm-uuid", {
  scheme: "0245",
  identifier: "1234567890",
});

// Assign firm by ICO
await client.firms.assign({ ico: "12345678" });
await client.firms.assignBatch({ icos: ["12345678", "87654321"] });

Webhooks

// Create webhook (store secret for HMAC verification!)
const webhook = await client.webhooks.create(
  {
    url: "https://example.com/webhook",
    events: ["document.received", "document.sent"],
  },
  { idempotencyKey: "create-prod-webhook" },
);

const list = await client.webhooks.list();
const detail = await client.webhooks.get(webhook.id);
await client.webhooks.update(webhook.id, { isActive: false });
await client.webhooks.delete(webhook.id);

// Rotate the signing secret (issues a fresh one, invalidates the old).
const { secret } = await client.webhooks.rotateSecret(webhook.id);

Verifying a delivery

import express from "express";
import { verifyWebhookSignature } from "@epostak/sdk";

const app = express();

app.post(
  "/webhooks/epostak",
  // express.raw is required — we MUST hash the bytes off the wire,
  // not the parsed-and-re-stringified JSON.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const result = verifyWebhookSignature({
      payload: req.body, // Buffer
      signature: req.header("x-webhook-signature") ?? "",
      timestamp: req.header("x-webhook-timestamp") ?? "",
      secret: process.env.EPOSTAK_WEBHOOK_SECRET!,
      // toleranceSeconds: 300, // default — clamps replay attacks
    });
    if (!result.valid) {
      return res.status(400).send(`bad signature: ${result.reason}`);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // process event...
    res.status(204).end();
  },
);

Dedup + retry headers (server v1.1 — 2026-05-12)

The server now ships three additional headers on every push delivery:

| Header | Value | Use | |-|-|-| | X-Webhook-Event-Id | UUID, stable across retries | Primary dedup key. Body also carries it as webhook_event_id. | | X-Webhook-Attempt | 1-based attempt number | Telemetry / logging. | | X-Webhook-Max-Attempts | Total attempts in the retry window (10) | Telemetry / logging. |

Recommended receiver pattern:

// INSERT ON CONFLICT DO NOTHING on the event id is enough — every retry
// of the same logical event carries the SAME X-Webhook-Event-Id.
const eventId = req.header("x-webhook-event-id");
const inserted = await db.query(
  `INSERT INTO processed_webhooks (event_id) VALUES ($1)
   ON CONFLICT (event_id) DO NOTHING RETURNING id`,
  [eventId],
);
if (inserted.rowCount === 0) {
  return res.status(200).end(); // duplicate — ack and skip
}
// process event for the first time...

Retry policy (server-side, as of 2026-05-12): we retry only on 408, 425, 429, 502, 503, 504 and network errors (~44h bounded backoff). Returning any other 4xx/5xx — including 500 — terminates the retry loop immediately. If your handler hits an app-level error and you want us to retry, return 503 (not 500).

The signature contract is unchangedverifyWebhookSignature continues to work without code changes.

Webhook Pull Queue

// Pull pending events
const queue = await client.webhooks.queue.pull({ limit: 50 });
for (const item of queue.items) {
  console.log(item.event_id, item.event, item.payload);
  await client.webhooks.queue.ack(item.event_id);
}
if (queue.has_more) {
  // Drain remaining events on the next iteration
}

// Batch acknowledge
await client.webhooks.queue.batchAck(queue.items.map((e) => e.event_id));

// Cross-firm (integrator)
const allEvents = await client.webhooks.queue.pullAll({ limit: 200 });
await client.webhooks.queue.batchAckAll(
  allEvents.items.map((e) => e.event_id),
);

Reporting

// Convenience period selector
const stats = await client.reporting.statistics({ period: "month" });
console.log(stats.sent.total, stats.sent.by_type);
console.log(stats.received.total, stats.received.by_type);
console.log(stats.delivery_rate); // e.g. 0.987
console.log(stats.top_recipients); // up to 5
console.log(stats.top_senders);

// Or an explicit window
await client.reporting.statistics({ from: "2026-01-01", to: "2026-03-31" });

Account

const account = await client.account.get();

Extract (AI OCR)

import { readFileSync } from "fs";

// Single file
const result = await client.extract.single(
  readFileSync("invoice.pdf"),
  "application/pdf",
  "invoice.pdf",
);

// Batch (up to 10 files, server-side)
const batch = await client.extract.batch([
  { file: pdfBuffer, mimeType: "application/pdf", fileName: "inv1.pdf" },
  { file: imgBuffer, mimeType: "image/png", fileName: "inv2.png" },
]);

Integrator Mode

// Option 1: firmId in constructor
const client = new EPostak({
  clientId: "sk_int_xxxxx",
  clientSecret: "sk_int_xxxxx",
  firmId: "client-firm-uuid",
});

// Option 2: withFirm() for switching (shares JWT)
const base = new EPostak({
  clientId: "sk_int_xxxxx",
  clientSecret: "sk_int_xxxxx",
});
const clientA = base.withFirm("firm-uuid-a");
const clientB = base.withFirm("firm-uuid-b");

Error Handling

EPostakError normalizes both the legacy { error: { code, message } } envelope and RFC 7807 application/problem+json.

import { EPostak, EPostakError } from "@epostak/sdk";

try {
  await client.documents.send({ ... });
} catch (err) {
  if (err instanceof EPostakError) {
    console.error(err.status);         // HTTP status (0 for network errors)
    console.error(err.code);           // e.g. 'VALIDATION_FAILED'
    console.error(err.message);        // Human-readable
    console.error(err.details);        // Validation error list (422)
    console.error(err.requestId);      // From X-Request-Id (or body)
    console.error(err.title, err.detail, err.type, err.instance); // RFC 7807

    if (err.code === "idempotency_conflict") {
      // The same Idempotency-Key is still in flight server-side.
      return;
    }
    if (err.requiredScope) {
      // 403 with WWW-Authenticate: insufficient_scope
      console.error(`Mint a token with scope: ${err.requiredScope}`);
    }
  }
}

Common error codes from documents.send():

| Status | Code | Meaning | | ------ | ---------------------- | ------------------------------------------------------------------------ | | 409 | idempotency_conflict | Same Idempotency-Key is still in flight server-side. Retry shortly. | | 422 | VALIDATION_FAILED | Document failed Peppol BIS 3.0 validation. details has the error list. | | 502 | SEND_FAILED | Peppol network temporarily unavailable. Retryable. |


Full Endpoint Map

| Method | HTTP | Path | | ---------------------------------------- | ------ | -------------------------------------------- | | auth.token({ clientId, clientSecret }) | POST | /auth/token | | auth.renew({ refreshToken }) | POST | /auth/renew | | auth.revoke({ token }) | POST | /auth/revoke | | auth.status() | GET | /auth/status (alias: /auth/token/status) | | auth.rotateSecret() | POST | /auth/rotate-secret | | auth.ipAllowlist.get() | GET | /auth/ip-allowlist | | auth.ipAllowlist.update({ cidrs }) | PUT | /auth/ip-allowlist | | audit.list(params?) | GET | /audit | | documents.get(id) | GET | /documents/{id} | | documents.update(id, body) | PATCH | /documents/{id} | | documents.send(body, opts?) | POST | /documents/send | | documents.sendBatch(items, opts?) | POST | /documents/send/batch | | documents.status(id) | GET | /documents/{id}/status | | documents.evidence(id) | GET | /documents/{id}/evidence | | documents.pdf(id) | GET | /documents/{id}/pdf | | documents.ubl(id) | GET | /documents/{id}/ubl | | documents.respond(id, body) | POST | /documents/{id}/respond | | documents.validate(body) | POST | /documents/validate | | documents.preflight(body) | POST | /documents/preflight | | documents.convert(body) | POST | /documents/convert | | documents.inbox.list(params?) | GET | /documents/inbox | | documents.inbox.get(id) | GET | /documents/inbox/{id} | | documents.inbox.acknowledge(id) | POST | /documents/inbox/{id}/acknowledge | | documents.inbox.listAll(params?) | GET | /documents/inbox/all | | peppol.lookup(scheme, id) | GET | /peppol/participants/{scheme}/{id} | | peppol.directory.search(params?) | GET | /peppol/directory/search | | peppol.companyLookup(ico) | GET | /company/lookup/{ico} | | firms.list() | GET | /firms | | firms.get(id) | GET | /firms/{id} | | firms.documents(id, params?) | GET | /firms/{id}/documents | | firms.registerPeppolId(id, body) | POST | /firms/{id}/peppol-identifiers | | firms.assign(body) | POST | /firms/assign | | firms.assignBatch(body) | POST | /firms/assign/batch | | webhooks.create(body, opts?) | POST | /webhooks | | webhooks.list() | GET | /webhooks | | webhooks.get(id) | GET | /webhooks/{id} | | webhooks.update(id, body) | PATCH | /webhooks/{id} | | webhooks.delete(id) | DELETE | /webhooks/{id} | | webhooks.rotateSecret(id) | POST | /webhooks/{id}/rotate-secret | | webhooks.queue.pull(params?) | GET | /webhook-queue | | webhooks.queue.ack(eventId) | DELETE | /webhook-queue/{eventId} | | webhooks.queue.batchAck(ids) | POST | /webhook-queue/batch-ack | | webhooks.queue.pullAll(params?) | GET | /webhook-queue/all | | webhooks.queue.batchAckAll(ids) | POST | /webhook-queue/all/batch-ack | | reporting.statistics(params?) | GET | /reporting/statistics | | account.get() | GET | /account | | extract.single(file, mime, name) | POST | /extract | | extract.batch(files) | POST | /extract/batch |

All paths relative to https://epostak.sk/api/v1.


License

MIT