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.
| Credential | What 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.
| Scope | What it permits |
|---|---|
domains:read | Read the domains an organisation monitors. |
domains:write | Add or change a monitored domain. |
checks:run | Run a check. |
checks:read | Read past check results. |
tenant:read | Read the organisation record. |
members:read | List members and invitations. |
members:write | Invite, re-role and remove members. |
tokens:read | List tokens and their metadata - never their secrets. |
tokens:write | Mint, rotate and revoke tokens. |
dns:write | Change 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.
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": "..." } ]
}
}
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.
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.
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.
| Endpoint | What it does | Credential |
|---|---|---|
| POST /v1/checks | Run a domain check | |
| GET /v1/checks/catalog | List the checks that a run performs | |
| GET /v1/checks/history | List past check results | |
| POST /v1/checks/report-email | Ask for a check report by email | |
| POST /v1/checks/report-email/confirm | Confirm the address and receive the report | |
| GET /v1/checks/report-email/notice | The wording the check page must show beside the optional email box | |
| POST /v1/checks/report-email/unsubscribe | Never write to this address again | |
| GET /v1/orgs/{organizationId}/checks/history | List one organisation's check history |
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.
| Endpoint | What it does | Credential |
|---|---|---|
| GET /v1/domains | List monitored domains | |
| POST /v1/domains | Start monitoring a domain | |
| GET /v1/domains/{id} | Fetch one monitored domain | |
| DELETE /v1/domains/{id}/ownership | Withdraw this domain's verification | |
| GET /v1/domains/{id}/ownership | Whether this domain's ownership has been proven | |
| POST /v1/domains/{id}/ownership | Start proving control of this domain | |
| GET /v1/domains/{id}/ownership/evidence | Everywhere we looked, and what we saw | |
| POST /v1/domains/{id}/ownership/verify | Look for the proof now | |
| GET /v1/orgs/{organizationId}/domains | List one organisation's monitored domains | |
| GET /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestations | Read a domain's "Check these yourself" checklist | |
| PUT /v1/orgs/{organizationId}/domains/{monitoredDomainId}/signal-attestations/{signalCode} | Record the answer for one signal on a domain's checklist |
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.
| Endpoint | What it does | Credential |
|---|---|---|
| POST /v1/accounts/login | Sign in with email and password | |
| POST /v1/accounts/logout | End the current session | |
| GET /v1/accounts/me | The signed-in person | |
| POST /v1/accounts/resend-verification | Send the verification link again | |
| POST /v1/accounts/signup | Create an account and its first organisation | |
| POST /v1/accounts/step-up | Re-confirm the password for a privileged action | |
| POST /v1/accounts/verify-email | Prove ownership of the address | |
| POST /v1/provisional-sessions | Start an anonymous session so check results can be kept and later claimed |
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.
| Endpoint | What it does | Credential |
|---|---|---|
| GET /v1/orgs/{organizationId}/settings | List this organisation's settings | |
| PUT /v1/orgs/{organizationId}/settings/{key} | Change one of this organisation's settings | |
| POST /v1/orgs/{organizationId}/settings/{key}/reset | Drop this organisation's own value and follow the platform default again |
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.
| Endpoint | What it does | Credential |
|---|---|---|
| POST /v1/invitations/accept | Accept an invitation to join an organisation | |
| GET /v1/orgs | List the organisations you belong to | |
| POST /v1/orgs | Create another organisation | |
| GET /v1/orgs/{organizationId} | Fetch one organisation | |
| GET /v1/orgs/{organizationId}/activity | What has happened to this organisation, including access by MailGuardHQ | |
| GET /v1/orgs/{organizationId}/invitations | List an organisation's invitations | |
| POST /v1/orgs/{organizationId}/invitations | Invite somebody to the organisation | |
| GET /v1/orgs/{organizationId}/members | List an organisation's members | |
| DELETE /v1/orgs/{organizationId}/members/{seatId} | Remove a member and revoke their tokens | |
| PATCH /v1/orgs/{organizationId}/members/{seatId} | Change a member's role |
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.
| Endpoint | What it does | Credential |
|---|---|---|
| GET /v1/orgs/{organizationId}/tokens | List an organisation's tokens | |
| POST /v1/orgs/{organizationId}/tokens | Mint an API token | |
| DELETE /v1/orgs/{organizationId}/tokens/{tokenId} | Revoke one token | |
| GET /v1/orgs/{organizationId}/tokens/{tokenId} | Fetch one token's metadata | |
| POST /v1/orgs/{organizationId}/tokens/{tokenId}/rotate | Rotate a token, with overlap | |
| POST /v1/orgs/{organizationId}/tokens/revoke | Revoke many tokens at once - the breach-response path |
Health
Liveness and readiness, for whatever runs the container. Neither needs a credential and neither is a product capability.
| Endpoint | What it does | Credential |
|---|---|---|
| GET /health/dependencies | Registry dependency check - reads a real row and the schema. Not a probe. | |
| GET /health/live | Liveness probe | |
| GET /health/ready | Readiness probe |
Pricing
| Endpoint | What it does | Credential |
|---|---|---|
| GET /v1/pricing/quote | Price a plan |
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.
| Group | Operations | Why |
|---|---|---|
| Platform administration | 16 | The 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 settings | 5 | Vendor-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.
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.