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 four canonical cases:
| Status | Trigger | Body type |
|---|---|---|
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.
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 in production and staging (Sentry captures the
exception and assigns its event id as the
traceId). - The request trace identifier 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.
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.