Skip to main content

Error Reference

Every error from the Invostaq API uses RFC 7807 Problem Details. You can match on the type field to handle each error programmatically.


Error shape

{
"type": "https://invostaq.com/errors/{slug}",
"title": "Human-readable summary",
"detail": "What went wrong, specific to this request.",
"status": 400
}
FieldTypeDescription
typestringStable error identifier URI. Match on this in code.
titlestringShort summary, same for all instances of this error type.
detailstringRequest-specific explanation. May include values or field names.
statusintegerHTTP status code (matches the response status).

Some errors include additional fields — see Extended errors.


Authentication errors

api-key-required — 401

You didn't include the x-api-key header.

curl https://api.invostaq.com/api/v2/lookup?participantId=0196:971501234567
{
"type": "https://invostaq.com/errors/api-key-required",
"title": "API key required",
"detail": "Include your API key in the x-api-key header.",
"status": 401
}

Fix: Add -H "x-api-key: sk_test_..." to your request.


invalid-api-key — 401

The key doesn't exist, was revoked, or is malformed.

{
"type": "https://invostaq.com/errors/invalid-api-key",
"title": "Invalid API key",
"detail": "The API key provided is invalid, revoked, or malformed.",
"status": 401
}

Fix: Verify you're using the correct key and that it hasn't been revoked.


Validation errors

missing-participant-id — 400

The lookup endpoint requires a participantId query parameter.

{
"type": "https://invostaq.com/errors/missing-participant-id",
"title": "Missing participant ID",
"detail": "The participantId query parameter is required.",
"status": 400
}

Fix: Add ?participantId=0196:971501234567 to the URL.


missing-routing — 400

The send request is missing required routing fields.

{
"type": "https://invostaq.com/errors/missing-routing",
"title": "Missing routing information",
"detail": "senderParticipantId, receiverParticipantId, accessPointRef, and extracted invoice data are all required.",
"status": 400
}

Fix: Include all four required fields: senderParticipantId, receiverParticipantId, accessPointRef, and extracted.


invalid-sender-id — 400

The sender participant ID doesn't match ISO 6523 format.

{
"type": "https://invostaq.com/errors/invalid-sender-id",
"title": "Invalid sender participant ID",
"detail": "senderParticipantId must be in ISO 6523 format, e.g. 0196:971500000001.",
"status": 400
}

Fix: Use the format {4-digit-scheme}:{identifier}. For UAE, the scheme is 0196. Example: 0196:971500000001.


invalid-receiver-id — 400

Same format requirement as invalid-sender-id, for the receiver.

{
"type": "https://invostaq.com/errors/invalid-receiver-id",
"title": "Invalid receiver participant ID",
"detail": "receiverParticipantId must be in ISO 6523 format, e.g. 0196:971501234567.",
"status": 400
}

declared-totals-mismatch — 400

v2 only. The declared totals block is internally inconsistent. Either subtotal doesn't match the sum of line extensions, or grandTotal doesn't match subtotal + taxAmount.

{
"type": "https://invostaq.com/errors/declared-totals-mismatch",
"title": "Declared totals mismatch",
"detail": "grandTotal (1200.00) does not equal subtotal (1000.00) + taxAmount (210.00). Expected 1210.00.",
"status": 400
}

How the math works: subtotal must equal the sum of quantity * unitPrice across all line items. grandTotal must equal subtotal + taxAmount. Both checks use exact decimal comparison (no tolerance).

Fix: Recalculate your totals block. For each line: lineExtension = quantity * unitPrice. Then: subtotal = sum(lineExtensions), taxAmount = sum(round(lineExtension * taxRate / 100, 2)), grandTotal = subtotal + taxAmount.


totals-mismatch — 400

v1 only. The sum of line items (including tax) doesn't match the declared totalAmount.

{
"type": "https://invostaq.com/errors/totals-mismatch",
"title": "Invoice totals mismatch",
"detail": "Computed line total (including VAT) is 105.00, but totalAmount is 999.99. Difference exceeds ±0.02 tolerance.",
"status": 400
}

How the math works: For each line item: lineTotal = round(quantity * unitPrice, 2), then lineTax = round(lineTotal * vatRate, 2). Sum all (lineTotal + lineTax) values. This must equal totalAmount within ±0.02.


invalid-body — 400

The request body isn't valid JSON.

{
"type": "https://invostaq.com/errors/invalid-body",
"title": "Invalid request body",
"detail": "The request body could not be parsed as JSON.",
"status": 400
}

Fix: Check for syntax errors — a common mistake is a trailing comma after the last field.


Extended errors

These errors include additional fields beyond the standard shape.

validation-failed — 400

UBL pre-flight validation found issues with the invoice data.

{
"type": "https://invostaq.com/errors/validation-failed",
"title": "Invoice validation failed",
"detail": "The invoice data failed UBL pre-flight validation.",
"status": 400,
"validationErrors": [
"cbc:IssueDate is required and must not be empty",
"cac:InvoiceLine must contain at least one line item",
"cbc:TaxAmount must be a non-negative decimal"
]
}
Extra fieldTypeDescription
validationErrorsstring[]List of UBL/Peppol BIS 3.0 validation rule violations

Fix: Address each error in the array. These are structural UBL rules — the invoice must pass validation before it can be submitted to Peppol.


network-failed — 400

The invoice was valid but the Peppol Access Point rejected the submission.

{
"type": "https://invostaq.com/errors/network-failed",
"title": "Network delivery failed",
"detail": "The Peppol Access Point rejected the submission.",
"status": 400,
"errorCode": "RECIPIENT_NOT_FOUND",
"upstreamStatus": 404
}
Extra fieldTypeDescription
errorCodestringError code from the Peppol Access Point
upstreamStatusintegerHTTP status code from the Access Point

Fix: Check errorCode for the specific failure. Transient errors (5xx upstreamStatus) may succeed on retry.


Rate limiting

rate-limit-exceeded — 429

You've exceeded the per-key rate limit (GET: 60/min, POST: 20/min).

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json
{
"type": "https://invostaq.com/errors/rate-limit-exceeded",
"title": "Rate limit exceeded",
"detail": "Too many requests. Retry after the period specified in the Retry-After header.",
"status": 429
}

Fix: Wait the number of seconds in the Retry-After header before retrying.


Handling errors in code

StatusWhat to do
400Fix the request. Don't retry with the same data.
401Check your API key. Don't retry.
429Wait Retry-After seconds, then retry the same request.
500Retry with exponential backoff (1s, 2s, 4s).
const res = await fetch("https://api.invostaq.com/api/v2/invoices/send", {
method: "POST",
headers: {
"x-api-key": process.env.INVOSTAQ_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(invoice),
});

if (!res.ok) {
const problem = await res.json();

switch (problem.type) {
case "https://invostaq.com/errors/rate-limit-exceeded":
const retryAfter = parseInt(res.headers.get("Retry-After") || "60");
// wait retryAfter seconds, then retry
break;

case "https://invostaq.com/errors/validation-failed":
console.error("Fix these:", problem.validationErrors);
break;

case "https://invostaq.com/errors/network-failed":
console.error(`AP error: ${problem.errorCode} (${problem.upstreamStatus})`);
break;

default:
throw new Error(`${problem.title}: ${problem.detail}`);
}
}

Error reference table

SlugStatusExtensions
api-key-required401
invalid-api-key401
missing-participant-id400
missing-routing400
invalid-sender-id400
invalid-receiver-id400
declared-totals-mismatch400
totals-mismatch400
invalid-body400
validation-failed400validationErrors: string[]
network-failed400errorCode: string, upstreamStatus: number
rate-limit-exceeded429