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
}
| Field | Type | Description |
|---|---|---|
type | string | Stable error identifier URI. Match on this in code. |
title | string | Short summary, same for all instances of this error type. |
detail | string | Request-specific explanation. May include values or field names. |
status | integer | HTTP 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 field | Type | Description |
|---|---|---|
validationErrors | string[] | 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 field | Type | Description |
|---|---|---|
errorCode | string | Error code from the Peppol Access Point |
upstreamStatus | integer | HTTP 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
| Status | What to do |
|---|---|
400 | Fix the request. Don't retry with the same data. |
401 | Check your API key. Don't retry. |
429 | Wait Retry-After seconds, then retry the same request. |
500 | Retry 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
| Slug | Status | Extensions |
|---|---|---|
api-key-required | 401 | — |
invalid-api-key | 401 | — |
missing-participant-id | 400 | — |
missing-routing | 400 | — |
invalid-sender-id | 400 | — |
invalid-receiver-id | 400 | — |
declared-totals-mismatch | 400 | — |
totals-mismatch | 400 | — |
invalid-body | 400 | — |
validation-failed | 400 | validationErrors: string[] |
network-failed | 400 | errorCode: string, upstreamStatus: number |
rate-limit-exceeded | 429 | — |