Error Handling
Zyntem uses standard HTTP status codes and returns structured error responses.
Error response format
All errors return a JSON object with an error field:
{
"error": "human-readable error message"
}
HTTP status codes
Client errors (4xx)
| Status | Meaning | When it occurs |
|---|---|---|
400 Bad Request | Validation error | Missing/invalid fields, constraint violations |
401 Unauthorized | Authentication failed | Missing, invalid, or revoked API key |
403 Forbidden | Access denied | Inactive account or live mode not enabled |
404 Not Found | Resource not found | Invalid ID or accessing another account's resource |
207 Multi-Status | Partial success | Some items in a batch operation succeeded while others failed |
409 Conflict | Duplicate resource or state conflict | E.g., duplicate billing email, non-cancellable transaction |
Server errors (5xx)
| Status | Meaning | When it occurs |
|---|---|---|
500 Internal Server Error | Unexpected failure | Database errors, upstream failures |
Common errors by endpoint
Account creation (POST /v1/accounts)
# Missing name
curl -X POST https://api.zyntem.dev/v1/accounts \
-H "Content-Type: application/json" \
-d '{"billing_email": "admin@acme.com"}'
{
"error": "Key: 'CreateRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"
}
# Duplicate email
curl -X POST https://api.zyntem.dev/v1/accounts \
-H "Content-Type: application/json" \
-d '{"name": "Acme 2", "billing_email": "admin@acme.com"}'
{
"error": "an account with this billing email already exists"
}
Location creation (POST /v1/locations)
# Spain without country_config
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_..." \
-d '{"name": "Test", "country": "ES"}'
{
"error": "Spain locations require country_config with system field (ticketbai or verifactu)"
}
Authentication errors
# No Authorization header
curl https://api.zyntem.dev/v1/locations
{
"error": "missing or invalid Authorization header"
}
# Invalid key
curl https://api.zyntem.dev/v1/locations \
-H "Authorization: Bearer invalid_key"
{
"error": "invalid API key format"
}
Pagination errors
# Invalid status filter
curl "https://api.zyntem.dev/v1/locations?status=deleted" \
-H "Authorization: Bearer zyn_test_..."
{
"error": "status must be 'active' or 'inactive'"
}
Live mode guard (403 Forbidden)
# Using a live key when live mode is not enabled
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_live_..." \
-H "Content-Type: application/json" \
-d '{ ... }'
{
"error": "live mode is not enabled"
}
Transaction cancellation (409 Conflict)
# Cancelling a transaction that is not in a cancellable status
curl -X POST https://api.zyntem.dev/v1/transactions/{id}/cancel \
-H "Authorization: Bearer zyn_test_..."
{
"error": "transaction cannot be cancelled"
}
Batch processing (207 Multi-Status)
Batch processing endpoints may return 207 when some items succeed and others fail. Check the alert_triggered field in the response to detect failures.
Transaction statuses and errors
The failed_permanent status indicates a transaction that has exhausted all retry attempts and moved to dead letter. Subscribe to the fiscalization.dead_letter webhook event to be notified when this occurs.
Asynchronous error translation
When a tax-authority submission fails, the transaction is persisted as failed immediately so the API call does not block on translation latency. The raw authority message is often in the local language (Spanish, Italian, French, Portuguese), so Zyntem translates it to English in the background using the Claude API (cache-first) and updates the transaction record when the translation is ready.
The shape you observe on a freshly failed transaction depends on whether AI translation is enabled for your deployment:
error_message value | Meaning |
|---|---|
null | Translation is pending. Poll the transaction or wait a few seconds, then re-read it to get the English translation. |
| non-empty string | Final message. Either the translated English text (when AI is enabled) or the raw adapter message (when AI is disabled). |
Regardless of the translation state, the raw tax-authority error code and message are always persisted in response_payload.submit_error so you can inspect them immediately:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed",
"error_message": null,
"response_payload": {
"submit_error": {
"error_code": "AEAT-4321",
"raw_message": "NIF del obligado tributario no identificado"
}
}
}
Polling pattern:
import time
import requests
def get_translated_error(tx_id, api_key, max_wait_seconds=10):
deadline = time.time() + max_wait_seconds
while time.time() < deadline:
r = requests.get(
f"https://api.zyntem.dev/v1/transactions/{tx_id}",
headers={"Authorization": f"Bearer {api_key}"},
)
tx = r.json()
if tx["status"] != "failed":
return tx
if tx.get("error_message"):
return tx
time.sleep(0.5)
return tx # translation still pending; response_payload still has raw details
If you prefer push over poll, see the webhooks guide for the fiscalization.completed and fiscalization.dead_letter events, which fire when a transaction reaches a terminal state.
Best practices
- Always check the status code before parsing the response body
- Log error responses for debugging -- the
errorfield provides context - Handle 401 errors by refreshing or rotating your API key
- Retry 500 errors with exponential backoff (e.g., 1s, 2s, 4s)
- Don't retry 4xx errors without fixing the request -- they indicate client-side issues