/** * Peppol SDK Types * * Design principle: JSON-native, zero XML exposure. * A developer should never see angle brackets. */ import type { ApiResult } from "../core/api-result.js"; /** * Peppol participant identifier — `:` (e.g. `"0208:0685660237"`). * * ## Both spellings of a scheme are accepted (GPR-1110) * * The official code list publishes every scheme under TWO names: its numeric EAS * code (`iso6523`, e.g. `9932`) and its symbolic form (`schemeid`, e.g. `GB:VAT`). * They are two names for the SAME scheme, and this SDK accepts either: * * ```ts * { peppolId: "9932:123456789" } // numeric EAS code * { peppolId: "GB:VAT:123456789" } // symbolic form — same scheme * ``` * * ⭐ Accepting both is not a convenience: `GB:VAT` is what our own guidance hands * a British developer who has a VAT number, and it is handed in symbolic form. * ⚠️ It is no longer the ONLY UK code — GPR-1041 added `0060`, `0088` and `0199` * for companies under the £90,000 VAT threshold. This comment claimed exclusivity * until then, so it now states a default instead. * * ## What comes OUT is always the numeric code * * `BR-CL-25` (fatal) lists the legal values of `cbc:EndpointID/@schemeID` * literally, and every one of them is numeric — `9932` is in that list, `GB:VAT` * is not. So the builder resolves the scheme before writing it, and the same * applies to `cac:PartyIdentification/cbc:ID` under `BR-CL-10`. You never have to * do that conversion yourself, and you should not: pass the identifier as you * hold it. * * ⚠️ The `:` that separates scheme from value is the same character the symbolic * form contains, so this string cannot be split by position. If you parse it * yourself, resolve the scheme against the code list rather than cutting at the * first (or last) `:`. The rule that keeps this honest is * `core/peppol-scheme-spelling.test.ts`, which exercises both spellings of the * same schemes and asserts they produce identical XML. */ export type PeppolId = `${string}:${string}`; /** ISO 4217 currency code */ export type CurrencyCode = "EUR" | "GBP" | "SEK" | "NOK" | "DKK" | "USD" | "CHF" | (string & {}); /** ISO 3166-1 alpha-2 country code */ export type CountryCode = "BE" | "FR" | "DE" | "NL" | "IT" | "ES" | "SE" | "NO" | "DK" | "FI" | "AT" | "PT" | "PL" | "GB" | (string & {}); /** VAT rate as percentage (e.g., 21 for 21%) */ export type VatRate = number; /** * Peppol/UBL invoice type code (UNTDID 1001) — the UNION of both BR-CL-01 * vocabularies: 50 codes legal as ``, 13 as * `` (only 81 appears in both). * * ⚠️ GPR-1234 — the type is only the outer bound and cannot express the * context: 381 is legal on a credit note but fatal on an invoice, 382/326 the * reverse. Runtime validation picks the vocabulary from `isCreditNote`. * Common codes: 380 (commercial invoice, default), 381 (credit note, default * when isCreditNote), 383 (debit note), 384 (corrective invoice), * 386 (prepayment), 389 (self-billed), 751 (accounting purposes). * * Defined in `core/code-lists.ts` so the type and the validated vocabularies * can never drift apart. */ import type { InvoiceTypeCode } from "../core/code-lists.js"; export type { InvoiceTypeCode }; /** A Peppol participant entry from the directory (SMP/SML lookup) */ export interface DirectoryEntry { /** Registered business name */ name: string; /** Peppol participant ID */ peppolId: PeppolId; /** Country of registration */ country: CountryCode; /** Supported document types (e.g., ["invoice", "credit_note"]) */ capabilities: string[]; /** Date of Peppol registration (ISO 8601 date) */ registrationDate?: string; /** VAT number if available from directory */ vatNumber?: string; /** Additional identifiers (e.g., GLN, DUNS) */ additionalIds?: Array<{ scheme: string; value: string; }>; /** Contact information from directory */ contactInfo?: { name?: string; email?: string; phone?: string; }; /** Website URL */ website?: string; } /** Options for searching the Peppol Directory */ export interface DirectorySearchOptions { /** Business name to search (min 3 characters) */ name?: string; /** Country filter (ISO 3166-1 alpha-2) */ country?: CountryCode; /** VAT number to search */ vatNumber?: string; /** Max Peppol participants per page (default 20, max 100) */ limit?: number; /** Zero-based participant offset for exact pagination (default 0) */ offset?: number; } /** Paginated result from a Peppol Directory search */ export interface DirectorySearchResult { /** Matching directory entries */ data: DirectoryEntry[]; /** Pagination metadata */ meta: { /** Total unique participants matching the completed Directory result set */ totalCount: number; offset: number; limit: number; hasMore: boolean; }; } export interface Party { /** Business name */ name: string; /** Peppol participant ID (scheme:id format, e.g. "0208:0685660237") */ peppolId: PeppolId; /** VAT number (e.g., "BE0685660237") */ vatNumber?: string; /** Street address */ street?: string; /** City */ city?: string; /** Postal/zip code */ postalCode?: string; /** Country */ country: CountryCode; /** Company registration number (BT-30/BT-47), distinct from VAT */ companyId?: string; /** Company ID scheme identifier (e.g., "0208" for Belgian BCE, "0088" for EAN/GLN) */ companyIdScheme?: string; /** Contact person name (BT-41) */ contactName?: string; /** Contact telephone (BT-42) */ phone?: string; /** Contact email */ email?: string; } /** Additional item property — custom key-value pair on a line item (BG-32) */ export interface ItemProperty { /** Property name (BT-160) */ name: string; /** Property value (BT-161) */ value: string; } /** * Buyer party — postal address required by Peppol BIS 3.0 (BG-8) and Storecove. * Extends Party with mandatory street, city, and postalCode. */ export type BuyerParty = Party & { /** Street address (required for invoice recipient) */ street: string; /** City (required for invoice recipient) */ city: string; /** Postal/zip code (required for invoice recipient) */ postalCode: string; }; export interface InvoiceLine { /** Line item description */ description: string; /** Quantity */ quantity: number; /** * Unit of measure (default: "EA" = each). * Accepts UN/ECE codes (e.g., "HUR", "DAY", "KGM") or human-readable names * that are automatically resolved: "each"/"piece", "hour"/"hours", "day"/"days", * "week"/"weeks", "month"/"months", "year"/"years", "kilogram"/"kg", * "meter"/"metre", "liter"/"litre", "unit"/"units", "set"/"sets", "pack"/"packs". * Case insensitive. Unknown values pass through unchanged. */ unit?: string; /** Unit price (exclusive of tax) */ unitPrice: number; /** VAT rate in percent (e.g., 21 for 21%) */ vatRate: VatRate; /** * VAT category code (default: `"S"` = standard rate). * * **Case-sensitive.** `"AE"` is reverse charge; `"ae"` is not a category and * is refused with a 422. Omit the field to take the default — the gateway * never repairs a value you supplied. * * Sendable today: `S` `Z` `E` `AE` `K` `G` `O`. * * ⚠️ **`L` (IGIC, Canary Islands) and `M` (IPSI, Ceuta & Melilla) cannot * travel the JSON send path** — `invoices.send()` and `invoices.create()` * refuse them with a 422 (`unsupported_vat_category`), because our provider * has no vocabulary for either. * * ⚠️ That is narrower than "undeliverable", and the difference matters: both * are valid EN 16931 codes, and the official rulebook has no objection to * them — measured 2026-08-28, an IGIC document and a standard-rate one came * back from `POST /v1/validate/ubl` with the same verdict. * * ⛔ They are still in this union **deliberately**, and removing them from it * is not the same as honouring the 3.2.0 promise. This one type feeds two * different questions: what `invoices.send()` will transmit, and what the * local builders `buildInvoiceXml` / `buildCreditNoteXml` / `Peppol.toXml()` * will render. The builders apply **no provider routability gate** — that is * a decision, locked by `client-to-xml.test.ts` ("does not apply provider * routability gates to local UBL generation") — because a caller may * legitimately render an IGIC document and deliver it through another * channel. Narrowing this union takes that away to forbid something else. * Honouring the promise properly means SPLITTING the type: a send-side input * carrying the seven routable codes, and a builder-side input carrying the EN * 16931 catalogue. That is GPR-1218, and 5.0.0 deliberately did not fake it * (GPR-1212). * * ⚠️ Do not write "the next major release" here again: 3.2.0 promised exactly * that, 4.0.0 shipped without the removal, and nothing failed — a deadline * pinned to a version number has no enforcer. * * (No `@deprecated` tag: TypeScript cannot deprecate individual union members, * so tagging the property would strike through `"S"` and `"AE"` in the IDE * too — telling a developer that a field they must use is obsolete.) */ vatCategory?: "S" | "Z" | "E" | "AE" | "K" | "G" | "O" | "L" | "M"; /** * VAT exemption reason (BT-120) for `E`, `AE`, `K`, `G`, or `O`. * * `buildInvoiceXml`, `buildCreditNoteXml`, and `Peppol.toXml()` consolidate * this value into the matching VAT breakdown. It is never emitted on the * line itself, and is ignored for `S` and `Z`, where Peppol forbids it. * This is a builder-only field: `invoices.create()`, `invoices.send()`, and * `creditNotes.send()` strip it before JSON transport because the provider * derives its own exemption text. */ taxExemptReason?: string; /** Optional item identifier (seller's item number) */ itemId?: string; /** Line-level allowances / discounts (BG-27) */ allowances?: LineAllowanceCharge[]; /** Line-level charges / surcharges (BG-28) */ charges?: LineAllowanceCharge[]; /** Standard item identifier, e.g. GTIN/EAN barcode (BT-157) */ standardItemId?: string; /** Scheme ID for standard item identifier (BT-157-1). Default: "0160" for GTIN */ standardItemScheme?: string; /** Commodity classification code, e.g. UNSPSC or CPV (BT-158). @unsupported Not yet mapped by the gateway — value is accepted but silently ignored. */ commodityCode?: string; /** List ID for commodity classification (BT-158-1). e.g. "STI" for UNSPSC, "CPV" for EU procurement. @unsupported Not yet mapped by the gateway. */ commodityScheme?: string; /** Additional item properties — custom key-value pairs (BG-32) */ properties?: ItemProperty[]; /** Base quantity for price calculation (BT-149). Must be finite and greater than zero. E.g. "price per 100 units" → baseQuantity: 100 */ baseQuantity?: number; /** Unit code for base quantity (BT-150). Defaults to line's unit code. @unsupported Not yet mapped by the gateway — value is accepted but silently ignored. */ baseQuantityUnit?: string; /** * Buyer accounting reference for this line (BT-133). * Free-text cost allocation code used by the buyer for internal accounting. * Maps to `` at line level in UBL. */ accountingCost?: string; } export interface InvoiceInput { /** Your invoice number (must be unique per supplier) */ number: string; /** Invoice date (ISO 8601, defaults to today) */ date?: string; /** Due date (ISO 8601) */ dueDate?: string; /** Currency (defaults to EUR) */ currency?: CurrencyCode; /** Tax reporting currency if different from document currency (BT-6) */ taxCurrency?: CurrencyCode; /** Exchange rate: document currency → tax currency */ taxCurrencyRate?: number; /** * Seller / supplier. * @deprecated Seller is determined by your API key. This field is ignored and will be removed in v1. */ from?: Party; /** * Emit this invoice as one of your sub-tenant Legal Entities (master key only). * * A standard key that sends this field is refused with `403` — including * `null` or an empty object (GPR-1294). Omit it and the invoice is sent under * your account's own identity, as before. The gateway strips the deprecated * `from` and renders the printed supplier from the sub-tenant LE itself. */ sender?: Sender; /** Buyer / customer (address required by Peppol BIS 3.0) */ to: BuyerParty; /** Payee party (BG-10) — when payment recipient differs from seller */ payeeParty?: Party; /** * Tax representative party (BG-11) — tax agent filing VAT on behalf of seller. * `POST /v1/invoices` accepts this field but currently ignores it because the * provider mapping has no equivalent. The local UBL builder and * `POST /v1/validate/server` include it as `cac:TaxRepresentativeParty`. */ taxRepresentative?: Party; /** Line items */ lines: InvoiceLine[]; /** Additional supporting documents / attachments (BG-24), max 10 items */ attachments?: Attachment[]; /** Document-level allowances / discounts (BG-20) */ allowances?: AllowanceCharge[]; /** Document-level charges / surcharges (BG-21) */ charges?: AllowanceCharge[]; /** Billing period (BG-14) — common for SaaS/subscription invoices */ invoicePeriod?: InvoicePeriod; /** Delivery information (BG-13) — mandatory in some countries */ delivery?: Delivery; /** Optional note/memo */ note?: string; /** Payment reference (e.g., structured communication) */ paymentReference?: string; /** Buyer reference — required by Peppol BIS 3.0 if no orderReference (BT-10) */ buyerReference?: string; /** Buyer's order reference (PO number) */ orderReference?: string; /** Sales order reference issued by the seller (BT-14) */ salesOrderReference?: string; /** Contract reference number (BT-12) */ contractReference?: string; /** Project reference identifier (BT-11) */ projectReference?: string; /** Despatch advice / delivery note reference (BT-16) */ despatchReference?: string; /** Receiving advice reference (BT-15) */ receiptReference?: string; /** Free-text payment terms (e.g., "Net 30 days", "2% discount if paid within 10 days") (BT-20) */ paymentTerms?: string; /** Payment means code (30=credit transfer, 58=SEPA, etc.) */ paymentMeans?: number; /** IBAN for payment */ paymentIban?: string; /** BIC/SWIFT code */ paymentBic?: string; /** * Tax point date (BT-7) — date when VAT becomes accountable. * ISO 8601 format: YYYY-MM-DD. Omit to use invoice date. * Maps to `` in UBL. */ taxPointDate?: string; /** * Prepaid amount (BT-113) — sum already paid before this invoice. * Reduces the payable amount. Currency matches the document currency. * Maps to `` in ``. */ prepaidAmount?: number; /** * Rounding amount (BT-114) — rounding applied to the payable amount. * Must be between -0.99 and 0.99 (in document currency units). * The gateway stores this as integer cents (±99). * Maps to `` in ``. */ roundingAmount?: number; /** * Buyer accounting reference (BT-19) — cost allocation code at document level. * Free-text field used by the buyer for internal accounting. * Maps to `` at document level in UBL. */ accountingCost?: string; /** * Invoice type code (UNTDID 1001). Defaults: 380 for invoices, 381 for * credit notes (`isCreditNote: true`). * * GPR-1234 — validated against the BR-CL-01 vocabulary of the document kind: * 383 (debit note), 384 (corrective), 386 (prepayment), 389 (self-billed) and * 751 (accounting) are legal on invoices only; a credit note accepts a * distinct 13-code vocabulary. On the send path (`POST /v1/invoices`) this * field is currently inert — the document kind is derived from * `isCreditNote` — it shapes the UBL built for validation * (`POST /v1/validate/server`). That local UBL uses Peppol billing profile * `01`: P0100/P0101 narrow the invoice/credit-note vocabularies for that * profile without removing globally legal values from this type. P0112 also * reserves codes 326 and 384 for documents whose buyer and seller are both * German organizations. */ invoiceTypeCode?: InvoiceTypeCode; /** * French e-invoicing reform (Facturation Électronique) declaration. * * **Declaring this block IS the declaration of the French regulated régime.** * The Peppol France Solution Architecture 1.3.2 §4.1 puts the declaration on * the sender: "C2 is then aware if the invoice is regulated or not (because * C1 have declared it, or because of a business rule)". The régime is never * inferred from `to.country` — a French buyer on a plain Peppol send stays a * plain Peppol send. * * The gateway verifies that the identity actually issuing the document is * registered for the French flow, and refuses the send before any regulated * submission when it is not. */ france?: FranceDeclaration; /** Set to true for credit notes */ isCreditNote?: boolean; /** Reference to the original invoice being credited (required when isCreditNote is true) */ invoiceReference?: string; } /** * Cadre de Facturation — the French invoicing context, AFNOR XP Z12-012. * * ⛔ The vocabulary is the provider contract's, read from * `Invoice.frCadreDeFacturation` in the live Storecove OpenAPI (sha256 * `6ed2b79b…`, 2026-09-21), not transcribed from prose. */ export type FrCadreDeFacturation = "B1" | "S1" | "M1" | "B2" | "S2" | "M2" | "S3" | "B4" | "S4" | "M4" | "S5" | "S6" | "B7" | "S7" | "B8" | "S8" | "M8"; /** * The three legal mentions a French invoice must carry (BT-22, in the BG-3 * notes) — `BR-FR-05` of the FNFE-MPE socle, `fatal`. * * ⭐ Supply the **sentence only**: getpeppr writes the normative marker * (`#PMT#`, `#PMD#`, `#AAB#`) in front of it, and `BR-FR-06` refuses a marker * that appears twice. * * ⚠️ Override what your terms of sale actually say. The defaults are the * statutory fallbacks — a €40 recovery indemnity, which is identical for every * French issuer, and three times the legal interest rate with no early-payment * discount, which is only what the law applies when a contract is silent. * * ## Which channel is yours * * ⭐ **Sending your own invoices?** Save these once on your legal entity in the * getpeppr console and omit this block. Repeating them on every call is noise, * and a change of terms would then mean a change of code. * * ⭐ **Sending on behalf of your customers?** This block IS your channel. * Arbitrage Ironman, 2026-09-21: a platform's customers' terms of sale live in * the PLATFORM's own system, and our job is to give it a way to transmit them — * not to store them. We keep no per-customer terms for a platform, and no * screen exists where they could be entered. * * Whatever you send here wins over anything saved, **mention by mention**: an * invoice can override one sentence without disturbing the other two. */ export interface FranceLegalMentions { /** `#PMT#` — flat recovery indemnity. Default: 40 euros, article D441-5. */ recoveryCosts?: string; /** `#PMD#` — late-payment penalties. Default: three times the legal rate. */ latePaymentPenalties?: string; /** `#AAB#` — early-payment discount, or the statement that there is none. */ earlyPaymentDiscount?: string; } /** French regulated-flow declaration (GPR-1358). */ export interface FranceDeclaration { /** * Cadre de Facturation (BT-122), the use case this document falls under. * * ⛔ In the UBL this SDK builds it is carried in BG-24 * (`cac:AdditionalDocumentReference`), **not** in the ProfileID: under the * Peppol BIS customization a ProfileID outside * `urn:fdc:peppol.eu:2017:poacc:billing:NN:1.0` is a `fatal` * `PEPPOL-EN16931-R007` violation, measured 2026-09-21 * (`docs/research/2026-09-21-gpr-1358-bt24-isolation/`, shot V2). * * ⛔⭐ **That placement is a Peppol BIS compromise, and the French socle * refuses it.** Measured the same day against FNFE-MPE 1.4.0 fix04: * `BR-FR-08/BT-23` demands the cadre in `cbc:ProfileID` (`fatal`), and * `BR-FR-17/BT-123` rejects the BG-24 attachment code (`fatal`). The two * requirements cannot both hold, so **the cadre is not expressible under * Peppol BIS at all** — it presupposes the French CIUS, where `S1` in BT-23 * is legitimate. * * ⚠️ This does not make the field wrong, because **the document that travels * is not this one**. On the regulated route the gateway forwards the value as * `Invoice.frCadreDeFacturation` and Storecove emits the French CIUS, which * was measured passing `BR-FR-08`. The UBL built here is a local artefact for * preview and validation. See * `docs/research/2026-09-21-gpr-1358-fnfe-offline-validation/`. */ cadreDeFacturation: FrCadreDeFacturation; /** * Replace any of the three mandatory legal mentions with the issuer's own * wording. * * Precedence is resolved one mention at a time, never as a block: this * invoice field wins, then the mention saved on the issuing legal entity, * then the statutory default. ⚠️ An entry that is omitted, empty or blank * counts as ABSENT and falls through to the NEXT level — which is the legal * entity's saved mention when one exists, and only then the statutory text. * Sending `""` does not restore the default. * * ⛔ Each entry must be a string. `null` — or any other type a JSON body can * carry — is refused with `400 invoices.france_legal_mentions_invalid`; omit * the field instead. This type says `string` and the API agrees with it. */ legalMentions?: FranceLegalMentions; } /** Additional supporting document (BG-24) */ export interface Attachment { /** Document reference identifier (required) */ id: string; /** Human-readable description */ description?: string; /** Filename (required when content is provided) */ filename?: string; /** MIME type (e.g., "application/pdf") — required when content is provided */ mimeType?: string; /** Raw base64-encoded file content (for embedded attachments, no data URI prefix); server validation caps decoded content at 2 MB */ content?: string; /** * External URL reference. * * ⛔ **Local builders only.** `buildInvoiceXml` writes it as * `cac:ExternalReference/cbc:URI`, but `invoices.send()` REFUSES any non-empty * `url` with `invoices.attachment_url_not_supported` — including alongside * `content`. The document that travels is written by our provider, whose * contract has no field for a link, so an accepted `url` would be discarded in * silence and the preview would disagree with what was sent. * * To attach a file on a send, give `content`, `mimeType` and `filename`. */ url?: string; } /** Document-level allowance or charge (BG-20/BG-21) */ export interface AllowanceCharge { /** Reason for the allowance/charge */ reason: string; /** Amount (positive number, exclusive of tax) */ amount: number; /** VAT rate in percent */ vatRate: VatRate; /** * VAT category code (default: `"S"`). **Case-sensitive** — see * {@link InvoiceLine.vatCategory}. * * Sendable today: `S` `Z` `E` `AE` `K` `G` `O`. * * ⚠️ **`L` and `M` are not routable** and are refused with a 422 on send, but * the local UBL builders still render them on purpose — see * {@link InvoiceLine.vatCategory} for why narrowing this union is not the way * to honour the 3.2.0 promise (GPR-1218). (No `@deprecated` tag — it would * strike through the whole property, valid codes included.) */ vatCategory?: "S" | "Z" | "E" | "AE" | "K" | "G" | "O" | "L" | "M"; /** * VAT exemption reason (BT-120) for the matching VAT breakdown. * Builder-only; stripped from gateway transports. See * {@link InvoiceLine.taxExemptReason}. */ taxExemptReason?: string; } /** Line-level allowance or charge (BG-27/BG-28) */ export interface LineAllowanceCharge { /** Reason for the allowance/charge */ reason: string; /** Amount (positive number) */ amount: number; } /** Billing period (BG-14) — common for SaaS/subscription invoices */ export interface InvoicePeriod { /** Period start date (ISO 8601) (BT-73) */ startDate?: string; /** Period end date (ISO 8601) (BT-74) */ endDate?: string; } export interface DeliveryAddress { street?: string; city?: string; postalCode?: string; country: CountryCode; } export interface Delivery { /** Actual delivery date (ISO 8601) (BT-72) */ date?: string; /** Delivery location identifier (BT-71) */ locationId?: string; /** Delivery address (BG-15) */ address?: DeliveryAddress; } /** @deprecated Use InvoiceInput with isCreditNote: true instead */ export interface CreditNoteInput extends Omit { /** Credit note number */ number: string; /** Reference to the original invoice being credited */ invoiceReference: string; } export interface PaginationMeta { totalCount: number; offset: number; limit: number; hasMore: boolean; /** * Always `false` through this SDK today: every gateway list route emits * exactly `total_count`, `offset`, `limit` and `has_more` (measured * GPR-1270 — no route emits `truncated`), and the client derives this * field from the wire rather than fabricating it. Kept rather than * removed so a future gateway that starts emitting it surfaces here * without a breaking release. */ truncated: boolean; } export interface PaginatedResult { data: T[]; meta: PaginationMeta; } /** Options for `invoices.getStatus()` (GPR-1061). */ export interface GetStatusOptions { /** * Ask the gateway to read the sending evidence from the Peppol network, which * is what makes `peppolMessageId` available. Off by default: it costs the * gateway a provider round trip. Degrades silently — a document that has not * gone out yet comes back without the field, never with an error. */ includeEvidence?: boolean; } export interface ListInvoicesOptions { limit?: number; offset?: number; /** Exact, case-sensitive invoice number filter. */ number?: string; /** Include line details */ includeLines?: boolean; } export interface InvoiceSummary { id: string; number: string; status: DocumentStatus; /** * The raw pre-coercion status string the gateway sent. * * Required since 4.0.0 (GPR-1061). It was optional because the SDK omitted * it when the wire carried no status — the case that is now refused, so * there is nothing left to omit. `status` is this value coerced to the SDK * vocabulary; compare them to detect a gateway status we cannot map. */ rawStatus: string; /** Layer-2 structured national detail, when the gateway exposes it. */ detail?: StatusDetail; /** Same value as `id` on this surface, under a name that cannot be misread. */ submissionId?: string; /** * The provider (Storecove) document GUID — the bridge from a list row to any * `invoices/{id}` call, and to your own provider-side traces (GPR-1062). The * gateway has always returned it; this parser used to drop it. */ providerDocumentId?: string; createdAt?: string; /** Whether this document is a credit note rather than an invoice. */ isCreditNote?: boolean; /** Recipient (buyer) company name, if recorded at send time. */ recipientName?: string; /** * BT-112 grand total including tax, in the minor units of `currency` * (EUR cents, JPY whole units, BHD fils). Includes line/document allowances * and charges plus VAT; prepaid amount and payable rounding affect the * separate BT-115 amount due, not this display total. `undefined` when the * gateway has no recordable amount. */ totalAmount?: number; /** ISO 4217 currency code (e.g. "EUR"). */ currency?: string; /** Environment the document was sent in ("sandbox" or "production"). */ environment?: string; } export type DocumentStatus = "submitted" | "delivered" | "accepted" | "rejected" | "paid" | "failed" | "cleared" | "acknowledged" | "in_process" | "under_query" | "conditionally_accepted" | "partially_paid" | "no_action" | "unknown"; /** * Layer-2 structured national detail (multi-jurisdiction status model, additive). * One entry per axis; the native code is preserved, never over-translated. * NOTE: no `message` field by design — provider free text never reaches the * public surface (GDPR short-retention, scrubbed at the source). */ export interface StatusDetailEntry { axis: "platformFiscal" | "delivery" | "businessDisposition" | "settlement" | "unmapped"; jurisdiction: string; /** Native code (e.g. "213") — NOT the label. */ code: string; /** Human label (e.g. "Rejetée"). */ label: string; codeSystem: string; codeVersion: string; standardCode?: { system: string; code: string; }; reason?: string; warnings?: string[]; failureCategory?: "transport" | "routing" | "syntax" | "semantic" | "authority"; payment?: { amount: number; currency: string; date: string; }; paymentSemantics?: "received" | "initiated"; actor?: "seller" | "buyer"; } /** Per-axis map of the latest structured detail. All keys optional. */ export interface StatusDetail { platformFiscal?: StatusDetailEntry; delivery?: StatusDetailEntry; businessDisposition?: StatusDetailEntry; settlement?: StatusDetailEntry; } /** * Valid states for the markAs transition API. * Subset of states that can be set manually via PUT /invoices/{id}/mark-as. */ export type MarkAsState = "draft" | "sending" | "sent" | "received" | "accepted" | "paid" | "refused" | "cancelled" | "corrected"; /** Options for the markAs state transition. */ export interface MarkAsOptions { /** Send an email notification when transitioning (e.g., payment confirmation). */ commit?: "with_mail"; /** Free-text reason for the transition (e.g., refusal reason). */ reason?: string; } /** * Legacy request shape for updating an invoice. * The current Storecove-backed gateway returns 501 because submitted documents * are immutable; this type remains for source compatibility. * Partial — only include fields you want to change when using a future provider. * `contact` is readOnly on update — use the to/from fields from the original. * Lines support `id` (modify existing), `_destroy: true` (delete), or no id (add new). */ export interface InvoiceUpdateInput { number?: string; issueDate?: string; dueDate?: string; currency?: CurrencyCode; note?: string; buyerReference?: string; orderReference?: string; paymentReference?: string; paymentTerms?: string; lines?: InvoiceUpdateLine[]; } /** A line in an invoice update — can modify, delete, or add lines. */ export interface InvoiceUpdateLine { /** Line ID — required for modifying or deleting existing lines. */ id?: string; /** Set to true to delete this line (requires `id`). */ _destroy?: boolean; description?: string; quantity?: number; unitPrice?: number; unit?: string; vatRate?: number; vatCategory?: string; itemId?: string; } export interface SendResult { /** Unique document ID */ id: string; /** Current status */ status: DocumentStatus; /** * The raw pre-coercion status string the gateway sent. * * Required since 4.0.0 (GPR-1061). It was optional because the SDK omitted * it when the wire carried no status — the case that is now refused, so * there is nothing left to omit. `status` is this value coerced to the SDK * vocabulary; compare them to detect a gateway status we cannot map. */ rawStatus: string; /** Layer-2 structured national detail, when the gateway exposes it. */ detail?: StatusDetail; /** * The getpeppr submission id — the local record of this document (GPR-1061). * * `id` names two different things depending on which surface answered * (`POST /invoices` and `GET /invoices/{id}` return the provider GUID; * `GET /invoices` returns this one), which is exactly how an integrator's * `getStatus(row.id)` earned a 404. Both are accepted as input on every `/v1` * endpoint that takes an invoice id — the `/v1/invoices/{id}` routes and the * `invoiceId`/`documentId` filter on `/v1/events` — so either name works. * * ⚠️ Two narrow exceptions, both of which answer explicitly rather than * silently: an id that would name TWO of your documents — one by its provider * GUID, another by its submission id — answers `404` instead of guessing; and * on the idempotent routes, replaying ONE `Idempotency-Key` across the two * names of one invoice reads as two operations, so it answers * `422 idempotency_key_reuse` rather than the cached body. * * Only these two names say which one you are holding. * * ⚠️ This sentence said "everywhere" until 2026-09-07, and it was FALSE: only * `GET /v1/invoices/{id}` translated. The six other doors on that path answered * 404 on a local id, so listing then exporting — the natural sequence — was * refused on an invoice the caller had just been shown (GPR-1289). * * Absent when the gateway did not send it, or sent something that is not a * non-empty string. Never coerced: `String(42)` would look like an id and * resolve to nothing. */ submissionId?: string; /** The provider (Storecove) document GUID. Same absence rule as above. */ providerDocumentId?: string; /** * The provider document GUID of an EARLIER send of this same document — same * sender, same recipient, same invoice number (GPR-1092). * * ⚠️ Its presence means a SECOND copy has been accepted for transmission — * not that it has been delivered. This field rides on the `201`, before any * transmission evidence exists; read delivery from the document's status, as * you would for any send. Peppol does not de-duplicate, and past a short * window the gateway cannot tell a late retry apart from a deliberate resend, * so it sends and tells you rather than guessing. Absent on a first send. * * Within that window the gateway refuses instead, with HTTP 409 * `duplicate_document` — a `PeppolApiError` whose `statusCode` is 409. * * ⚠️ Applies to BOTH send methods — `send()` and `importInvoice()` (GPR-1105). * On an import the invoice number and the credit-note flag are read from the * document you supplied, and the recipient is the one you declared. Until * gateway release 2026-08-20 the import route produced this field on no * response at all, so its absence there meant nothing; if you are integrating * against an older gateway, do not read a missing value as "no duplicate". */ duplicateOf?: string; /** * The Peppol AS4 message id, once the document has actually gone out. * * ⚠️ `GET /invoices/{id}` only carries it when called with * `?include=evidence` — it costs the gateway a network read, so it is opt-in. */ peppolMessageId?: string; /** Generated UBL XML (for debugging) */ ublXml?: string; /** Validation warnings (non-blocking) */ warnings?: ValidationWarning[]; /** * Creation timestamp, as measured by the gateway. * * Optional since 4.0.0 (GPR-1061): the SDK used to stamp the moment of the * call when the response carried none, which is indistinguishable from a real * measurement. Absent now stays absent. */ createdAt?: string; /** * The Peppol rulebook the gateway judged this document against, when it * judged one. * * ⛔ GPR-1069 — present ONLY on a raw-UBL import that actually ran the * validation gate. Absent means one of three things, and they are NOT * interchangeable: this was a JSON send (no UBL to judge), the caller passed * `x-skip-validation`, or the gateway predates the gate. **Absent is never a * pass** — never read a missing `rulebook` as "it was checked and it was * fine". * * Why it matters, verbatim from the integrator who asked for it: "Tell me * which rulebook version you validated against. That's the part of your * validator I actually value — not that it protects me from myself, but that * it tells me when I've fallen behind." */ rulebook?: { peppol: string; verifiedAt: string; }; /** * How the bytes that left getpeppr relate to the bytes you supplied. * * Present ONLY on a raw-UBL import (`importInvoice`) — a JSON send has no * bytes of yours to preserve, so the question does not arise and the field is * absent rather than false. * * ⚠️ **Absent is not `false`, and not a guarantee either.** Three ways it can * be missing: a JSON send (you supplied no bytes), a gateway deployed before * this field existed (this SDK talks to whichever version you point it at), * and a replayed `Idempotency-Key` whose cached body predates the field. * Absent means the response did not say. * * ⛔ GPR-1089 — this is a GUARANTEE, not a measurement of your document. * `bytePreservation: "not_guaranteed"` does NOT mean "we changed it": getpeppr * forwards your bytes verbatim and takes no decision from their content. It * means the Peppol network re-serialises in transit, so byte equality is * never something you may rely on — measured 2026-08-18: namespace * declarations reordered, numeric character references resolved, whitespace * inside tags dropped, no element or value altered. * * ⛔ Do NOT read this as "canonicalise first and my bytes will survive". A * canonical document has been observed to come back unchanged **once**, on a * document the provider had itself generated — the very measurement that * misled us before. An idempotent normaliser is not a contract. * * In one line: byte-for-byte equality is not guaranteed, and the remedy is to * seal a canonical form (C14N) rather than raw bytes. * * Practical consequence, and the reason the field exists: **seal a canonical * form (C14N), not raw bytes.** A digest over the bytes you sent attests to * YOUR record, not to what was transmitted. * * `mode` says whose rendering went out. Today the gateway sends * `"enveloped"` — yours, forwarded, not one we generated. * * ⚠️ Both fields are typed `string`, not a literal union, and that is * deliberate. A union would force this SDK to DROP any value it predates — * and a dropped object is an ABSENT `transmission`, which a caller reads as * "a JSON send, none of my bytes involved". For a customer who seals * documents that reading is both wrong and dangerous, so an unknown value * reaches you intact instead. Compare against the values you know; do not * assume the set is closed. Same reasoning as `rawStatus`. */ transmission?: { /** Currently `"enveloped"`. Treat unknown values as "not a JSON send". */ mode: string; /** Currently `"not_guaranteed"`. */ bytePreservation: string; }; } export interface ReceivedInvoice { /** Document ID */ id: string; /** Parsed invoice data (same structure as InvoiceInput) */ invoice: InvoiceInput; /** Raw UBL XML */ rawXml: string; /** Peppol sender ID */ senderId: PeppolId; /** Reception timestamp */ receivedAt: string; } export interface ValidationWarning { /** Field path (e.g., "lines[0].vatRate") */ field: string; /** Warning message */ message: string; /** Stable Peppol or getpeppr rule ID (e.g., "BR-CO-26") */ ruleId?: string; } export interface ValidationError { /** Field path */ field: string; /** Error message (human-readable, actionable) */ message: string; /** Stable Peppol or getpeppr rule ID */ ruleId?: string; /** Suggested fix */ suggestion?: string; } export interface ValidationResult { valid: boolean; errors: ValidationError[]; warnings: ValidationWarning[]; } /** * Available document export formats (GET /invoices/{id}/as/{format}). * * ⚠️ The two `xml.*` members name a SCHEMA, and the endpoint serves each only * when the business document is written in it: on the Peppol network that * document is UBL, so `xml.facturae.3.2` normally answers 404. `original` and * `payload` name no schema, so they resolve on any XML on file. * * ⚠️ "On file" is the qualifier that matters: for a short window after a send, * the sending evidence is not registered yet and EVERY format — `original` * included — answers 404. Poll rather than treating the first one as failure. * * ⛔ The schema check reads THROUGH the Peppol SBDH envelope; it does not * remove it. `original` and `xml.ubl.invoice.bis3` return the transmitted * envelope, rooted on ``. Only `payload` returns * the business document bare, so it is the one to feed a schema validator. */ export type DocumentFormat = "pdf" /** * Served only when the business document is UBL — envelope included, same * bytes as `original`. */ | "xml.ubl.invoice.bis3" /** Served only when the stored document is Facturae — see the note above. */ | "xml.facturae.3.2" | "original" /** * The business document with the Peppol SBDH envelope stripped (served * since GPR-1053), for callers who supplied their own UBL and want to * inspect what was transmitted. It is the document AS TRANSMITTED, not * the caller's own bytes: the network re-serialises in transit, so seal * a canonical form (C14N) if you need to compare across the boundary. */ | "payload"; /** Individual validation message from server-side gateway validation */ export interface ServerValidationMessage { /** Severity: "error" or "warning" */ severity: "error" | "warning"; /** Validation message */ message: string; /** Location in the XML document (XPath or line reference) */ location?: string; /** Rule ID (e.g., Schematron rule) */ ruleId?: string; } /** Server-side validation result with SDK, UBL build, and partial offline pre-flight checks */ export interface ServerValidationResult { /** * Result of getpeppr's offline checks only. This is not proof that the * provider will accept a later Standard JSON submission. */ valid: boolean; /** SDK-level validation errors from validateInvoice */ errors: ValidationError[]; /** SDK-level validation warnings from validateInvoice */ warnings: ValidationWarning[]; /** UBL XML generation sanity-check result */ ubl: { valid: boolean; errors: ServerValidationMessage[]; }; /** * @deprecated Compatibility field. `valid` mirrors `ubl.valid`; the gateway * does not run a standalone XSD validator. */ xsd: { valid: boolean; errors: ServerValidationMessage[]; note?: string; }; /** Results from the registered partial offline checks */ schematron: { valid: boolean; /** Offline checks executed; zero when UBL generation fails before validation. */ coverage: { rulesChecked: number; /** These checks cover only part of the network's fatal rules. */ ofNetworkFatalRules: "partial"; }; errors: ServerValidationMessage[]; warnings: ServerValidationMessage[]; }; /** * Gateway-level country-rule findings (GPR-872) — checks that need the * account's registered Peppol identity, which client-side validators cannot * see. A non-empty array means `POST /v1/invoices` would reject the same * payload with a gateway-owned 422 carrying the same `code`. Never affects * `valid` and does not include provider validation. * Optional: only present on gateway versions that implement it. */ countryRules?: Array<{ /** Official Peppol rule id (e.g. "NL-R-003") */ code: string; /** Actionable explanation of the violated rule */ message: string; /** Official Peppol documentation page for the rule */ docs: string; }>; /** * Storecove Standard JSON validation is not run by `/validate/server`. * `not_checked` means a later `POST /v1/invoices` can still return 422. */ providerSendability: "not_checked"; } /** An event entry from the getpeppr usage event feed (`GET /v1/events`) */ export interface EventEntry { /** Unique event ID */ id: string; /** Event type (e.g., "invoice.sent", "inbound.invoice.received") */ eventType: string; /** Associated document ID, when the event relates to a specific document */ documentId: string | null; /** Event-specific metadata (shape varies by event type) */ metadata: Record | null; /** Timestamp of the event (ISO 8601) */ createdAt: string; } /** Options for listing events */ export interface ListEventsOptions { /** Maximum number of events to return */ limit?: number; /** Offset for pagination */ offset?: number; /** Filter events by provider document ID or getpeppr submission ID. */ documentId?: string; /** * Legacy alias for `documentId`. * @deprecated Use `documentId` for consistency with `EventEntry`. */ invoiceId?: string; /** Filter events from this date (ISO 8601) */ dateFrom?: string; /** Filter events until this date (ISO 8601) */ dateTo?: string; } /** A contact in the address book (client/provider) */ export interface Contact { /** Unique contact ID */ id: string; /** Business name */ name: string; /** Peppol participant ID (scheme:id format, e.g. "0208:0685660237") */ peppolId?: string; /** Whether this contact is verified on the Peppol Directory */ directoryVerified?: boolean; /** Timestamp of the last Peppol Directory verification (ISO 8601) */ directoryLastChecked?: string; /** VAT number (e.g., "BE0685660237") */ vatNumber?: string; /** Company registration number */ companyId?: string; /** Street address */ street?: string; /** City */ city?: string; /** Postal/zip code */ postalCode?: string; /** Country (ISO 3166-1 alpha-2) */ country?: string; /** Email address */ email?: string; /** Phone number */ phone?: string; /** Whether this contact is a client */ isClient?: boolean; /** Whether this contact is a provider/supplier */ isProvider?: boolean; /** Creation timestamp (ISO 8601) */ createdAt?: string; /** Last update timestamp (ISO 8601) */ updatedAt?: string; } /** Input for creating or updating a contact */ export interface ContactInput { /** Business name (required) */ name: string; /** Peppol participant ID (scheme:id format) */ peppolId?: string; /** VAT number */ vatNumber?: string; /** Company registration number */ companyId?: string; /** Street address */ street?: string; /** City */ city?: string; /** Postal/zip code */ postalCode?: string; /** Country (ISO 3166-1 alpha-2) */ country?: string; /** Email address */ email?: string; /** Phone number */ phone?: string; /** Whether this contact is a client (default: true) */ isClient?: boolean; /** Whether this contact is a provider/supplier (default: false) */ isProvider?: boolean; } /** Options for listing contacts with filtering and pagination */ export interface ListContactsOptions { /** Maximum number of contacts to return */ limit?: number; /** Offset for pagination */ offset?: number; /** Search by name, address, or VAT code */ name?: string; /** Filter by client status */ isClient?: boolean; /** Filter by provider status */ isProvider?: boolean; } /** A bank account linked to the account */ export interface BankAccount { /** Unique bank account ID */ id: string; /** Display name for this bank account */ name: string; /** Account type: IBAN-based or raw account number */ type: "iban" | "number"; /** IBAN (required when type is "iban") */ iban?: string; /** Account number (required when type is "number") */ number?: string; /** BIC/SWIFT code */ bic?: string; /** Country (ISO 3166-1 alpha-2) */ country?: string; /** Creation timestamp (ISO 8601) */ createdAt?: string; /** Last update timestamp (ISO 8601) */ updatedAt?: string; } /** Input for creating or updating a bank account */ export interface BankAccountInput { /** Display name for this bank account (required) */ name: string; /** Account type (default: "iban") */ type?: "iban" | "number"; /** IBAN (required when type is "iban") */ iban?: string; /** Account number (required when type is "number") */ number?: string; /** BIC/SWIFT code */ bic?: string; /** Country (ISO 3166-1 alpha-2) */ country?: string; } /** Options for listing bank accounts with pagination */ export interface ListBankAccountsOptions { /** Maximum number of bank accounts to return */ limit?: number; /** Offset for pagination */ offset?: number; } /** A transport type available in the Peppol network (e.g., Peppol BIS 3.0, FatturaE) */ export interface TransportType { /** Transport type code (e.g., "peppol", "fatturae") */ code: string; /** Human-readable name */ name: string; } /** A configured transport for the account */ export interface Transport { /** Unique transport ID */ id: string; /** Code of the transport type (references TransportType.code) */ transportTypeCode: string; /** Human-readable name */ name: string; /** Transport status (e.g., "active", "inactive") */ status?: string; } /** Input for creating a transport */ export interface TransportInput { /** Transport type code (required — references TransportType.code) */ transportTypeCode: string; /** Notification email for this transport */ email?: string; /** Additional provider-specific fields */ [key: string]: unknown; } /** Input for updating a transport */ export interface TransportUpdateInput { /** Notification email for this transport */ email?: string; /** Additional provider-specific fields */ [key: string]: unknown; } /** * Event types the gateway accepts for webhook subscriptions — mirrors the * gateway's `VALID_EVENT_TYPES` SSoT minus the `"*"` wildcard. Locked by a * console↔SDK drift test (GPR-868); update both sides together. */ export declare const WEBHOOK_EVENT_TYPES: readonly ["invoice.sent", "invoice.accepted", "invoice.refused", "invoice.error", "invoice.registered", "invoice.paid", "invoice.received", "invoice.undeliverable", "invoice.delivery_unconfirmed", "invoice.partially_paid", "invoice.under_query", "invoice.conditionally_accepted", "invoice.status_changed", "legal_entity.registered", "legal_entity.unsupported_scheme", "legal_entity.verification_failed", "legal_entity.awaiting_authz", "legal_entity.registration_failed", "peppol_identifier.verified", "peppol_identifier.verification_failed", "inbound.invoice.received", "inbound.creditnote.received", "inbound.document.undeliverable", "test.ping"]; export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number]; /** * The business environment a webhook event occurred in (GPR-1329). * * Read it AFTER verifying the signature, then use the matching API key: * `sk_sandbox_*` for `"sandbox"`, `sk_live_*` for `"production"`. */ export type WebhookEnvironment = "sandbox" | "production"; export interface WebhookEvent { /** Event ID */ id: string; /** Event type */ type: WebhookEventType; /** * Business environment of the occurrence (GPR-1329). OPTIONAL and it is a * contract, not a gap: events emitted before this field existed are still * replayed verbatim from the delivery outbox, and `test.ping` (a synthetic * dashboard event tied to no resource) deliberately carries none. When * present the value is exact — it is never guessed or defaulted. */ environment?: WebhookEnvironment; /** Event payload */ data: T; /** Timestamp */ createdAt: string; } export interface RetryConfig { /** Maximum number of retries (default: 3) */ maxRetries?: number; /** Initial delay in ms before first retry (default: 500) */ initialDelayMs?: number; /** Maximum delay in ms between retries (default: 30000) */ maxDelayMs?: number; } export interface WaitForOptions { /** Timeout in ms (default: 120000 = 2 min) */ timeout?: number; /** Polling interval in ms (default: 5000 = 5 sec) */ interval?: number; } /** Log entry for an outgoing HTTP request */ export interface RequestLogEntry { method: string; url: string; headers: Record; body?: unknown; timestamp: number; } /** * Log entry for an incoming HTTP response. * * Fires once per ATTEMPT, so a retried call produces several — including on a * `204`, on an error response, and on a 2xx whose body is not JSON. * * ⛔ It does NOT fire on a network failure (DNS, refused connection, abort): * nothing arrived, and synthesising an entry would mean inventing a status. * Catch the rejected promise for those. */ export interface ResponseLogEntry { status: number; headers: Record; body: unknown; durationMs: number; timestamp: number; /** * The canonical getpeppr result declared by the response headers (GPR-1178). * * `undefined` against a gateway that has not activated the result catalogue, * and behind any hop that strips unknown headers — so treat its absence as * "not stated", never as a property of the request. */ result?: ApiResult; } /** Options for invoice operations (send, create) */ export interface InvoiceOperationOptions { /** * Idempotency key to prevent duplicate operations (max 256 chars, 24h TTL). * * Supplying it also enables the SDK's automatic retry of transient failures. * Without it, POSTs are not retried — 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: a * `429` the gateway marks `Getpeppr-Retryable: false` is still not retried. * Both questions must answer yes. * * ⛔ It must be a key the gateway can use. A blank one (`""`, `" "`) is * refused with a `PeppolValidationError` before anything is sent: HTTP strips * whitespace from the edges of a header value, so it would arrive empty, the * gateway would find no key to look up, and the retry it appeared to * authorise would submit the document twice. Surrounding whitespace on a real * key is harmless — `" inv-42 "` is sent as `inv-42`. */ idempotencyKey?: string; /** * Validate recipient exists on the Peppol network before sending. * - `true` or `"warn"`: adds a warning to `result.warnings[]` if recipient not found (default behavior) * - `"strict"`: rejects the request with 422 if recipient not found * - `false` or omitted: no validation */ validateRecipient?: boolean | "warn" | "strict"; } /** * Per-request options for a write the gateway keys by `Idempotency-Key`. * * Carried by every operation the API contract lists that header on, so a caller * can retry a lost response without executing the write twice. Before GPR-1189 * the contract declared it on six operations and this SDK accepted it on one — * the public docs told developers to send the header, and no user of * `@getpeppr/sdk` could. * * ⚠️ Supplying a key also unlocks the SDK's automatic retry of transient * failures on that call. Without one, POSTs are not retried — with a single * deliberate exception: a `429` needs no key, because a rate-limited request was * refused before it was processed, so there is nothing to duplicate. That * exception lifts the REPLAY guard, not the retryability verdict: a response the * gateway marks `Getpeppr-Retryable: false` is not retried whatever its status, * and a retryable `409` is retried when a key is present. * * ⛔ The key must be one the gateway can use. A blank one (`""`, `" "`) is * refused with a `PeppolValidationError` before anything leaves the process: * HTTP strips whitespace from the edges of a header value, so it would arrive * empty, the gateway would find no key to look up, and the retry it appeared to * authorise would execute the write a second time. Whitespace around a real key * is harmless — `" ack-42 "` is sent as `ack-42`. */ export interface IdempotentRequestOptions { /** Idempotency key for this request (max 256 chars, 24h TTL). */ idempotencyKey?: string; } export interface PeppolConfig { /** getpeppr API key (starts with sk_sandbox_ or sk_live_) */ apiKey: string; /** Environment (default: "sandbox") — no longer affects base URL */ environment?: "sandbox" | "production"; /** Base URL override (default: https://api.getpeppr.dev/v1) */ baseUrl?: string; /** Request timeout in ms (default: 30000) */ timeout?: number; /** Retry configuration for transient errors */ retry?: RetryConfig; /** Hook called before each HTTP request (for logging/debugging) */ onRequest?: (entry: RequestLogEntry) => void; /** Hook called after each HTTP response (for logging/debugging) */ onResponse?: (entry: ResponseLogEntry) => void; } /** * Options for importing an invoice you generated yourself. * * getpeppr does not regenerate, normalise or repair the document — we forward * the bytes you supplied, unchanged, to the network. Accepts a UBL Invoice or * CreditNote. * * ⚠️ Byte-for-byte equality is NOT guaranteed, because the network * re-serialises the document in transit. Measured 2026-08-18 on a test * document: namespace declarations come back reordered, numeric character * references are resolved (`A` → `A`), whitespace inside tags is dropped, * and no element was added, removed or altered. That is one document and four * kinds of difference — indicative, not a warranty of what is preserved. * **If you seal your documents, hash a canonical form (C14N) rather than the * raw bytes.** */ export interface ImportInvoiceOptions { /** * Idempotency key for this import (max 256 chars, 24h TTL). Same rules as * {@link IdempotentRequestOptions.idempotencyKey} — it travels as a header, * never as a field of the document you supply. * * ⚠️ A key protects a REQUEST, never a DOCUMENT, and this endpoint carries a * second guard: the same supplier sending the same invoice number to the same * recipient again within 15 minutes is refused `409 duplicate_document` under * any key OTHER than the original one — a replay of the original request is * served from the idempotency cache before that guard is ever reached. Past * the 15 minutes the resend is allowed, and for up to 30 days after the first * send the response names the earlier document in `duplicateOf`. So for a lost * response, reuse the ORIGINAL key rather than minting a new one: a new key on * the same document is simply a new request. * * ⛔ But reusing it does not GUARANTEE you get the first result back. Unlike * `POST /v1/invoices`, this endpoint holds no lock, and it writes its cache * entry only once the submission has finished. A retry landing inside that * window misses the cache and meets the duplicate guard instead, so it can * surface as `409 duplicate_document` rather than a replay of the original * `201`. Read that 409 as "the first attempt got through" rather than as a * failure: it names the document already created. */ idempotencyKey?: string; /** File content as ArrayBuffer or Uint8Array */ file: ArrayBuffer | Uint8Array; /** Original filename (e.g. "invoice.xml") */ filename: string; /** MIME type (default: auto-detected from filename extension) */ mimeType?: string; /** * The recipient, declared explicitly. * * ⛔ Required and the sole routing authority. getpeppr reads the document's * customer EndpointID only to warn when it disagrees; it never derives or * rewrites the destination. Check `SendResult.warnings` and make both values * agree before the next send. */ to: { peppolId: string; }; /** * Send this document on behalf of one of your sub-tenants — **platform * accounts, master key only** (GPR-1129). * * Name the sub-tenant by `legalEntityId` or by your own * `externalSubTenantId`, one of the two, never both. A standard key that * sends this field is refused with `403`. * * ⛔ Registering a sub-tenant is NOT enough on its own. Without this field the * document is sent under your account's own legal entity, and one issued by * anyone else is refused with `supplier_identity_not_owned` — whatever the * scheme, and however the sub-tenant was verified. * * ⚠️ The document and this field must agree: the supplier endpoint stated in * the document must be the sub-tenant's own. getpeppr never rewrites the * document you supply, so a disagreement is refused * (`supplier_identity_mismatch`) rather than resolved for you. * * ⛔ Your document needs that supplier endpoint in any case — the rulebook * requires it (`PEPPOL-EN16931-R020`, *Seller electronic address MUST be * provided*, fatal). A document omitting it is refused at validation with * `422 validation_failed`, before identity is even considered; only * `x-skip-validation` reaches the identity check without one. */ sender?: Sender; } export interface BatchSendOptions { /** Max concurrent requests (default: 5) */ concurrency?: number; /** Whether to stop on first error or continue (default: false = continue all) */ stopOnError?: boolean; } export interface BatchSendResult { /** Successfully sent invoices */ succeeded: Array<{ index: number; result: SendResult; }>; /** Failed invoices with error details */ failed: Array<{ index: number; input: InvoiceInput; error: Error; }>; /** Total invoices attempted */ total: number; } /** * Designates the sub-tenant emitting an invoice (master key only). Exactly one key: * the `?: never` arms make this an exclusive XOR, mirroring the gateway which rejects * a payload carrying both `legalEntityId` and `externalSubTenantId` (or neither). */ export type Sender = { legalEntityId: string; externalSubTenantId?: never; } | { externalSubTenantId: string; legalEntityId?: never; }; /** Input to create a sub-tenant Legal Entity. */ export interface LegalEntityInput { /** Your opaque reference for this customer (1–64 chars), idempotency key for create. */ externalId: string; /** Registered legal name (2–64 chars). */ companyName: string; /** ISO 3166-1 alpha-2 country code. */ country: string; address: { line1: string; city: string; zip: string; }; /** Peppol identifier — scheme (e.g. "0007") + value. */ identifier: { scheme: string; value: string; }; } /** Public, derived status of a sub-tenant Legal Entity. */ export type LegalEntityStatus = "pending" | "verifying" | "verified" /** * The Peppol code list states that no entity stands behind this identifier's * scheme ("No entity behind id"), so there is no registry to query — nothing was * verified, and nothing ever will be. * * Sendable in **sandbox** only, which is what makes it useful: you can exercise * the whole platform flow — create a customer, watch the lifecycle webhooks, * send in their name — without registering a tax number that belongs to someone * else. Registering such a scheme with a production key is refused at intake. * * ⚠️ Deliberately NOT reported as `verified`. A green here proves the identifier * is registered and routable on the test network; it proves nothing about a * company existing or about your right to act for it. Treating it as `verified` * would let a sandbox success promise something production cannot deliver. */ | "no_registry" /** * Automatic verification is unavailable for this identifier scheme. No * registry decision was made and no verification job remains in progress. * The gateway may retry automatically if support for the scheme is added. */ | "unsupported_scheme" | "verification_failed" | "archived" | "awaiting_authz" | "expired" | "attested" | "provisioning" | "active" | "provisioning_failed" | "registration_failed"; /** Cross-AP receive discovery state, independent from outbound send readiness. */ export type NetworkDiscoveryState = "pending" | "verified" | "failed"; /** Stable, PII-safe reason codes returned when public SML/SMP discovery fails. */ export type NetworkDiscoveryFailureReason = "invalid_participant" | "sml_record_not_found" | "dns_timeout" | "dns_error" | "naptr_not_found" | "naptr_invalid" | "unsafe_smp_url" | "network_timeout" | "network_error" | "http_redirect_rejected" | "service_group_not_found" | "service_metadata_not_found" | "smp_response_too_large" | "smp_xml_invalid" | "participant_mismatch" | "invoice_service_missing" | "service_metadata_mismatch" | "as4_endpoint_missing" | "as4_endpoint_inactive" | "as4_certificate_untrusted" | "as4_certificate_inactive" | "as4_certificate_revoked" | "as4_certificate_status_unavailable"; /** Minimized proof that another Peppol Access Point can discover this entity. */ export interface NetworkDiscoveryDetail { state: NetworkDiscoveryState; /** Attempts started since the latest success; reset to 0 when verified. */ attempts: number; checkedAt?: string; nextAttemptAt?: string; error?: NetworkDiscoveryFailureReason; } /** Stable reasons why Storecove could not register a Peppol participant. */ export type LegalEntityRegistrationFailureReason = "already_registered" | "invalid_format" | "provider_error"; /** Present only while the Legal Entity status is `registration_failed`. */ export interface LegalEntityRegistrationDetail { reason: LegalEntityRegistrationFailureReason; } /** A sub-tenant Legal Entity as returned by the gateway. */ export interface LegalEntity { id: string; externalId: string | null; companyName: string | null; country: string | null; identifier: { scheme: string; value: string; } | null; status: LegalEntityStatus; /** * Cross-AP receive readiness through the public SML → SMP → Invoice metadata * → active AS4 path. `status: "active"` is only returned after this reaches * `verified`; outbound sending remains governed independently. */ networkDiscovery: NetworkDiscoveryDetail; /** * Why verification failed, when it did. * * `reason` is a FROZEN enum — no value is ever added to it. `registryStatus` * carries what `reason` cannot express. * * - `registryStatus: "inactive"` — the registry KNOWS this company and does not * consider it active: struck off, in liquidation, or **not yet active**. It * cannot be used as it stands, and re-sending the same details changes nothing * until the registry itself changes its answer. * - **field absent** — we have no such finding to show for the current state. * That is NOT the same as no finding existing. Any of these produce it: the * registry has no entry for this identifier (check for a typo, and that the * scheme matches the number); the registry could not be reached at check time; * our team has since reviewed the identity, so an earlier finding is no longer * current; the supporting evidence has aged out of retention; or no `inactive` * finding was recorded — verification can succeed on the VAT registration * alone, so the national registry is not always consulted, and where it is, it * is not always the source that decides. * * ⚠️ Absence is therefore NOT evidence about the company — least of all that the * registry does not know it. Treat this field as a POSITIVE signal only: act on * it when present, and never infer anything from its absence. */ verificationDetail?: { reason?: "name_mismatch" | "not_found"; checkedAt?: string; registryStatus?: "inactive"; /** * What to do next, in the **sandbox** only, when an identity could not be * verified — a plain sentence, safe to show to whoever is integrating. * * Inventing a company to test with is the natural first move, and it does not * work: registries are queried in sandbox exactly as in production, so a * plausible-looking number is refused. This field names the way through — * scheme `9915`, which the Peppol code list publishes as having no entity * behind it — and links to the walkthrough. * * **Absent in production**, deliberately: `9915` stands for no one, so * proposing it for a real invoice would mean invoicing a real customer under * a fictitious identity. Absent, too, on any status other than * `verification_failed`. * * The wording is not a contract — read it, show it, never parse it. */ hint?: string; }; /** Safe provider-registration diagnostic; absent for every other status. */ registrationDetail?: LegalEntityRegistrationDetail; environment: string; createdAt: string; } export interface ListLegalEntitiesOptions { limit?: number; offset?: number; /** * Narrow the page to the customer you registered under this reference — the * `externalId` you chose when you created the Legal Entity, so you never have * to store our id to find one again. * * Matched exactly: no prefix search, no case folding, no trimming. Because the * reference is unique among your live customers, the page carries at most one * entity; an unknown or archived reference is an empty `data`, not an error. * An empty string, or a reference longer than 64 characters, is refused with * `400` rather than answered with an empty list. */ externalId?: string; } export interface ArchiveLegalEntityResult { id: string; externalId: string | null; status: "archived"; } export interface AttestationInput { contactEmail: string; contactName?: string; /** * Language of the authorisation email, the confirmation page and the wording * the contact confirms, as a BCP 47 tag (e.g. `"en"`). Omit it for English. * * The gateway matches it without regard to case and never approximates it: * `"nl"` or `"en-GB"` is refused, not mapped to a neighbour. A language the * deployment does not serve — including a translation still awaiting its * native review — is refused with a `400` whose result code is * `attestation.language_unsupported` and whose body lists * `supportedLanguages`. Typed `string` on purpose: the list grows on the * gateway without an SDK release. */ language?: string; } export interface AttestationResult { id: string; externalId: string | null; status: LegalEntityStatus; expiresAt: string; /** * The language the request was sent in (canonical BCP 47 tag). Absent only * when the gateway that answered predates languages (before GPR-1318) — in * which case the request went out in English, but this SDK does not claim it. */ language?: string; /** * `"explicit"` when your input named the language, `"default"` when it named * none. Absent under the same condition as `language`. Typed `string` so a * value added by the gateway later is passed through rather than dropped. */ languageSource?: string; } /** Per-request options for legal-entity write operations. */ export interface LegalEntityRequestOptions { /** * Idempotency-Key header. Supplying it enables the SDK's automatic retry of * transient failures. Without it, POSTs are NOT retried (to avoid duplicate * side effects) — with one deliberate exception: a `429` does not need the * key, because a rate-limited request was rejected before it was processed. * ⚠️ That lifts the REPLAY guard, not the retryability verdict: a `429` the * gateway marks `Getpeppr-Retryable: false` is still not retried. * * ⛔ Neither route READS the header — the key only decides whether the SDK * retries, and what a retry costs is decided by the route: * - `create` never creates a second entity. An SDK retry repeats the identical * request, and an identical request returns the existing one. ⚠️ A retry that * OVERLAPS the first attempt — the usual case after a client timeout — gets * `409 legal_entities.creation_in_progress` rather than the entity; it is * retryable, so the following attempt converges. CHANGED details under * the same `externalId` are not a replay, and the outcome depends on what * changed: a different participant is refused; a different company name is * refused while the entity is live and sending, archives and recreates it * once its verification has failed, and is IGNORED while a verification is * still running — the existing entity comes back unchanged. * - `requestAttestation` does NOT. Re-issuing mints a fresh token, so a retry * sends your contact a SECOND email and makes the first link stop working. * Omit the key here unless a duplicate email is acceptable to you. * * ⚠️ "Transient" is decided by the gateway's result code, not by the status: * a `500` the catalogue marks `retryable: false` is not retried even with a * key, and a retryable `409` is. See the Retries section of the README. * * ⛔ It must be a key the gateway can use. A blank one (`""`, `" "`) is * refused with a `PeppolValidationError` before anything is sent — it would * reach the wire empty and unlock a replay that protects nothing. Surrounding * whitespace on a real key is harmless: `" le-42 "` is sent as `le-42`. */ idempotencyKey?: string; } /** * Postal address of the account's own legal entity, as the gateway holds it. * Every field is nullable: the gateway reports what it has, and this SDK never * fabricates a value the network did not send. */ export interface AccountIdentityAddress { line1: string | null; city: string | null; zip: string | null; } /** * The account's OWN legal entity — the company behind the API key, not a * sub-tenant. `null` fields mean the gateway holds no value, not that the * value is empty. */ export interface AccountIdentityLegalEntity { companyName: string | null; country: string | null; address: AccountIdentityAddress | null; /** ISO 8601 timestamp, or `null` when the gateway did not send one. */ createdAt: string | null; } /** * One Peppol identifier registered for the account's own legal entity. */ export interface AccountIdentifier { /** EAS scheme code, e.g. `"0192"` (Norway Organisasjonsnummer). */ scheme: string; /** The identifier value under that scheme. */ value: string; /** * Lifecycle status of this identifier as the gateway reports it. * * Drawn from the same public vocabulary as {@link LegalEntityStatus}. The * exact set the gateway emits today: `"pending"`, `"verifying"`, * `"verified"`, `"verification_failed"`, `"no_registry"`, `"unsupported_scheme"`, `"provisioning"`, * `"registration_failed"`. Typed `string`, not a closed union: the gateway * may introduce new statuses, and this SDK passes them through unchanged * rather than rejecting or hiding an identifier it does not recognise (a * TYPE guard, never a VALUE guard — same reasoning as `rawStatus`). Do not * assume the set is closed. */ status: string; /** ISO 8601 timestamp, or `null` when the gateway did not send one. */ createdAt: string | null; } export type SandboxFirstSendProfile = { status: "ready"; taxMode: "outside_scope"; line: { vatRate: 0; vatCategory: "O"; taxExemptReason: string; }; } | { status: "ready"; taxMode: "reverse_charge"; line: { vatRate: 0; vatCategory: "AE"; taxExemptReason: string; }; } | { status: "blocked"; code: string; message: string; }; /** * The Peppol identity of the account behind the API key, as returned by * `peppol.identity.get()`. Readable with ANY key — standard keys included. */ export interface AccountIdentity { /** The environment this key operates in: `"sandbox"` or `"production"`. */ environment: string; /** * The account's own legal entity, or `null` when onboarding has not created * one yet. `null` is an answer ("no legal entity yet"), not a gap. */ legalEntity: AccountIdentityLegalEntity | null; /** * Peppol identifiers registered for this account's own legal entity. An * empty array means none are registered — for instance before onboarding * completes. */ identifiers: AccountIdentifier[]; /** * Provider-compatible tax fields for a sandbox integration fixture, or a * fail-closed reason. `null` on production keys. */ sandboxFirstSend: SandboxFirstSendProfile | null; } //# sourceMappingURL=invoice.d.ts.map