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

@getpeppr/sdk

v5.10.0

Published

Send Peppol e-invoices in under 20 lines of code

Readme

@getpeppr/sdk

Send Peppol e-invoices in under 20 lines of code.

npm version License: MIT TypeScript Node

import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_sandbox_..." });
const identity = await peppol.identity.get();
if (identity.sandboxFirstSend?.status !== "ready") {
  throw new Error(identity.sandboxFirstSend?.message ?? "Sandbox profile unavailable");
}

const result = await peppol.invoices.send({
  number: "INV-2026-001",
  // 9925:BE0314595348 = SPF Economie, the Peppol test receiver —
  // sandbox delivers only to test recipients. In production,
  // replace with your customer's Peppol ID.
  to: {
    name: "SPF Economie (test receiver)",
    peppolId: "9925:BE0314595348",
    street: "Rue du Progrès 50",
    city: "Brussels",
    postalCode: "1210",
    country: "BE",
  },
  lines: [
    { description: "Consulting services", quantity: 10, unitPrice: 150, ...identity.sandboxFirstSend.line },
  ],
});

console.log(`Invoice sent! ID: ${result.id}`);

Installation

npm install @getpeppr/sdk

Upgrading to 5.0.0

Three things changed for callers, all measured against the published 4.8.1. The full list is in CHANGELOG.md, shipped inside the package.

BGN and HRK are no longer currencies. Bulgaria and Croatia joined the euro, and Peppol BIS Billing 3.0.21 — mandatory on the network since 2026-08-17 — dropped both from BR-CL-04/BR-CL-05. getCurrency("BGN") now returns undefined and validateInvoice rejects the code. Use EUR. In the other direction, XCG (Caribbean guilder) is accepted; it replaces ANG, which this SDK never carried.

A blank idempotency key now throws instead of travelling. idempotencyKey: " " used to be sent; HTTP strips the whitespace, so the gateway received nothing, skipped its cache and its lock, and treated every retry as a fresh request. It now raises a PeppolValidationError before anything leaves. If you passed one, you were already unprotected — this is where you find out.

A negative allowance or charge amount is refused. 4.8.1 accepted it and rendered a document whose totals disagreed with its lines. An allowance reduces the invoice and a charge increases it — the direction is carried by the field, never by the sign.

If you pass none of the three, upgrading changes nothing you have to fix.

What did NOT change, despite an older promise. The 3.2.0 release notes said L (IGIC) and M (IPSI) would leave the vatCategory type "in the next major release". They are still there, and this is the second major since. The reason is that buildInvoiceXml, buildCreditNoteXml and Peppol.toXml() take the same input type as invoices.send(), and they deliberately render those categories — an IGIC invoice is a valid EN 16931 document you may deliver through another channel. Removing them from the type would take that away. What is true is narrower: invoices.send() refuses L and M with 422 unsupported_vat_category, because our provider has no vocabulary for them. Tracked as GPR-1218.

Upgrading to 3.0.0

Two changes are worth a minute before you upgrade. The full list is in CHANGELOG.md, shipped inside the package.

GB:CRN was removed from SCHEMES_BY_COUNTRY.GB. The Companies House number is not a Peppol scheme — it has no EAS code and is absent from the OpenPeppol code list, so an entity registered under it is reachable by nobody. If you read that constant to build a scheme picker, the entry is now gone and UK users should be offered GB:VAT.

Calling validatePeppolIdentifier("GB:CRN", …) directly now fails open: since the scheme is no longer listed, it takes the permissive path any unrecognised scheme takes, so ("GB:CRN", "abc") returns valid: true where 2.6.0 returned valid: false. If you relied on that call to reject malformed Companies House numbers, it no longer does.

Since GPR-1350 that permissive path is no longer unconditional. An unrecognised scheme still skips pattern and checksum, but the identifier VALUE must use POLICY 1's alphabet — a-z, A-Z, 0-9, -, ., _, ~. The example above is unaffected (abc is inside the set), but ("GB:CRN", "AB/CD") now returns valid: false. See the CHANGELOG.

