Skip to content

Errors

Every error response from the Auradonors API is an RFC 7807 ProblemDetails document with Content-Type: application/problem+json. The shape is the same whether the failure is a routine validation problem or an unexpected 500.

{
"type": "https://auradonors.com/errors/validation",
"title": "One or more validation errors occurred.",
"status": 400,
"instance": "/api/tenants/abc.../orders",
"errors": {
"ContactId": ["Contact id must be greater than 0."]
},
"traceId": "5f8b1c00b3e74b0fa6a3..."
}
Field Meaning
type URI naming the error category. Stable; safe to switch on.
title One-sentence human summary. Safe to log; safe to surface to internal staff.
status HTTP status code (mirrors the response status).
instance Path of the failing request. Useful when correlating with your own logs.
errors Present only on validation failures. Per-field error messages, grouped by property name.
traceId Sentry event id (when Sentry is enabled) or the ASP.NET request trace identifier. Always quote this when filing a support ticket.

The envelope deliberately does not carry the underlying exception message or stack trace. Those are written to Sentry against the traceId. To investigate a specific failure, paste the traceId to support.

The platform’s GlobalExceptionHandler maps unhandled exceptions to status codes consistently. There are five canonical cases:

Status Trigger Body type
400 A query or route value could not be read — a required parameter was omitted (year on a tax statement), a number carried letters (page=abc), a value was empty (dateFrom=). ProblemDetails of type https://auradonors.com/errors/bad-request; detail names the parameter.
400 FluentValidation.ValidationException — request shape or business rule rejected the input. ValidationProblemDetails (envelope above with errors populated).
403 UnauthorizedAccessException raised by a guard behavior — [RequirePermission], [RequiresFeature], or [ProhibitedDuringImpersonation] rejected the call. ProblemDetails. The title names the missing permission or guard.
404 A path-bound resource ({tenantId}, {contactId}, …) didn’t resolve. Empty body or minimal Result.Failure shape.
500 Anything unhandled. ProblemDetails with a sanitized title and a traceId.

Authentication failures (401) come from the auth handler, not the exception handler — see Authentication for the specific rejection cases.

A request the framework cannot bind never reaches a handler. It is a 400 with a body of its own type, and the detail tells you which parameter and why:

{
"type": "https://auradonors.com/errors/bad-request",
"title": "The request could not be read.",
"status": 400,
"detail": "Failed to bind parameter \"int page\" from \"abc\".",
"instance": "/api/tenants/abc.../orders",
"traceId": "0HNF..."
}

Three shapes land here: a required query parameter that was not sent, a value that does not parse as its type, and an empty value (?dateFrom=) — an empty string is not the same as omitting the parameter. Every optional flag and every paging parameter is optional to omit, so a bare GET on a list endpoint is always answered; the parameters that are genuinely required say so in their description in the Live API explorer. Nothing about this response reaches Sentry — it is a client mistake, not a fault — so the traceId here is the request identifier, not an event.

Validation failures group per-field errors by the property name that failed:

{
"type": "https://auradonors.com/errors/validation",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Items[0].Quantity": ["Quantity must be greater than 0."],
"BillingAddress.PostalCode": ["Postal code is required for US addresses."]
},
"traceId": "..."
}

Render these inline against the matching form field. The keys mirror the request DTO’s path; treat them as opaque strings.

Many handlers return a typed failure rather than throwing — for example, POST /orders rejecting a closed batch returns 400 with:

{
"type": "https://auradonors.com/errors/validation",
"title": "...",
"status": 400,
"errors": {
"": ["The batch is closed."]
},
"traceId": "..."
}

Where the failure carries a typed code (e.g., Order.BatchClosed, Payment.DeclinedByProcessor), the code identifies the specific failure mode in a stable, switch-on-able way. The error codes catalog lists every code emitted by the application, generated from the source so it can’t drift.

The transport always wraps these as the errors map for compatibility with the validation envelope; future shapes may surface the code in a dedicated field.

Every error response carries a traceId. It maps to:

  • A Sentry event on a 500 in production and staging (the handler captures the exception itself and returns that event’s id, so the id always belongs to this request’s failure).
  • The request trace identifier on every 4xx, and on a 500 when Sentry isn’t enabled (local dev, some test environments).

Quote the traceId to support when reporting an issue. Don’t try to use it as a primary key in your own systems — its lifetime is tied to the Sentry retention window.

  • Stack traces in any response body.
  • Database identifiers, schema names, or SQL fragments.
  • Third-party API response bodies. When a downstream provider (Stripe, ShipStation, Avalara, Postmark, etc.) fails, the API maps the exception to a sanitized message.
  • Field-level error model beyond ValidationProblemDetails. A request that fails business rules surfaces a single message under errors[""], not a structured per-field tree.
  • Localized error text. All messages are English. There is no Accept-Language negotiation.

Treat the response status as the primary signal:

var response = await http.PostAsJsonAsync(url, request);
if (!response.IsSuccessStatusCode)
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails>();
if ((int)response.StatusCode == 400 && problem?.Extensions.GetValueOrDefault("errors") is { } errors)
{
// ValidationProblemDetails shape — render inline.
}
else if ((int)response.StatusCode == 403)
{
// Permission missing. Reconcile against /_generated/permissions/.
}
else
{
// Capture problem.Extensions["traceId"] before retrying or surfacing.
}
}

Retry policy: see Idempotency for the safe retry path on order creation. For most reads, an immediate retry on 5xx is fine; on 429, see Rate limits.