Changelog
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/mewrites) refuse a browser request sent from a site other than the DMARCguard app with403andcode: "cross_origin". Clients that send noOriginorSec-Fetch-Siteheader, such as servers and scripts, are unaffected.
2026-10-18
- New
POST /auth/passkey/optionsandPOST /auth/passkey/verifysign 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 with403,code: "sso_required"and the account’semail, so you can start single sign-on for it; the password sign-in’s answer for such a domain carries nocode. An unverified email is refused as onPOST /auth/login:403with{"error": "email_not_verified", ...}. Every failed assertion is the same generic401as a failed password sign-in; the one exception is a passkey the account no longer holds, which answers that401pluscode: "unknown_credential"and thecredential_idyou sent. kind("passkey"or"security_key") is now required onPOST /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}andPOST /auth/passkeys/{id}/removemanage a person’s passkeys and security keys. Like every/auth/2fa/*route they are session-only: an API key is refused with403andcode: "api_key_not_allowed". - New
POST /auth/2fa/stepup/optionsstarts a passkey step-up for the two-factor routes, andPOST /auth/2fa/login-challengestarts a passkey ceremony at the second sign-in step (and at lost-authenticator recovery).login-challengerefuses everything it cannot trace to a live challenge with the same generic401aslogin-verify. webauthnjoinstotpin themethodsa sign-in can ask for, andlogin-verifyandlogin-enrollacceptmethod: "webauthn". Ignore a method type you do not know.GET /auth/2fa/statusnow carriesfactors("totp"and/or"webauthn") andprotected(any second factor is on).enabledstill 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 answers401when 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/loginnow answers with astatusfield.okis the existing response, unchanged. An account with two-factor authentication on getsmfa_requiredinstead, with anmfa_token, itsexpires_atand themethodsit 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-verifyandPOST /auth/2fa/login-enrollfinish a sign-in or a lost-authenticator recovery. Every refusal is the same401; an exhausted attempt budget is429withRetry-After. - Every
/auth/2fa/*route is session-only: a request that authenticates with an API key is refused with403andcode: "api_key_not_allowed". POST /auth/2fa/validateis removed;login-verifyreplaces it.
2026-10-14
GET /domains: each domain now carriesaprf_enabledandaprf_state(APRF performance reporting; same states asdmarc_state).GET /domains/{domain}/health: newdns.aprfrecord status (ok,warningorunknown) andaprf_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’sversion,sdi_usedandextra_info; a change ofversionmeans 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_stscarries an optionalhosted_cta(label,url) when the domain’s MTA-STS record is missing or failing, or a hosted setup is still in progress. Theurlis a path on the app. Thedns_mta_stsaction item now links there too, and a missing record raises alowitem it did not before.
2026-10-02
GET /reports/{id}no longer adds aparse_warningsentry for a report whoseversionis1or0.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 /enumsreturns 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, rendersnull. See the conventions. - A request that authenticates with an API key is refused with
403onDELETE /api-keys/{id}, matching the existing rule for creating keys: a full key getscode: "api_key_cannot_revoke", a read key is stopped earlier withpermission_denied. See authentication. - An unknown path under the base URL returns a JSON
404instead of an HTML page. - Lists in documented responses are never
null; an empty list is[]. GET /enumsreturns the categories the reference documents.gradedocuments itsN/Avalue, returned before the first report.GET /domainsandGET /top-sourcesnow return a paged envelope (total,limit,offset) instead of a bare array.GET /eventsechoes back thelimitandoffsetit actually applied.- A
403permission denial now carriescode: "permission_denied"and arequiredfield naming the permission the key lacked. See reading a rejection. GET /sources/{ip}gainedspam_signals, renamed from an earlier field.GET /enumsis now documented in the reference.- A malformed
fromortoonGET /reportsnow returns400instead of being silently dropped. - The
domainfilter on statistics, compliance score, trends, top sources and sources is now case-insensitive, and a malformed value returns400instead of an empty result. GET /api-keys/usagedocuments its503.