Scheme lookup became case-insensitive, so format rules now apply to spellings that used to escape them. In 2.6.0, validatePeppolIdentifier("gb:vat", "not-a-vat") returned valid: true, because the lowercase spelling matched no known scheme and fell through to a permissive path. It now returns valid: false. The same applies to de:lwid and to the numeric spelling 9932 — spellings that resolved to nothing before. 0204 is not affected: it was already recognised and already rejected malformed values. Nothing that was correct becomes incorrect — but input that silently passed may now be rejected.

Features

  • Stripe-like DX -- JSON in, Peppol out
  • Local UBL 2.1 tooling -- build and validate Peppol BIS documents before you transmit them
  • Document type codes in local UBL -- builders preserve BR-CL-01 invoice and credit-note codes, including standard (380), credit note (381), debit (383), prepayment (386), and self-billed (389)
  • Honest JSON send contract -- POST /v1/invoices currently derives invoice vs credit note from isCreditNote; invoiceTypeCode is inert until the provider route carries it into UBL
  • Client-side + server-side validation -- partial offline pre-flight checks and UBL generation sanity checks
  • Peppol Directory lookup -- verify participants before sending
  • Webhook signature verification -- HMAC-SHA256 with replay protection
  • Batch send with concurrency control
  • Built-in retry with exponential backoff and rate limit handling
  • Async pagination -- iterate over all invoices, events, contacts
  • TypeScript-first, zero dependencies

Quick Examples

Credit note

const creditNote = await peppol.invoices.send({
  number: "CN-2026-001",
  isCreditNote: true,
  invoiceReference: "INV-2026-001",
  to: {
    name: "SPF Economie (test receiver)",
    peppolId: "9925:BE0314595348",
    street: "Rue du Progrès 50",
    city: "Brussels",
    postalCode: "1210",
    country: "BE",
  },
  lines: [
    // Positive quantities: `isCreditNote` carries the sign. Reduce a credit with an allowance.
    { description: "Refund for consulting services", quantity: 2, unitPrice: 150, ...identity.sandboxFirstSend.line },
  ],
});

Webhook verification

import { webhooks } from "@getpeppr/sdk";

app.post("/webhooks/peppol", async (req, res) => {
  try {
    const event = await webhooks.constructEvent(
      req.body,                                    // Raw body string (NOT parsed JSON)
      String(req.headers["getpeppr-signature"] ?? ""), // Getpeppr-Signature header
      "whsec_your_webhook_secret",                 // Your webhook secret
    );

    switch (event.type) {
      case "invoice.accepted":
        console.log("Invoice accepted:", event.data);
        break;
      case "invoice.received":
        console.log("New invoice received:", event.data);
        break;
    }

    res.sendStatus(200);
  } catch (err) {
    res.status(400).send("Webhook verification failed");
  }
});

Directory lookup

const entry = await peppol.directory.lookup("0208:0685660237");
console.log(entry.name);         // "ACMEDIA"
console.log(entry.country);      // "BE"
console.log(entry.capabilities); // ["invoice", "credit_note"]

Batch send

const invoices = [invoice1, invoice2, invoice3];

const result = await peppol.invoices.sendBatch(invoices, {
  concurrency: 5,
  stopOnError: false,
});

console.log(`${result.succeeded.length} sent, ${result.failed.length} failed`);

Validation

const result = peppol.validate({
  number: "INV-001",
  to: {
    name: "Acme",
    peppolId: "0208:0685660237",
    street: "123 Business Ave",
    city: "Brussels",
    postalCode: "1000",
    country: "BE",
  },
  lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }],
});

if (!result.valid) {
  for (const err of result.errors) {
    console.log(`${err.field}: ${err.message}`);
  }
}

peppol.validate() runs instantly offline and checks the structured input, including country-specific advisories. It does not run the Schematron-style registry.

Import validateSchematron(input) when you also want the 40 registered pre-flight checks. Some use exact network-rule IDs; the ten local checks use explicit GETPEPPR-* IDs. The legacy provider-capability diagnostic unsupported_vat_category can also appear, but is outside that count. This partial pass is not a Peppol conformance verdict.

