Rate limits
The Auradonors API limits every API key, and the anonymous endpoints, with fixed-window
counters from Microsoft.AspNetCore.RateLimiting.
Active policies
Section titled “Active policies”| 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.
429 response shape
Section titled “429 response shape”When the limiter rejects a request:
HTTP/1.1 429 Too Many RequestsContent-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.
Recommended retry strategy
Section titled “Recommended retry strategy”For automated integrations:
- Don’t retry immediately. A
429means 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. - 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. - Surface persistent
429s as alerts. Three or more consecutive429s 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.
Designing within the budget
Section titled “Designing within the budget”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
pageSizeup 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.
Deliberate non-goals (today)
Section titled “Deliberate non-goals (today)”- No
Retry-Afterheader. 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.