Error Handling¶
Invora APIs use standard gRPC status codes augmented with domain-specific error details.
Wire format follows google.rpc.Status: top-level gRPC status carries code, message, and a details array of google.protobuf.Any-packed messages (for example google.rpc.ErrorInfo, google.rpc.BadRequest, or Invora-specific types such as invora.documents.v2.ValidationError). JSON transcoding maps these fields per gRPC JSON mapping.
gRPC Status Codes¶
| Code | Meaning | When |
|---|---|---|
OK (0) |
Success | Request completed |
INVALID_ARGUMENT (3) |
Bad request | Missing required fields, invalid enum values, malformed input |
NOT_FOUND (5) |
Resource missing | Document key, party, plan code doesn't exist |
ALREADY_EXISTS (6) |
Duplicate | Key collision on create, duplicate link |
PERMISSION_DENIED (7) |
Unauthorized | Missing scope, wrong tenant, billing entitlement required |
FAILED_PRECONDITION (9) |
State conflict | Freeze a non-draft, update a frozen document, link non-customer party |
ABORTED (10) |
Concurrency conflict | concurrency_stamp mismatch (another write happened) |
OUT_OF_RANGE (11) |
Pagination invalid | Expired cursor, page_size > max |
UNIMPLEMENTED (12) |
Not available | RPC exists in proto but not yet implemented |
UNAVAILABLE (14) |
Temporary failure | Upstream timeout (ZATCA, billing provider), retry with backoff |
RESOURCE_EXHAUSTED (8) |
Rate limited | Too many requests — see Rate Limiting section |
Error Detail Structure¶
Every error includes a google.rpc.Status with structured details in details field:
{
"code": 3,
"message": "Document validation failed",
"details": [
{
"@type": "type.googleapis.com/invora.documents.v2.ValidationError",
"field": "invoice_line[0].line_extension_amount",
"code": "AMOUNT_MISMATCH",
"message": "Line extension amount (100.00) does not equal quantity * price (90.00)",
"severity": "ERROR"
}
]
}
Domain Error Codes¶
Domain errors are returned as string codes in google.rpc.ErrorInfo details or as ValidationError messages. These are not proto enums — they are server-defined string constants returned in error responses.
Document State Errors¶
| Code | gRPC Status | Trigger |
|---|---|---|
DOCUMENT_NOT_DRAFT |
FAILED_PRECONDITION |
Attempting to Update/Delete a frozen document |
DOCUMENT_NOT_FROZEN |
FAILED_PRECONDITION |
Attempting to Send a draft document |
DOCUMENT_ALREADY_CANCELLED |
FAILED_PRECONDITION |
Cancel/WriteOff an already-cancelled document |
CONCURRENCY_CONFLICT |
ABORTED |
concurrency_stamp doesn't match server version |
MISSING_SUPPLIER |
INVALID_ARGUMENT |
Document has no supplier party |
MISSING_CUSTOMER |
INVALID_ARGUMENT |
Document has no customer party |
MISSING_LINE_ITEMS |
INVALID_ARGUMENT |
Document has zero line items |
Regulation Errors¶
| Code | gRPC Status | Trigger |
|---|---|---|
REGULATION_NOT_ENABLED |
FAILED_PRECONDITION |
Freeze with regulation that isn't enabled for tenant |
REGULATION_NOT_ONBOARDED |
FAILED_PRECONDITION |
Submit to ZATCA before completing onboarding |
SUBMISSION_REJECTED |
ABORTED |
Regulatory authority rejected the document |
SUBMISSION_TIMEOUT |
UNAVAILABLE |
Regulatory authority didn't respond in time |
ZATCA-Specific Errors¶
Returned as string values in ZatcaOnboardingError.code and ZatcaOnboardingError.category within the InitiateOnboarding stream. These are server-defined string codes, not proto enums:
| Category | Code | Meaning |
|---|---|---|
OTP |
INVALID_OTP |
OTP expired or already used |
OTP |
OTP_RATE_LIMITED |
Too many OTP attempts |
CSR |
CSR_GENERATION_FAILED |
Certificate signing request generation failed |
COMPLIANCE |
COMPLIANCE_CHECK_FAILED |
ZATCA rejected compliance invoice |
COMPLIANCE |
CSID_NOT_ISSUED |
ZATCA did not issue compliance CSID |
PRODUCTION |
PRODUCTION_CSID_FAILED |
Production CSID issuance failed |
Billing Errors¶
| Code | gRPC Status | Trigger |
|---|---|---|
BILLING_NOT_ENABLED |
PERMISSION_DENIED |
Tenant plan doesn't include billing capability |
CUSTOMER_NOT_FOUND |
NOT_FOUND |
external_id doesn't match any billing customer |
PLAN_NOT_FOUND |
NOT_FOUND |
Plan code doesn't exist |
SUBSCRIPTION_ALREADY_ACTIVE |
ALREADY_EXISTS |
Customer already has active subscription to this plan |
WALLET_INSUFFICIENT_BALANCE |
FAILED_PRECONDITION |
Wallet balance too low for transaction |
Party Errors¶
| Code | gRPC Status | Trigger |
|---|---|---|
PARTY_NOT_CUSTOMER_ROLE |
FAILED_PRECONDITION |
LinkBillingCustomer on a SUPPLIER-only party |
PARTY_ALREADY_LINKED |
ALREADY_EXISTS |
Party already linked to a billing customer |
BILLING_ENTITLEMENT_REQUIRED |
PERMISSION_DENIED |
Tenant plan doesn't include billing; can't link |
Retry Strategy¶
| Error | Retry? | Strategy |
|---|---|---|
UNAVAILABLE |
Yes | Exponential backoff: 1s, 2s, 4s, 8s, max 5 retries |
RESOURCE_EXHAUSTED |
Yes | Wait for retry-after header, or backoff 30s |
ABORTED (concurrency) |
Yes | Re-read resource, get fresh stamp, retry once |
INTERNAL |
Maybe | Report to Invora support if persistent |
| All others | No | Fix request and resubmit |
Rate Limiting¶
Rate limits are enforced per tenant. When exceeded, the server returns RESOURCE_EXHAUSTED.
Limits vary by plan tier and endpoint category (read vs write vs bulk). Check response headers for current limits:
x-ratelimit-limit: max requests in current windowx-ratelimit-remaining: requests left in windowx-ratelimit-reset: UTC epoch seconds when window resets
When rate limited, back off and retry after the x-ratelimit-reset time. If no reset header is present, use exponential backoff starting at 30 seconds.
Contact support or check your plan details for specific rate limit quotas.