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..."
}
FieldMeaning
typeURI naming the error category. Stable; safe to switch on.
titleOne-sentence human summary. Safe to log; safe to surface to internal staff.
statusHTTP status code (mirrors the response status).
instancePath of the failing request. Useful when correlating with your own logs.
errorsPresent only on validation failures. Per-field error messages, grouped by property name.
traceIdSentry 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 four canonical cases:

StatusTriggerBody type
400FluentValidation.ValidationException — request shape or business rule rejected the input.ValidationProblemDetails (envelope above with errors populated).
403UnauthorizedAccessException raised by a guard behavior — [RequirePermission], [RequiresFeature], or [ProhibitedDuringImpersonation] rejected the call.ProblemDetails. The title names the missing permission or guard.
404A path-bound resource ({tenantId}, {contactId}, …) didn’t resolve.Empty body or minimal Result.Failure shape.
500Anything 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 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 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.

  • 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.