Skip to content

Error handling

All errors come back as RFC 7807 ProblemDetails. One pattern for logging, monitoring, and troubleshooting — no per-provider special-casing.

Error response format

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "status": 404,
  "detail": "Application APP-12345 not found.",
  "instance": "/allianz/applications/APP-12345",
  "correlationId": "9f3a2b1c-4d5e-6789-abcd-ef0123456789"
}
Field What it is
type URI for the error category
title Short description
status HTTP status
detail Detailed message (no sensitive data)
instance Request path
correlationId Trace ID — send this to support

Status codes

4xx — fix the request

Status Meaning Common causes
400 Bad Request Couldn't parse Invalid JSON, wrong types
401 Unauthorized Auth failed Missing/expired/invalid token
403 Forbidden Authenticated, no permission Missing role/scope
404 Not Found No route/resource Wrong URL or unknown ID
409 Conflict State mismatch Duplicate submission
422 Unprocessable Entity Business validation failed Provider rules
429 Too Many Requests Rate limit See Rate limits

5xx — retry with care

Status Meaning What to do
500 Internal Server Error Unexpected exception Retry with backoff
502 Bad Gateway Upstream provider error Usually worth a retry
503 Service Unavailable Service down or circuit breaker Exponential backoff
504 Gateway Timeout Upstream timeout Retry, escalate if persistent

Provider business errors

Requests that are technically valid but rejected by the upstream insurer for business reasons → 422 with extra context:

{
  "type": "https://docs.insurancegateway.gr/errors/provider-business",
  "title": "Provider rejected the request",
  "status": 422,
  "detail": "Allianz declined: vehicle has active policy.",
  "providerErrorCode": "E_DUPLICATE_POLICY",
  "providerErrorMessage": "Vehicle ABC-1234 already has an active policy.",
  "correlationId": "9f3a2b1c-..."
}

What the consumer sees vs what stays hidden

Not every upstream error is surfaced the same way. The API draws a clear line between business errors (safe to pass through verbatim) and transport / infrastructure errors (which may leak internal details — credentials, internal URLs, stack traces).

422 — business: full upstream message

providerErrorMessage and detail contain the verbatim upstream message. It's actionable end-user feedback (e.g. "invalid tax ID", "vehicle already insured").

{
  "status": 422,
  "title": "Provider business error",
  "detail": "Interamerican declined: Tax ID 123456789 is not valid.",
  "provider": "Interamerican",
  "providerErrorMessage": "Tax ID 123456789 is not valid.",
  "category": "business",
  "correlationId": "9f3a2b1c-..."
}

502 — transport: summary only, no raw body

The upstream body is not exposed. Only provider and providerStatusCode come back as a summary. The raw payload (which may contain expired API keys, internal endpoints, SOAP envelopes with credentials) is sent only to server-side logs with redaction.

{
  "status": 502,
  "title": "Upstream provider error",
  "detail": "Upstream provider returned an error.",
  "provider": "Allianz",
  "providerStatusCode": 401,
  "category": "transport",
  "correlationId": "9f3a2b1c-..."
}

Example: the upstream provider returned 401 Unauthorized with body {"error":"Invalid API Key 'sk_live_abc123...'"}. The consumer sees only providerStatusCode: 401 — never the API key, never the raw error message.

504 — timeout: generic message

Generic "timed out" — no hints about what the upstream was doing or internal retry mechanics / topology.

How to present this to end-users

  • 422: surface providerErrorMessage verbatim — it's actionable ("Fix the tax ID", "Vehicle already has a policy").
  • 502 / 504: show a generic message ("Service temporarily unavailable — please try again") plus the correlationId / traceId for support escalation. Don't try to interpret providerStatusCode for the end-user.

correlationId — why it matters

Every request has a correlationId. We log it across our API and upstream calls — fastest way for both sides to talk about the same incident.

Also returned in the X-Correlation-ID response header.

Got your own request tracing? Send X-Correlation-ID and we'll propagate it:

curl -H "X-Correlation-ID: my-trace-id-123" \
     -H "Authorization: Bearer $TOKEN" \
     https://api.insurancegateway.gr/...

Retry strategy with code samples

For 429, 502, 503, 504 the right strategy is exponential backoff with jitter. Concrete numbers and ready-to-paste snippets in C#, Node.js and Python follow.

Backoff parameters

Parameter Value Notes
Initial delay 1s First retry after 1 second
Multiplier 2x Exponential growth (1s → 2s → 4s → 8s → 16s)
Max attempts 5 After this, escalate / fail
Jitter ±25% Random offset to avoid thundering herd
Max delay cap 30s Never wait longer than this between retries

C# — Polly

var policy = Policy
    .HandleResult<HttpResponseMessage>(r =>
        r.StatusCode == HttpStatusCode.BadGateway ||
        r.StatusCode == HttpStatusCode.GatewayTimeout ||
        r.StatusCode == HttpStatusCode.TooManyRequests)
    .WaitAndRetryAsync(
        retryCount: 5,
        sleepDurationProvider: attempt =>
            TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, attempt)))
            + TimeSpan.FromMilliseconds(Random.Shared.Next(-250, 250)));

Node.js — axios-retry

axiosRetry(axios, {
  retries: 5,
  retryDelay: (retryCount) => {
    const delay = Math.min(30000, Math.pow(2, retryCount) * 1000);
    const jitter = Math.random() * 500 - 250;
    return delay + jitter;
  },
  retryCondition: (error) => {
    const status = error.response?.status;
    return status === 502 || status === 504 || status === 429;
  }
});

Python — tenacity

from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception

@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential_jitter(initial=1, max=30, jitter=0.25),
    retry=retry_if_exception(lambda e: e.response.status_code in (502, 504, 429))
)
def call_estia(...): ...

POST/PATCH idempotency — check-before-retry

For mutating calls (POST/PATCH) that returned 502/504, don't blind-retry. Before a second write, do a GET by external reference (e.g. quote ID, external request ID) to verify whether the upstream already created the entity. If it did, treat that as success — do not retry.

// Pseudocode: check-before-retry for a POST that returned 502/504
var existing = await api.GetQuoteByExternalRefAsync(externalRef);
if (existing is not null)
    return existing;                       // already created upstream — success
return await api.CreateQuoteAsync(payload); // safe to retry

Retry guide

Status Retry? Notes
400, 401, 403, 404, 422 No Fix request or credentials
409 Maybe Check if it already completed
429 Yes Respect Retry-After, exponential backoff
500 Limited A few attempts with spacing
502, 503, 504 Yes Idempotent calls or careful retries

Mutating requests

Don't blind-retry POST/PATCH that change state. If the request partly succeeded upstream, a retry can create duplicates or inconsistent state.