Error Handling
Understand error types, HTTP status codes, and implement robust retry strategies.
Error Types
The SDK throws typed errors you can catch and handle precisely. Two main error classes cover all failure modes:
-
PeppolValidationError— Invoice failed local validation. Contains anerrorsarray with field paths and messages. -
PeppolApiError— Network or server error. IncludesstatusCodeandrequestIdfor support.
import { Peppol, PeppolValidationError, PeppolApiError } from "@getpeppr/sdk";
const peppol = new Peppol({ apiKey: "sk_live_..." });
try {
const result = await peppol.invoices.send(invoiceData);
console.log(`Success: ${result.id}`);
} catch (error) {
if (error instanceof PeppolValidationError) {
// Local validation failed — invoice never sent
console.error("Validation errors:");
for (const e of error.errors) {
console.error(` ${e.field}: ${e.message}`);
}
} else if (error instanceof PeppolApiError) {
// API returned an error — network or server issue
console.error(`API error [${error.statusCode}]: ${error.message}`);
console.error(`Request ID: ${error.requestId}`); // for support
} else {
throw error; // unexpected
}
}HTTP Status Codes
The API uses standard HTTP status codes. Here are the ones you'll encounter:
| Status | Meaning |
|---|---|
200 | Success — request completed. |
201 | Created — invoice sent successfully. |
400 | Bad Request — invalid parameters or validation error. |
401 | Unauthorized — missing or invalid API key. |
403 | Forbidden — your API key is valid but lacks the required scope. |
404 | Not Found — resource doesn't exist. |
409 | Conflict — the request is valid but the resource is not in the right state. |
422 | Unprocessable Entity — business-rule gate failed, such as identity verification or send readiness. |
429 | Too Many Requests — rate limit exceeded (see Rate Limits). |
500 | Server Error — retry with exponential backoff. |
{
"error": "validation_error",
"message": "Invoice validation failed",
"errors": [
{
"field": "from.vatNumber",
"message": "VAT number is required for invoices exceeding EUR 400",
"suggestion": "Add vatNumber to the seller party"
}
],
"requestId": "req_abc123xyz"
}{
"error": "not_found",
"message": "Invoice inv_xyz789 not found",
"statusCode": 404,
"requestId": "req_def456uvw"
}Platform Errors
Platform accounts use a master key to manage sub-tenants and send on their behalf. These endpoints add a few multi-tenant error semantics on top of the generic API errors above.
Status codes
| Status | Meaning |
|---|---|
403 | Authenticated key is not a master key, or lacks the required legal_entities:* scope. |
404 | Sub-tenant is unknown, disabled, malformed, or belongs to another platform. getpeppr returns 404 instead of 403 to prevent resource enumeration. |
409 | Operation conflicts with the sub-tenant lifecycle, for example requesting attestation too early or after the customer already attested. |
422 | Sending is blocked by a business gate. Check the code field to decide what your UI should show. |
502 | Attestation email delivery failed. Retrying can mint a fresh authorisation link. |
Send-as gate codes
When POST /v1/invoices includes sender, a 422
response uses error: "peppol_identity_not_verified" with one of these
code values:
| Code | Meaning / next step |
|---|---|
verification_pending | Registry verification has not completed yet. Poll the legal entity or wait for a lifecycle webhook. |
verification_failed | Registry verification failed. Inspect the legal entity status or the legal_entity.verification_failed webhook. |
attestation_required | Production customer authorisation is required before sending. Request attestation and wait for the customer to authorise. |
peppol_identity_expired | The authorisation window expired. Request a new attestation. |
provisioning | Sub-tenant is attested but still being registered on the network. Wait until the legal entity becomes active. |
legal_entity.verification_failed as a lifecycle webhook, not as a retryable
HTTP failure. Your UI should map it to the customer record via subTenantId and
show the remediation path.
{
"error": "sender requires exactly one of legalEntityId or externalSubTenantId"
}{
"error": "peppol_identity_not_verified",
"code": "attestation_required",
"message": "Sub-tenant attestation is required before sending in production.",
"docs": "https://getpeppr.dev/docs/platform/sending-and-webhooks/"
}Retry Strategies
For transient failures (5xx errors, network timeouts), implement exponential backoff. Never retry client errors (4xx) — they require fixing the request.
Best Practices
- Retry only 5xx — server errors are transient, client errors need fixes
- Exponential backoff — wait 1s, 2s, 4s between retries
- Max 3 retries — avoid hammering the API
- Include requestId — send it to support for debugging persistent failures
send() — the same invoice won't be sent twice.
import { Peppol, PeppolApiError } from "@getpeppr/sdk";
const peppol = new Peppol({ apiKey: "sk_live_..." });
async function sendWithRetry(invoice: any, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await peppol.invoices.send(invoice);
} catch (error) {
if (error instanceof PeppolApiError) {
// Don't retry client errors (4xx) — only server errors (5xx)
if (error.statusCode < 500) throw error;
// Don't retry on last attempt
if (attempt === maxRetries) throw error;
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, attempt - 1) * 1000;
await new Promise((r) => setTimeout(r, delay));
} else {
throw error;
}
}
}
}