Skip to content

Rate limits

The Auradonors API limits every API key, and the anonymous endpoints, with fixed-window counters from Microsoft.AspNetCore.RateLimiting.

Policy Limit Window Where it’s attached
Per API key 120 requests 1 minute Every request made with X-Api-Key, counted per key. A request whose key doesn’t authenticate is counted against its source IP, so guessing keys is limited too.
auth-sign-in 60 requests 10 minutes POST /api/auth/login, per source IP.
auth-recovery 10 requests 10 minutes POST /api/auth/forgot-password and /reset-password, per source IP.
contact-form 5 requests 15 minutes POST /api/contact-form (the public marketing form), per source IP.
mfa-verify 5 attempts 15 minutes MFA verify and recovery-code endpoints, per user. Cookie sign-in only — not relevant to API key callers.
mfa-send-email 3 sends 1 hour The MFA email-code send endpoint, per user. Cookie sign-in only.

Signing in is also limited per account, separately from the table above: ten wrong passwords for one address within 15 minutes block that address for 15 minutes (400 Auth.TooManyAttempts), even for the right password. See the changelog.

The public event pages, the signing pages and the phone system lookup carry limits of their own, per source IP; they are not called with an API key.

When the limiter rejects a request:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "message": "Too many requests. Please try again later." }

The response is a flat JSON object today, not an RFC 7807 ProblemDetails envelope. A future revision is expected to align it with the rest of the error model. Treat the body as opaque and switch on response.statusCode == 429.

The platform does not currently emit a Retry-After header. See the backoff guidance below for recommended pacing.

For automated integrations:

  1. Don’t retry immediately. A 429 means the server has measured that you’re over budget; an immediate retry will burn through the budget the moment the window resets and trip again.
  2. Back off exponentially with jitter. Start at one minute, double on each consecutive 429, cap at fifteen minutes. Add ±20 % jitter so a restart of multiple workers doesn’t hammer the same window.
  3. Surface persistent 429s as alerts. Three or more consecutive 429s on the same logical operation indicates a sizing problem — your integration is doing more than the policy allows.
async Task<HttpResponseMessage> SendWithBackoffAsync(HttpRequestMessage request)
{
var delay = TimeSpan.FromMinutes(1);
while (true)
{
var response = await http.SendAsync(request);
if ((int)response.StatusCode != 429) return response;
// ±20% jitter so synchronized clients don't all retry at the same instant.
var jitter = Random.Shared.NextDouble() * 0.4 - 0.2;
await Task.Delay(delay * (1 + jitter));
delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, TimeSpan.FromMinutes(15).Ticks));
}
}

For interactive callers (a person waiting on a UI), surface a “try again in a moment” message instead of retrying silently.

A key’s minute is its own: one integration’s calls don’t use up another’s. To stay inside it:

  • Batch where you can. Most list endpoints accept pageSize up to reasonable upper bounds (see Pagination).
  • Cache reference data. Funds, departments, batch sources, contact sources, and tag categories don’t change often; pull them once per process start.
  • Use webhooks rather than poll loops. When a downstream change triggers your work, Webhooks is the lower-impact path.
  • Avoid synchronous fan-out from a single user action. A donor click shouldn’t translate to a hundred outbound calls.
  • No Retry-After header. Use the backoff guidance above.
  • No usage / quota dashboard. No view of your current rate-limit state. Treat the policies as soft constraints and design accordingly.
  • No X-RateLimit-* advisory headers.