Skip to content

Permissions, rate limits and request changes

Date: 2026-10-20

An integration whose API key holds the scopes for what it does is not affected by the permission changes below.

  • Dashboard widgets need their permission. GET /dashboard/kpi/{key}, /dashboard/list/{key} and /dashboard/chart/{key} answer 403 Forbidden unless the caller holds the permission the widget needs — for example users:view for list.recent-mfa-attempts, orders:refund for list.recent-refunds, orders:view for kpi.ytd-giving. The four chart endpoints under /dashboard/charts/ need orders:view (donation trend, by fund, payment methods) or contacts:view (donor growth). An unknown key is still 400.
  • GET /dashboard/summary and /dashboard/home say which figures they left out. A figure the caller has no permission for is sent as zero, and new fields say which: donorsVisible, donationsVisible and recurringRevenueVisible on the summary; donationTrendVisible, donationsByFundVisible, paymentMethodsVisible and donorGrowthVisible on the home payload. Read the flag before treating a zero as a zero.
  • GET /audit-logs/{id} needs audit-logs:view, as the list does.
  • GET /users/{userId}/permission-overrides needs users:manage-permissions.
  • POST /uploads/images needs a permission that puts a picture somewhere: writing a letter, packing slip, tax statement or contact email template, setting up an event, or managing the organization’s settings. It accepts PNG, JPEG, GIF and WebP, checked by the file’s contents.
  • POST /onboarding/complete needs import:manage.
  • POST /api-keys takes only scopes the caller holds, and answers ApiKey.ScopeNotHeld naming any others.
  • POST /orders: a taxAmountOverride needs orders:override-tax (Order.TaxOverrideForbidden), and a shippingAmountOverride other than the chosen shipping option’s own rate needs orders:override-shipping (Order.ShippingOverrideForbidden), as the handling fee does.
  • PUT /tasks/{id}: changing who a task is assigned to needs tasks:assign (DonorTask.ReassignForbidden), unless the caller is taking the task themselves.
  • POST /fulfillment/packing-slips/generate returns one PDF (application/pdf) rather than an HTML page.
  • POST /api/auth/change-password changes the signed-in person’s own password; the body takes no userId. It answers 200 OK with { "sessionStamp": "…" }, and a new password ends the person’s other sessions.
  • POST /api/auth/reset-password answers Auth.InvalidRequest for a link that doesn’t work, where it answered InvalidToken.
  • POST /api/auth/session-status is new: whether a signed-in session may go on. The web app asks it; an integration has no need to.
  • POST /api/auth/login and POST /api/auth/mfa/verify ignore an ipAddress in the body; the address is the connection’s.
  • Every CSV and Excel export writes a value that starts with =, +, -, @, a tab or a carriage return with a leading apostrophe (in Excel, the quote-prefix style), so a spreadsheet shows it as text. A plain number or phone number keeps its sign. The customer data export is the records as they were typed.
  • POST /imports/upload refuses a workbook that unpacks to more than 250 MB or holds more than 10,000 parts (Import.FileTooLarge).
  • Anonymous endpoints read a bounded body. Sign-in, two-factor, the contact form, signing and the phone system’s endpoints accept at most 64 KB; the public event pages 256 KB; payment processor webhooks 1 MB. A larger body is refused with 413 Payload Too Large.
  • API responses carry security headers: Content-Security-Policy: default-src 'none', X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, and Strict-Transport-Security over HTTPS.
  • Each API key may make 120 requests a minute. A request past that is answered 429 Too Many Requests; each key has its own minute. A request whose key doesn’t authenticate is counted against its address.
  • Signing in is limited. Ten wrong passwords for one address within 15 minutes block it for 15 minutes, and longer if it happens again within a day: 400 with Auth.TooManyAttempts, even for the right password. Five wrong current passwords within 15 minutes block a password change the same way. POST /api/auth/login, /forgot-password and /reset-password are also limited per address (429).

See Rate limits.

  • A webhook address must be a public HTTPS host name. POST and PUT /webhooks refuse an IP address as the host, localhost, a single-label name, a name under .local, .localhost, .internal, .lan, .home.arpa, .intranet or .corp, and a user name or password in the address. A delivery or test send to a name that resolves to a private, loopback or link-local address isn’t sent; its error reads “The address isn’t on the public internet, so nothing was sent.”
  • Redirects aren’t followed — a 3xx answer is a failed attempt. Each delivery attempt is one request, on the same retry schedule.
  • POST /webhooks/{id}/test signs the test as deliveries are signed (sha256={hex}), and its errorMessage is a sentence.