Skip to content

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 an errors array with field paths and messages.
  • PeppolApiError — Network or server error. Includes statusCode and requestId for 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:

HTTP status codes returned by the API
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.
400 Validation Error
{
  "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"
}
404 Not Found
{
  "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

Platform-specific HTTP 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:

Send-as gate error codes
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.
Treat 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.
400 Invalid sender
{
  "error": "sender requires exactly one of legalEntityId or externalSubTenantId"
}
422 Send-as gate
{
  "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
The SDK handles idempotency automatically. It's safe to retry send() — the same invoice won't be sent twice.
retry.ts
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;
      }
    }
  }
}