Errors
All errors come back in the same envelope. Build your logic on error.code; message is in English and may change, so it is not suitable for showing to users.
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid calculation input", "details": { "fields": [{ "field": "amount", "reason": "SCALE_EXCEEDED", "message": "amount allows at most 2 fractional digits" }] } }, "requestId": "8a3e7260-dcb6-4227-acdb-88b2a07774e8"}requestId is the same as the X-Request-Id header. Include this value when asking for support.
Error codes
Section titled “Error codes”| Code | HTTP | When | Retry? |
|---|---|---|---|
VALIDATION_ERROR |
422 (malformed JSON: 400, content type: 415) | Parameter/body schema or calculation input error. | No, fix the request. |
NOT_FOUND |
404 | The tool does not exist, is not published or is not in the requested language; unknown address. | No. |
CONFLICT |
409 | details.reason = "VERSION_OUTDATED": the version you sent is no longer live. |
Fetch the definition again and retry. |
CALCULATION_ERROR |
422 / 500 | 422: the result is undefined for these inputs. 500: the tool temporarily cannot be calculated. | 422: no. 500: after a while. |
RATE_UNAVAILABLE |
422 | No rate is in force on the asOf date, or the rate is unavailable. |
Change the date. |
PAYLOAD_TOO_LARGE |
413 | The body exceeds 16 KB. | No. |
RATE_LIMITED |
429 | The rate limit was exceeded. | Yes, after waiting Retry-After. |
TIMEOUT |
504 | The request could not be completed within 15 s. | Yes (with backoff). |
SERVICE_UNAVAILABLE |
503 | The service is temporarily unavailable. | Yes (with backoff). |
MAINTENANCE |
503 | The tool is temporarily under maintenance. | Yes, after Retry-After. |
INTERNAL_ERROR |
500 | Unexpected error; details only in server logs. | Yes, a few times; if it persists, report it with the requestId. |
details fields
Section titled “details fields”Parameter or body schema: details.issues[]
Section titled “Parameter or body schema: details.issues[]”{ "issues": [{ "path": "inputs.amount", "message": "Invalid input", "code": "invalid_union" }] }path is the path of the faulty field, and code is the schema validator’s code (invalid_type, unrecognized_keys…). For an invalid asOf, details.field = "asOf" is returned.
Calculation input: details.fields[]
Section titled “Calculation input: details.fields[]”{ "fields": [{ "field": "vatRateOption", "reason": "MISSING_INPUT", "message": "vatRateOption is required" }] }reason |
Meaning |
|---|---|
MISSING_INPUT |
A required field was not sent. |
NULL_INPUT |
The value is null. |
INVALID_INPUT |
The value is invalid (e.g. an enum value that is not in the definition). |
TYPE_MISMATCH |
The type does not match. |
OUT_OF_RANGE |
Outside min/max. |
SCALE_EXCEEDED |
Too many decimal places. |
UNKNOWN_INPUT |
A field that is not in the definition. |
To show these errors to the user per field, match fields[].field to the field in your form; generate the text in your own language.
Calculation error: details.reason
Section titled “Calculation error: details.reason”For 422 CALCULATION_ERROR: DIVISION_BY_ZERO, CALCULATION_OVERFLOW, INVALID_DATE_OPERATION, INVALID_ARGUMENT, NO_SOLUTION. The input combination is mathematically meaningless; ask the user to change the value.
No rate: RATE_UNAVAILABLE
Section titled “No rate: RATE_UNAVAILABLE”{ "error": { "code": "RATE_UNAVAILABLE", "message": "No rate is in force for the requested date", "details": { "rates": ["tr.vat.standard"], "asOf": "1990-01-01" } }, "requestId": "…" }Rate limit: RATE_LIMITED
Section titled “Rate limit: RATE_LIMITED”It comes with the Retry-After header (seconds) and details.retryAfterSeconds. Details: Rate limits.
Handling strategy
Section titled “Handling strategy”- Do not retry 4xx (except 429 and 409): without fixing the request, it gives the same result.
- 429 and 503
MAINTENANCE: wait forRetry-After. - 5xx, 504 and network errors: try 2–3 times with exponential backoff and randomness (jitter).
- Always log the
requestId.
async function calculate(key, body, attempt = 0) { const response = await fetch(`https://api.mirac-ozdemir.com/v1/public/tools/by-key/${key}/calculate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(20_000), }); if (response.ok) return response.json();
const { error, requestId } = await response.json().catch(() => ({ error: { code: 'INTERNAL_ERROR' } })); const retryAfter = Number(response.headers.get('Retry-After')); const retryable = response.status === 429 || response.status >= 500;
if (retryable && attempt < 3) { const wait = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500 + Math.random() * 250; await new Promise((resolve) => setTimeout(resolve, wait)); return calculate(key, body, attempt + 1); } throw Object.assign(new Error(error.code), { code: error.code, details: error.details, requestId });}import random, time, requests
def calculate(key, body, attempts=3): url = f"https://api.mirac-ozdemir.com/v1/public/tools/by-key/{key}/calculate" for attempt in range(attempts + 1): r = requests.post(url, json=body, timeout=20) if r.ok: return r.json() retryable = r.status_code == 429 or r.status_code >= 500 if not retryable or attempt == attempts: err = r.json().get("error", {}) raise RuntimeError(f"{err.get('code')} (requestId={r.json().get('requestId')})") wait = float(r.headers.get("Retry-After", 0)) or (2 ** attempt * 0.5 + random.random() * 0.25) time.sleep(wait)