For the same gateway-side checks exposed by POST /v1/validate/server, use invoices.validateServer(). It does not send the invoice to Storecove or consume billing usage.

const serverResult = await peppol.invoices.validateServer({
  number: "INV-001",
  to: {
    name: "Acme",
    peppolId: "0208:0685660237",
    street: "123 Business Ave",
    city: "Brussels",
    postalCode: "1000",
    country: "BE",
  },
  lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }],
});

console.log(serverResult.ubl.valid);
console.log(serverResult.schematron.valid);
console.log(serverResult.providerSendability); // "not_checked"

serverResult.xsd is kept as a deprecated compatibility field. It mirrors the UBL generation sanity check; the gateway does not run a standalone XSD validator. serverResult.valid covers getpeppr's offline checks only. Storecove Standard JSON validation is not called, so providerSendability: "not_checked" means a later send can still return 422.

Export PDF / XML

import { writeFileSync } from "fs";

const pdf = await peppol.invoices.getAs("inv-abc123", "pdf");
writeFileSync("invoice.pdf", Buffer.from(pdf));

const xml = await peppol.invoices.getAs("inv-abc123", "xml.ubl.invoice.bis3");
writeFileSync("invoice.xml", Buffer.from(xml));

For a short window after a send, the sending evidence is not registered yet and EVERY format answers 404 — poll rather than treating the first one as a failure. Beyond that window, pdf and the two schema-named formats can still legitimately answer 404 invoices.export_format_unavailable: the invoice exists, that representation does not. xml.ubl.invoice.bis3 resolves only when the business document is UBL, xml.facturae.3.2 only when it is Facturae; on the Peppol network the document is UBL, so the Facturae format normally has nothing to return.

⛔ Those bytes still carry the Peppol SBDH envelope: original and xml.ubl.invoice.bis3 are rooted on <sh:StandardBusinessDocument>, not <Invoice>. Use payload when you need the business document bare — to feed a schema validator, for instance.

Legal Entities (multi-tenant)

Platform customers (master key) can manage Legal Entities for their own customers and send invoices on their behalf.

This surface needs platform mode on your account. In the sandbox it is self-service: an organisation admin chooses "A platform for my customers" at signup, or starts the platform sandbox trial from the console overview, then creates the sandbox master key at console.getpeppr.dev/api-keys. Production platform access is set up with our team — email [email protected] to request it. Without a master key, every call below returns 403 master_key_required. Onboarding your own company needs none of this — that is done in the console, on the Peppol identity page.

const peppol = new Peppol({ apiKey: "sk_sandbox_..." }); // sandbox master key (trial); sk_live_ once production is set up

// Create a sub-tenant Legal Entity (idempotent on externalId)
const le = await peppol.legalEntities.create({
  externalId: "tenant-42",
  companyName: "Acme Health AB",
  country: "SE",
  address: { line1: "Storgatan 1", city: "Stockholm", zip: "11122" },
  identifier: { scheme: "0007", value: "5560000001" },
});
// Several identifiers, in order: pass `identifiers` instead. For a French customer,
// the SIREN (0002) first, then the directory address (0225). Sandbox only for now —
// see https://getpeppr.dev/docs/platform/legal-entities/

// Poll after 202. A registration failure has one stable, coarse reason:
const current = await peppol.legalEntities.get(le.id);
if (current.status === "registration_failed") {
  console.log(current.registrationDetail?.reason);
  // "already_registered" | "invalid_format" | "provider_error"
}

await peppol.legalEntities.get(le.id);
await peppol.legalEntities.list({ limit: 50 });
await peppol.legalEntities.list({ externalId: "acme-42" }); // find one customer by YOUR reference
for await (const e of peppol.legalEntities.listAll()) { /* … */ } // listAll takes every list() option except offset
await peppol.legalEntities.archive(le.id);

// Production: trigger the co-branded attestation email
await peppol.legalEntities.requestAttestation(le.id, { contactEmail: "[email protected]", language: "en" });

// Send an invoice as a sub-tenant
await peppol.invoices.send({ number: "INV-1", to, lines, sender: { externalSubTenantId: "tenant-42" } });

