Skip to main content

Dated changes to the API contract and the documentation.

Changes to the v1 API contract and the guides that document it, newest first.

2026-10-19

  • The session routes (/auth/login, /auth/register, /auth/refresh, /auth/logout, the /auth/2fa/login-* steps, passkey sign-in (/auth/passkey/options, /auth/passkey/verify), password reset, email verification and the /auth/me writes) refuse a browser request sent from a site other than the DMARCguard app with 403 and code: "cross_origin". Clients that send no Origin or Sec-Fetch-Site header, such as servers and scripts, are unaffected.

2026-10-18

  • New POST /auth/passkey/options and POST /auth/passkey/verify sign a person in with a passkey, without an email or password. An account that must use single sign-on (its domain enforces it, or single sign-on created it and still links it) is refused with 403, code: "sso_required" and the account’s email, so you can start single sign-on for it; the password sign-in’s answer for such a domain carries no code. An unverified email is refused as on POST /auth/login: 403 with {"error": "email_not_verified", ...}. Every failed assertion is the same generic 401 as a failed password sign-in; the one exception is a passkey the account no longer holds, which answers that 401 plus code: "unknown_credential" and the credential_id you sent.
  • kind ("passkey" or "security_key") is now required on POST /auth/passkeys/register/verify; it must match the options request.
  • A password sign-in response can now carry passkey_upgrade, an offer to save a passkey automatically. Ignore it if you do not support it.
  • New GET /auth/passkeys, POST /auth/passkeys/register/options, POST /auth/passkeys/register/verify, PATCH /auth/passkeys/{id} and POST /auth/passkeys/{id}/remove manage a person’s passkeys and security keys. Like every /auth/2fa/* route they are session-only: an API key is refused with 403 and code: "api_key_not_allowed".
  • New POST /auth/2fa/stepup/options starts a passkey step-up for the two-factor routes, and POST /auth/2fa/login-challenge starts a passkey ceremony at the second sign-in step (and at lost-authenticator recovery). login-challenge refuses everything it cannot trace to a live challenge with the same generic 401 as login-verify.
  • webauthn joins totp in the methods a sign-in can ask for, and login-verify and login-enroll accept method: "webauthn". Ignore a method type you do not know.
  • GET /auth/2fa/status now carries factors ("totp" and/or "webauthn") and protected (any second factor is on). enabled still means an authenticator app.
  • A change to a second factor (confirming the authenticator app on POST /auth/2fa/verify, disabling it, regenerating recovery codes, and adding, renaming or removing a passkey) now answers 401 when the request carries no live session, instead of signing the other devices out. Refresh the session and sign in again.

2026-10-17

  • POST /auth/login now answers with a status field. ok is the existing response, unchanged. An account with two-factor authentication on gets mfa_required instead, with an mfa_token, its expires_at and the methods it accepts, and no session. Ignore a method type you do not know. Dashboard sign-in is the only caller; API keys are unaffected.
  • New POST /auth/2fa/login-verify and POST /auth/2fa/login-enroll finish a sign-in or a lost-authenticator recovery. Every refusal is the same 401; an exhausted attempt budget is 429 with Retry-After.
  • Every /auth/2fa/* route is session-only: a request that authenticates with an API key is refused with 403 and code: "api_key_not_allowed".
  • POST /auth/2fa/validate is removed; login-verify replaces it.

2026-10-14

  • GET /domains: each domain now carries aprf_enabled and aprf_state (APRF performance reporting; same states as dmarc_state).
  • GET /domains/{domain}/health: new dns.aprf record status (ok, warning or unknown) and aprf_placement (APRF placement data: each provider’s, DKIM domain’s and selector’s most recent report within the last 7 UTC days, today included; an empty list when none). Each row carries the provider’s version, sdi_used and extra_info; a change of version means its categories may count different actions, so compare counts only within one version. Rows are independent measurements, so do not add them together.

2026-10-07

  • GET /domains/{domain}/health: dns.mta_sts carries an optional hosted_cta (label, url) when the domain’s MTA-STS record is missing or failing, or a hosted setup is still in progress. The url is a path on the app. The dns_mta_sts action item now links there too, and a missing record raises a low item it did not before.

2026-10-02

  • GET /reports/{id} no longer adds a parse_warnings entry for a report whose version is 1 or 0.1. Large reporters send both routinely and neither says anything about the report’s structure. Any other unexpected version is still flagged.

2026-09-19

  • GET /enums returns every category the product uses, not only the ones the reference documents. The documented categories and their shape are unchanged; read the ones your integration needs by name.

2026-09-16

  • Timestamps are RFC 3339 in UTC with whole seconds on every endpoint in the reference. A field naming a day rather than a moment is YYYY-MM-DD. An instant a reporter got wrong, one no RFC 3339 date can hold, renders null. See the conventions.
  • A request that authenticates with an API key is refused with 403 on DELETE /api-keys/{id}, matching the existing rule for creating keys: a full key gets code: "api_key_cannot_revoke", a read key is stopped earlier with permission_denied. See authentication.
  • An unknown path under the base URL returns a JSON 404 instead of an HTML page.
  • Lists in documented responses are never null; an empty list is [].
  • GET /enums returns the categories the reference documents.
  • grade documents its N/A value, returned before the first report.
  • GET /domains and GET /top-sources now return a paged envelope (total, limit, offset) instead of a bare array.
  • GET /events echoes back the limit and offset it actually applied.
  • A 403 permission denial now carries code: "permission_denied" and a required field naming the permission the key lacked. See reading a rejection.
  • GET /sources/{ip} gained spam_signals, renamed from an earlier field.
  • GET /enums is now documented in the reference.
  • A malformed from or to on GET /reports now returns 400 instead of being silently dropped.
  • The domain filter on statistics, compliance score, trends, top sources and sources is now case-insensitive, and a malformed value returns 400 instead of an empty result.
  • GET /api-keys/usage documents its 503.

Search the guides, the API reference and the learn articles.