Skip to content
MailGuardHQ
REST API

The MailGuardHQ REST API

Everything the product does is an API call - the web app is just another caller, with no back door of its own. 50 operations across 8 groups, one error shape, one auth model, and a contract you can generate a client from.

Start here

The one call that needs nothing from you: run the same thirteen checks the home page runs, on any domain, with no account and no credential.

Your first call

curl -s https://api.mailguardhq.com/v1/checks \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2b9e-4a77-4f1c-9f0e-2c1d8b5a3e77" \
  -d '{"domain":"yourcompany.com"}'

Everything else needs a token. A free account is enough to mint one - sign up, verify your address, and mint it from your organisation. Free carries one token at a time, a maximum lifetime of 30 days, and never dns:write; the paid plans raise both numbers.

The contract is the API, not a description of it

The OpenAPI document below is what the API is built from, not a document written about it afterwards - a test fails when the served document and the published one differ. Generate your client from it rather than hand-writing one: it is the same document our own MCP server's tools are generated from.

https://api-dev.mailguardhq.com/openapi/v1.json - public, and no credential needed to read it.

Authentication

Send Authorization: Bearer <token>. Two kinds of credential are accepted and they are not interchangeable.

The two credential kinds
CredentialWhat it is
API token
mgh_live_... / mgh_test_...
Minted from your organisation, bound to ONE organisation and to the seat of the person who minted it, and always expiring. This is what a script, a CI job or an MCP client uses.
Session token
mgh_sess_...
Returned by sign-in, not bound to an organisation, and the only credential accepted by the routes that change who can reach an account - signing out, stepping up, and accepting an invitation. A machine credential must never do those on a person's behalf.

Scopes

Scopes are resource:action and never imply one another: domains:write does not grant domains:read. Ask for both if you need both.

The scope vocabulary
ScopeWhat it permits
domains:readRead the domains an organisation monitors.
domains:writeAdd or change a monitored domain.
checks:runRun a check.
checks:readRead past check results.
tenant:readRead the organisation record.
members:readList members and invitations.
members:writeInvite, re-role and remove members.
tokens:readList tokens and their metadata - never their secrets.
tokens:writeMint, rotate and revoke tokens.
dns:writeChange a customer's DNS. Never granted to an MCP token, and minting it requires a password re-confirmation in the last five minutes - which an API token can never satisfy.

What a request may actually do is the intersection of four ceilings, recomputed on every request rather than decided once at minting: the token's scopes, your current role, the token's audience (api or mcp), and what your plan sells. A demotion or a downgrade therefore takes effect immediately, on tokens that already exist.

There is no preset picker yet. You name the scopes when you mint, and asking for one above any of the four ceilings is refused at that point rather than silently trimmed - so a token never quietly carries less than you think it does.

Conventions

One error shape, everywhere

Every failure on every endpoint returns the same body. Branch on code, never on message - the code is stable, the message is written for a human and may be reworded. action says what to do about it, and correlationId is the value to quote if you need to ask us what happened: it appears on the same request in our logs.

The error envelope

{
  "error": {
    "code": "wrong_audience",
    "message": "This token was issued for the \"mcp\" surface and cannot be used here.",
    "action": "Mint a token for the surface you are calling.",
    "correlationId": "0HN7A2C4E9F1K",
    "details": [ { "field": "scopes", "issue": "..." } ]
  }
}

Never a 200 with an error inside it, and never a stack trace, a SQL fragment or an upstream vendor payload. Across an organisation boundary the answer is 404 rather than 403, because a 403 would confirm the resource exists.

Every collection is paginated

Including ones that look small today. Send page and pageSize; the default page size is 100 and there is an explicit maximum. The envelope is the same for every collection:

A page of results

{
  "items": [ ... ],
  "page": 1,
  "pageSize": 100,
  "totalCount": 243,
  "totalPages": 3,
  "hasMore": true
}

Idempotency on writes