Gateway error codes are available via PeppolApiError.code (e.g. le_cap_exceeded, identifier_immutable, identifier_already_in_use, legal_entity_creation_in_progress, legal_entity_locked, forbidden). An exact externalId + normalized participant replay returns the existing Legal Entity; using the same participant under another active externalId returns 409 identifier_already_in_use before provider creation.

Configuration

import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({
  apiKey: "sk_live_...",       // Required. Starts with sk_sandbox_ or sk_live_
  baseUrl: "https://...",      // Default: https://api.getpeppr.dev/v1
  timeout: 30000,              // Request timeout in ms (default: 30s)
  retry: {
    maxRetries: 3,             // Max retry attempts (default: 3)
    initialDelayMs: 500,       // Initial retry delay (default: 500ms)
    maxDelayMs: 30000,         // Max retry delay (default: 30s)
  },
  onRequest: (entry) => {},    // Optional request hook
  onResponse: (entry) => {},   // Optional response hook — see note below
});

onResponse receives a copy of the response body, never the object the parsers go on to read. Mutating entry.body — redacting a field before logging it, say — therefore has no effect on what the SDK parses. Before 4.0.0 it did, and a hook that removed status made the SDK blame the gateway for an omission the hook had just committed. A body too deeply nested to clone yields a short marker string instead of the payload.

Error Handling

The SDK provides four error classes for precise error handling. All three specific ones extend PeppolError, so a single instanceof PeppolError catches every error the SDK raises itself. It does not catch errors that come from below it: a fetch that fails at the network layer surfaces as the runtime's own TypeError, and a timeout or an aborted request as an Error named AbortError — two different types, neither of them ours. A catch that must handle everything needs a final branch:

import {
  Peppol,
  PeppolError,
  PeppolValidationError,
  PeppolApiError,
  PeppolProtocolError,
} from "@getpeppr/sdk";

try {
  await peppol.invoices.send(input);
} catch (err) {
  if (err instanceof PeppolValidationError) {
    // Client-side validation failed
    console.log(err.validation.errors);   // ValidationError[]
    console.log(err.validation.warnings); // ValidationWarning[]
  } else if (err instanceof PeppolApiError) {
    // API returned an error
    console.log(err.statusCode);    // HTTP status code
    console.log(err.responseBody);  // Raw response body
    console.log(err.retryAfterMs);  // Retry-After delay (on 429)
  } else if (err instanceof PeppolProtocolError) {
    // The API answered 2xx with a body the SDK cannot parse honestly —
    // a mandatory field is missing, or the body is not an object.
    // Nothing you sent caused this; it is not worth retrying.
    console.log(err.field);         // e.g. "status"
    console.log(err.responseBody);  // the offending body, capped at 2000 chars
  } else if (err instanceof PeppolError) {
    // General SDK error (invalid config, timeout, etc.)
    console.log(err.message);
  } else {
    // Not raised by the SDK: a network-layer failure the runtime threw
    // (`TypeError: fetch failed`, DNS, TLS…). Reaching here is normal.
    console.log(err);
  }
}

Structured results

When the gateway sends its result headers, PeppolApiError surfaces them directly — no body parsing:

try {
  await peppol.invoices.send(invoice);
} catch (err) {
  if (err instanceof PeppolApiError) {
    err.resultCode;     // "auth.api_key_invalid" — stable, machine-readable
    err.resultMessage;  // the catalogue's sentence for that code
    err.requestId;      // "req_…" — quote this to support
    err.retryable;      // true | false | undefined ("we did not say")
    err.remediation;    // "fix_request" | "retry_after" | "authenticate" | …
    err.docs;           // https documentation link, when the code has one
  }
}

The same block reaches the onResponse hook as entry.result, on successes too. Each attempt gets its own copy, so a hook may inspect or redact it freely without changing what the SDK does next.

⚠️ Every one of these is undefined until the gateway publishes the headers, and behind any proxy that strips unknown ones. Write your code for their absence first: treat it as "not stated", never as a fact about the request. err.code is a different field and is unchanged — it still reads code from the response body, where routes put a request-specific sub-reason, so the two can both be present and differ.

