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.
Envelope
Section titled “Envelope”{ "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.
Status mappings
Section titled “Status mappings”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.
Unreadable requests
Section titled “Unreadable requests”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 errors
Section titled “Validation errors”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.
Typed Result<T> failures
Section titled “Typed Result<T> failures”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.
The traceId
Section titled “The traceId”Every error response carries a traceId. It maps to:
- A Sentry event on a
500in 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 a500when 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.
What you won’t see
Section titled “What you won’t see”- 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 undererrors[""], not a structured per-field tree. - Localized error text. All messages are English. There is no
Accept-Languagenegotiation.
Handling errors
Section titled “Handling errors”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.