Every non-GET accepts an Idempotency-Key header - any unique value, a UUID is fine. Repeat a call with the same key and the original response is replayed instead of the work happening twice. A client that times out cannot tell whether its write landed, and without this its retry acts twice.

The same key with a different body is a client bug rather than a retry, and is answered 409 - replaying a response that does not correspond to what was asked would be the more dangerous silent behaviour.

Rate limits

Three allowances, because three surfaces have different risks: the anonymous check, the signup and sign-in routes, and authenticated traffic per credential and per plan. Over the line you get 429 and a Retry-After header saying when to come back. Requests made from the interactive console are ordinary requests and meet the same limiter.

Versioning

The version is in the path: /v1/.... A shipped version is never mutated in place. Additive changes - a new optional field, a new endpoint, a new value you can ignore - stay inside the current version; anything you could break on gets a new one, and the old one keeps working and keeps being tested until its published sunset date.

v1 is current and nothing is deprecated, so no Deprecation or Sunset headers are being emitted yet.

Every endpoint

Generated from the contract, so this list cannot describe a route the API does not serve. "Credential" is read from the same endpoint metadata the authentication middleware enforces.

Checks

The thirteen checks, and the history of runs. Running a check needs no credential - it is the same call the free check on the home page makes - and sending one authenticated records the result against your organisation instead.

Checks endpoints
EndpointWhat it doesCredential
POST /v1/checksRun a domain checkNone
GET /v1/checks/catalogList the checks that a run performsNone
GET /v1/checks/historyList past check resultsOptional
POST /v1/checks/report-emailAsk for a check report by emailNone
POST /v1/checks/report-email/confirmConfirm the address and receive the reportNone
GET /v1/checks/report-email/noticeThe wording the check page must show beside the optional email boxNone
POST /v1/checks/report-email/unsubscribeNever write to this address againNone
GET /v1/orgs/{organizationId}/checks/historyList one organisation's check historyRequired

Domains

The domains an organisation monitors. Adding one is a data change: no deploy, no code, and the domain starts in a safe observe state with enforcement off.

Domains endpoints
EndpointWhat it doesCredential
GET /v1/domainsList monitored domainsRequired
POST /v1/domainsStart monitoring a domainRequired
GET /v1/domains/{id}Fetch one monitored domainRequired
DELETE /v1/domains/{id}/ownershipWithdraw this domain's verificationRequired
GET /v1/domains/{id}/ownershipWhether this domain's ownership has been provenRequired
POST /v1/domains/{id}/ownershipStart proving control of this domainRequired
GET /v1/domains/{id}/ownership/evidenceEverywhere we looked, and what we sawRequired
POST /v1/domains/{id}/ownership/verifyLook for the proof nowRequired
GET /v1/orgs/{organizationId}/domainsList one organisation's monitored domainsRequired
GET /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestationsRead a domain's "Check these yourself" checklistRequired
PUT /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestations/{signalCode}Record the answer for one signal on a domain's checklistRequired

Accounts

Signing up, proving an address, signing in and reading who you are. Most of these are browser flows - they take a password or consume a link from an email, which is why the MCP server does not expose any of them.

Accounts endpoints
EndpointWhat it doesCredential
POST /v1/accounts/loginSign in with email and passwordNone
POST /v1/accounts/logoutEnd the current sessionRequired
GET /v1/accounts/meThe signed-in personRequired
POST /v1/accounts/resend-verificationSend the verification link againNone
POST /v1/accounts/signupCreate an account and its first organisationNone
POST /v1/accounts/step-upRe-confirm the password for a privileged actionRequired
POST /v1/accounts/verify-emailProve ownership of the addressNone
POST /v1/provisional-sessionsStart an anonymous session so check results can be kept and later claimedNone

Account settings

The settings that belong to your organisation - readable, changeable and audited, each one bounded by a platform value above it that you do not control.