Values that arrive malformed are dropped, not repaired: an over-long sentence, a code carrying a control character, or a docs link that is not a plain https:// URL all leave the field undefined rather than surfacing a cleaned-up guess.

Retries

Transient failures are retried with exponential backoff and jitter. Which failures is decided by the gateway's result code when it sends one, not by the status alone:

  • retryable: false is honoured even on a 500 — nine public catalogue codes are permanently fatal, and retrying those is pure waste.
  • retryable: true is honoured on statuses that were never retried before, such as the 409s raised while a concurrent request is still in flight.
  • When the gateway says nothing — an older deployment, a stripping proxy, or an unreadable value — the SDK falls back to its historic list: 429, 500, 502, 503, 504.

Retry-After is honoured on a 429; the wait is capped by maxDelayMs.

⛔ A retryable result does not, on its own, make a write safe to replay. For POST, PUT and PATCH the SDK retries only when you supplied an Idempotency-Key — with one deliberate exception: a 429 does not need the key, because a rate-limited request was rejected before it was processed, so there is no side effect to duplicate. Every other status needs the key.

⚠️ That exception lifts the REPLAY guard, not the retryability verdict above. A 429 the gateway marks Getpeppr-Retryable: false is still not retried — both questions have to answer yes.

Where you can pass one. Every operation the API contract lists Idempotency-Key on takes an idempotencyKey: invoices.send, invoices.create, invoices.sendById, invoices.acknowledge, invoices.importFile, contacts.create and bankAccounts.create. A drift lock in the gateway repository fails if the contract ever keys an operation this package cannot send a key on.

⚠️ legalEntities.create and legalEntities.requestAttestation accept one too, and the routes behind them do not read the header. The key still unlocks the SDK's retry there, so what a replay does is decided by the route, not by the key:

  • legalEntities.create — a replay of the identical request returns the existing entity, and a retry the SDK issues is identical by construction, so it never creates a second one. ⚠️ If your retry overlaps the first attempt — the usual case after a client timeout — you get 409 legal_entities.creation_in_progress instead of the entity. That code is marked retryable, so the SDK's next attempt converges on the existing entity; a hand-rolled client should treat it as "wait and retry", not as a failure. Sending the same externalId with changed details is not a replay at all, and what happens then depends on what changed: a different participant is refused outright, while a different company name is refused if the entity is live and sending, archives it and creates a new one if its verification has failed, and — if a verification is still running — returns the existing entity unchanged. Read the entity you get back rather than assuming a rename took effect.
  • legalEntities.requestAttestation — a replay is not free. Re-issuing mints a fresh token, which sends a second email to your contact and makes the link from the first one stop working. If the first attempt reached the gateway and only its response was lost, the retry costs your contact a dead link.

The key changes nothing about either: it only decides whether the SDK retries at all. Worth knowing if you are reasoning about what the header does.

The key must be one the gateway can actually use, and that is checked twice. The SDK checks first: idempotencyKey: " " is refused with a PeppolValidationError and nothing is sent. The gateway checks too — a blank key that reaches its idempotency check over plain HTTP is refused with 400 idempotency_key_blank rather than ignored, so an older SDK release, or a client that does not use this package at all, gets an answer instead of silence. That check sits behind authentication, rate limiting and the production eligibility gates, so a request that fails one of those receives that failure instead.

A transport strips the whitespace at the edges of a header value, so a blank key arrives empty and can protect nothing. If you derive keys from your own data — a padded reference, a field that can be empty — the refusal is where you find that out, instead of a duplicate invoice on the network. If you do not need idempotency for a request, omit the option rather than passing an empty string. Padding around a real key is fine: " inv-42 " is sent as inv-42, exactly what the wire would have carried anyway.

Webhook Events

Invoice events (the legal-entity and identifier events are listed in the WEBHOOK_EVENT_TYPES constant and on getpeppr.dev/docs/webhooks):