Account settings endpoints
EndpointWhat it doesCredential
GET /v1/orgs/{organizationId}/settingsList this organisation's settingsRequired
PUT /v1/orgs/{organizationId}/settings/{key}Change one of this organisation's settingsRequired
POST /v1/orgs/{organizationId}/settings/{key}/resetDrop this organisation's own value and follow the platform default againRequired

Organisations

The organisation an account belongs to, the seats on it, and the invitations outstanding. Across an organisation boundary the answer is 404, never 403.

Organisations endpoints
EndpointWhat it doesCredential
POST /v1/invitations/acceptAccept an invitation to join an organisationRequired
GET /v1/orgsList the organisations you belong toRequired
POST /v1/orgsCreate another organisationRequired
GET /v1/orgs/{organizationId}Fetch one organisationRequired
GET /v1/orgs/{organizationId}/activityWhat has happened to this organisation, including access by MailGuardHQRequired
GET /v1/orgs/{organizationId}/invitationsList an organisation's invitationsRequired
POST /v1/orgs/{organizationId}/invitationsInvite somebody to the organisationRequired
GET /v1/orgs/{organizationId}/membersList an organisation's membersRequired
DELETE /v1/orgs/{organizationId}/members/{seatId}Remove a member and revoke their tokensRequired
PATCH /v1/orgs/{organizationId}/members/{seatId}Change a member's roleRequired

Tokens

Minting, listing, rotating and revoking API credentials, including the breach-response path that revokes many at once. Rotation deliberately leaves the original active, so a rotation is not an outage.

Tokens endpoints
EndpointWhat it doesCredential
GET /v1/orgs/{organizationId}/tokensList an organisation's tokensRequired
POST /v1/orgs/{organizationId}/tokensMint an API tokenRequired
DELETE /v1/orgs/{organizationId}/tokens/{tokenId}Revoke one tokenRequired
GET /v1/orgs/{organizationId}/tokens/{tokenId}Fetch one token's metadataRequired
POST /v1/orgs/{organizationId}/tokens/{tokenId}/rotateRotate a token, with overlapRequired
POST /v1/orgs/{organizationId}/tokens/revokeRevoke many tokens at once - the breach-response pathRequired

Health

Liveness and readiness, for whatever runs the container. Neither needs a credential and neither is a product capability.

Health endpoints
EndpointWhat it doesCredential
GET /health/dependenciesRegistry dependency check - reads a real row and the schema. Not a probe.None
GET /health/liveLiveness probeNone
GET /health/readyReadiness probeNone

Pricing

Pricing endpoints
EndpointWhat it doesCredential
GET /v1/pricing/quotePrice a planNone

21 operations are deliberately not listed

The contract declares 71 operations. 21 of them are the platform's own administration surface and are not documented here - not because the page is out of date, but because listing them would undo what they are built to do. They are refused to you whether or not you know their names.

Groups this page withholds
GroupOperationsWhy
Platform administration16The support and administration surface. MGH-119 makes it answer a non-admin with the same not_found as a route that does not exist, so listing it here would tell an outsider exactly what to probe for.
Platform settings5Vendor-controlled settings that apply to every tenant at once. Same reason, and the blast radius of one value is the whole platform rather than one account.

Trying it interactively

The Swagger console is gated - you cannot just click into it

The API also serves an interactive console, rendered from the same document linked above. It is not open to the public: without a MailGuardHQ credential it answers 401, and from outside an allowed network it answers 404 so that it is not even discoverable.

That is deliberate. A published contract is a document; a console is a request generator aimed at the live API and hosted on our own origin, and the failure mode of every browsable console is that it acquires a credential of its own. Ours never does - it sends only the token you type into it, so it can never do more than you could.

Open the console if you have an account. If you do not, the document above is public and a client generated from it will behave identically.

This page is generated from a checked-in copy of the OpenAPI document (1591d642ef32, 71 operations). A copy tells you which revision you are reading; the live document at the link above is always authoritative.

Or let an AI client do the calling

The same API is exposed as MCP tools, generated from this contract, so an assistant can operate the product with exactly the permissions your token carries.

Read about the MCP server Check a domain