| Event | Description | |-------|-------------| | invoice.sent | Invoice sent to the Peppol network | | invoice.accepted | Recipient accepted the invoice | | invoice.refused | Recipient refused the invoice | | invoice.error | The invoice failed (final state): delivery failed, or the recipient's accredited platform rejected it after receiving it (France: Rejetée, 213). Correct it and send again; detail.platformFiscal on the invoice tells which | | invoice.registered | Cleared by the tax authority (clearance jurisdictions) | | invoice.received | Receipt acknowledged by recipient | | invoice.paid | Payment confirmed by recipient | | invoice.undeliverable | Not deliverable — no receiving capability found for the recipient on the Peppol network (the document ends in status no_action). Also sent when no delivery evidence has appeared after 7 days | | invoice.delivery_unconfirmed | No delivery evidence yet. Not a failure — if delivery is confirmed later, invoice.sent follows and supersedes it | | invoice.partially_paid | Recipient confirmed a partial payment | | invoice.under_query | Recipient raised a question about the invoice | | invoice.conditionally_accepted | Recipient accepted the invoice subject to conditions | | invoice.status_changed | Generic status notification with the full per-axis state — opt-in: never matched by the * wildcard, subscribe to it explicitly | | test.ping | Test event for endpoint verification |

Reception events, for documents sent to you over Peppol:

| Event | Description | |-------|-------------| | inbound.invoice.received | An invoice sent to you was received. Deduplicate on data.receivedDocumentId | | inbound.creditnote.received | A credit note sent to you was received. Deduplicate on data.receivedDocumentId | | inbound.document.undeliverable | A document sent to you arrived but could not be delivered to you (data.reason: too_large or legal_entity_unresolved). There is no received document to fetch. Endpoints subscribed to the reception event of the document's type receive it without subscribing to it. Deduplicate on data.undeliverableDocumentId |

Status events carry a deterministic event id derived from the underlying provider occurrence: a provider redelivery reproduces the same id, so deduplicate by event id (deliveries are at-least-once).

Every new event also carries the business environment of its resource at the root: environment: "sandbox" | "production" (GPR-1329). Read it after verifying the signature and call the API with the matching key (sk_sandbox_* / sk_live_*) — a key from the other environment gets a 404 on the event's resources. The field is optional by contract, for three reasons: events emitted before it existed are replayed verbatim without it; test.ping (a synthetic event tied to no resource) deliberately carries none; and a status event whose submission environment is not yet resolved transiently carries none either — the field appears on the next event once resolution lands, and its absence never means "production by default". Treat "no field" as "not yet known", and keep whatever key-selection fallback you already use for pre-GPR-1329 payloads.

The full list of subscribable event types is exported as the WEBHOOK_EVENT_TYPES constant (the WebhookEventType type is derived from it) — it mirrors exactly the event types the gateway accepts for webhook subscriptions.

Status Tracking

  • The SDK never invents a status. A 2xx response that carries no status raises PeppolProtocolError (with field and responseBody) instead of returning a plausible one. Before 4.0.0 the SDK substituted "submitted", which is not a terminal state — so unless you were waiting for submitted itself, waitFor() and getpeppr send --watch could only expire on it, on invoices that had already been delivered. Nothing you sent causes this error and retrying will not clear it; it is worth reporting to [email protected].
  • SendResult.createdAt is optional for the same reason: it is present when the gateway measured one, and absent rather than stamped with the moment of your call, or borrowed from updatedAt.
  • SendResult and invoice list rows carry rawStatus (the raw gateway status string before SDK coercion — preserved even when status falls back to unknown) and detail (structured national status detail, one entry per axis, with the jurisdiction's native code and label — e.g. French DGFiP lifecycle codes).
  • waitFor(id, target) throws on the terminal failures failed, rejected and no_action. If the document reaches the terminal success paid while you wait for an earlier progress status (e.g. delivered), it resolves with the real result instead of timing out — the target was passed, not missed.
  • The STATUS_PRECEDENCE table (most final first), statusFamily() and TERMINAL_FAILURE_STATUSES are exported for consumers that need the same ordering the gateway uses.

Links

License

MIT