ShieldMX OpenAPI llms.txt

API reference · v1

ShieldMX API

Check a signup email before the account exists. Each check returns ALLOW, REVIEW or REJECT, with a reason and a risk score, in one request.

Overview

The API has two endpoints: one to check a single address, and one to check up to 100 at once. Both accept JSON over HTTPS and return JSON. Everything lives under /v1.

  • Base URL: https://shieldmx-core-api.slotboard-v.workers.dev
  • Format: UTF-8 JSON in and out. Send Content-Type: application/json.
  • Auth: a bearer key from your dashboard.
  • Addresses are not stored: the verify call does not save the address. Usage counts are kept per key.

There are no SDKs to install. Any HTTP client works. Examples below use curl, Node.js and Python.

Quickstart

Create a key in the dashboard under API keys, then run one request.

# Check one address
curl -X POST https://shieldmx-core-api.slotboard-v.workers.dev/v1/verify \
  -H "Authorization: Bearer $SHIELDMX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "someone@relay.firefox.com"}'

The response is a verdict. The alias check returns REVIEW, because privacy aliases belong to real people:

200 OK
{
  "valid_syntax": true,
  "email": "someone@relay.firefox.com",
  "domain": "relay.firefox.com",
  "disposable": false,
  "alias": true,
  "matched_domain": "relay.firefox.com",
  "mx_valid": true,
  "recommendation": "REVIEW",
  "reason": "privacy_alias",
  "risk_score": 50,
  "suggestion": null,
  "mode": "live",
  "evaluated_at": 1792841446
}

Try it in the browser

Paste a test key (it starts with smx_test_) and an address, then run the check. The request goes straight from this page to the API. The key stays in this tab and is not saved anywhere.

Only test keys work here. The API adds the cross-origin header only to responses for test keys. A live key sent from a browser still runs, and still counts toward quota, but the page cannot read the response. Keep live keys on your server.
Response
Run a check to see the verdict here.

Authentication

Send your key as a bearer token on every request:

Header
Authorization: Bearer smx_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Live keys start with smx_live_. They count toward your plan's monthly quota.
  • Test keys start with smx_test_. They run the same checks and do not count toward quota. See Test mode.
  • A key is shown once, when you create it. If you lose it, revoke it and create another.
  • Revoking a key stops it working on the next call.
Keep keys on the server. Never ship a key to a browser, a mobile app, or a public repository. Anyone with the key can spend your quota.

Verify one address

POST/v1/verify

Checks one address and returns one verdict.

Request body

FieldTypeRequiredDescription
emailstringyesThe address to check. 5 to 254 characters. Trimmed and lowercased before the check. No other fields are allowed.

Query parameters

ParameterDefaultDescription
mxtrueSet to false to skip the mail-server lookup. The verdict then comes from the list and your rules only.

Response fields

FieldTypeMeaning
valid_syntaxbooleanAlways true on a 200. Malformed input returns a 400 instead.
emailstringThe address, trimmed and lowercased.
domainstringThe part after the @.
disposablebooleantrue when the domain or one of its parents is on the disposable list.
aliasbooleantrue for privacy alias services such as Firefox Relay or SimpleLogin.
matched_domainstring or nullThe list entry that matched, for example mailinator.com for a.mailinator.com.
mx_validboolean or nullWhether the domain can receive mail. null when unknown, skipped, or already decided by a rule or the list.
recommendationstringALLOW, REVIEW or REJECT. See Verdicts and reasons.
reasonstringWhy the recommendation was made.
risk_scoreinteger0 to 100. Higher is riskier.
suggestionstring or nullA likely intended provider, for example gmail.com for gmial.com.
modestringlive or test, matching the key.
evaluated_atintegerUnix time in seconds when the verdict was made.

Verify a batch

POST/v1/verify/batch

Checks up to 100 addresses in one request. Use it for signup backfills, imports and re-checks.

Request body

JSON
{
  "emails": [
    "a@relay.firefox.com",
    "b@mailinator.com",
    "c@example.com"
  ]
}
  • emails is required and holds 1 to 100 strings.
  • Results come back in the same order as the input.
  • An address with bad syntax gets its own error object. The rest of the batch still runs.
  • The batch is refused with 429 QUOTA_EXCEEDED if the well-formed addresses in it are more than your remaining monthly quota. Nothing is checked in that case.

Response

200 OK
{
  "results": [
    { "email": "a@relay.firefox.com", "recommendation": "REVIEW", "reason": "privacy_alias", "...": "..." },
    { "email": "b@mailinator.com", "recommendation": "REJECT", "reason": "disposable_domain", "...": "..." },
    { "valid_syntax": false, "email": "c@", "error": { "code": "INVALID_SYNTAX", "message": "..." } }
  ],
  "summary": {
    "submitted": 3,
    "checked": 2,
    "rejected": 1,
    "reviewed": 1,
    "allowed": 0,
    "invalid": 1
  }
}

Each domain is looked up once per batch, so many addresses on the same provider cost one mail-server check.

Verdicts and reasons

Every verdict is decided in this order. The first rule that matches wins:

OrderConditionRecommendationReasonRisk
1A customer deny rule matches the domainREJECTcustomer_denied100
2A customer allow rule matches the domainALLOWcustomer_allowed0
3The domain or a parent is on the disposable listREJECTdisposable_domain100
4The domain has no mail server (including a null MX)REJECTno_mail_server90
5A privacy alias serviceREVIEWprivacy_alias50
6Nothing matchedALLOWclean0

What to do with each recommendation

  • ALLOW: let the signup through.
  • REVIEW: let the signup through only after an extra step, such as a card on file or a captcha. Real people use alias services, so don't refuse them outright.
  • REJECT: refuse the signup, with a generic message. Log the reason, not the address.
A rule beats the list. If you allow mailinator.com, an address there returns ALLOW with customer_allowed, even though disposable is still true.

Errors

Errors use one shape:

Error body
{
  "error": {
    "code": "INVALID_SYNTAX",
    "message": "The provided value fails standardized RFC 5322 parsing evaluations."
  }
}
StatusCodeWhen it happensWhat to do
400MALFORMED_JSONThe body is not JSON, or it has extra or missing fields.Fix the request. Don't retry unchanged.
400INVALID_SYNTAXThe address fails RFC 5322 checks.Show the user a clear message.
400INVALID_BATCHThe batch is empty or has more than 100 addresses.Split the batch.
401UNAUTHORIZEDThe key is missing, malformed, invalid or revoked.Check the key. Don't retry.
404NOT_FOUNDNo route matches.Check the path.
429RATE_LIMITEDMore than 60 requests a minute on one key.Wait for Retry-After seconds, then retry.
429QUOTA_EXCEEDEDYour monthly quota is used up.Upgrade, or wait for the next month.
503BLOCKLIST_UNAVAILABLEThe disposable list is not available yet.Retry shortly. Never treat this as an allow.
Fail closed. A 503 or a network error means the check did not run. Retry it with a short backoff. If it still fails, show "try again" rather than letting the signup start a free trial.

Rate limits and quotas

Rate limit

Each key may make 60 requests a minute. Going over returns 429 RATE_LIMITED with a Retry-After: 60 header. The limit is counted per data center, so it is approximate. It protects against bursts, it is not an exact global quota.

Monthly quota

PlanChecks a monthPrice
Free500$0
Pro50,000$9 a month (planned, may change before general availability)
  • Every well-formed live-mode address counts once. A batch counts one per well-formed address.
  • Requests with bad syntax, and requests refused by the rate limit, do not count.
  • The quota resets at the start of each calendar month, in UTC.

Test mode

Test keys start with smx_test_. They call the same endpoints and get the same response shape, with "mode": "test". They don't count toward quota, and they don't appear in your analytics.

Use a test key in CI, staging, and the tests for your signup flow. Switch to a live key only in production.

Domain rules

Rules let you decide the outcome for specific domains. You manage them in the dashboard under Domain rules. Each rule has a domain and an action.

  • Allow a partner domain, so it always returns ALLOW.
  • Deny a domain you never want, so it always returns REJECT.
  • A rule covers its subdomains. The most specific rule wins: ops.partner.com can be denied while partner.com is allowed.
  • You can have up to 500 rules.

Rules apply to the next check. A change applies at once in the region where you made it, and everywhere within about a minute.

Rules are dashboard routes. They use your signed-in session, not an API key, so they are not part of the key-based API. Use the dashboard to change them.

Blocklist and mail checks

Disposable list

  • More than 9,900 known disposable domains, refreshed once a day from a public source.
  • Matching is by domain and its parents. If mailinator.com is listed, so is anything.mailinator.com.
  • If a refresh fails, the last good copy keeps serving.

Mail-server check

  • We ask DNS whether the domain has an MX record. If it has none, we check for an A record, which mail servers fall back to.
  • A null MX (0 .) or a domain that does not exist means the domain cannot receive mail, so the verdict is REJECT.
  • Results are cached for an hour.
  • If the lookup times out or fails, mx_valid is null and the address is not blocked on that basis.
  • Turn the check off per request with ?mx=false.

Integration recipes

Every recipe does the same three things: call the API with a server-side key, act on recommendation, and fail closed when the check cannot run. None of them send the key to the browser.

Next.js (App Router)

app/api/signup/route.ts
// app/api/signup/route.ts
const BASE = "https://shieldmx-core-api.slotboard-v.workers.dev";

export async function POST(req: Request) {
  const { email } = await req.json();

  let verdict;
  try {
    const res = await fetch(BASE + "/v1/verify", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SHIELDMX_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    verdict = await res.json();
  } catch {
    // Fail closed: no account is created while the check cannot run.
    return Response.json({ error: "We could not verify your email right now. Please try again." }, { status: 503 });
  }

  if (verdict.recommendation === "REJECT") {
    console.log("signup rejected", verdict.reason, verdict.domain);
    return Response.json({ error: "We couldn't create an account with that address." }, { status: 422 });
  }

  // REVIEW: create the account, but start the trial only after a card or captcha.
  return Response.json({ ok: true, review: verdict.recommendation === "REVIEW" }, { status: 201 });
}

Express

server.js
const BASE = "https://shieldmx-core-api.slotboard-v.workers.dev";

app.post("/signup", express.json(), async (req, res) => {
  let verdict;
  try {
    const r = await fetch(BASE + "/v1/verify", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + process.env.SHIELDMX_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email: req.body.email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!r.ok) throw new Error("HTTP " + r.status);
    verdict = await r.json();
  } catch {
    return res.status(503).json({ error: "We could not verify your email right now. Please try again." });
  }

  if (verdict.recommendation === "REJECT") {
    console.log("signup rejected", verdict.reason, verdict.domain);
    return res.status(422).json({ error: "We couldn't create an account with that address." });
  }

  // REVIEW: create the account, but start the trial only after a card or captcha.
  res.status(201).json({ ok: true, review: verdict.recommendation === "REVIEW" });
});

Cloudflare Worker

src/index.ts
// src/index.ts, deployed with: wrangler secret put SHIELDMX_API_KEY
export interface Env {
  SHIELDMX_API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { email } = (await request.json()) as { email: string };

    const res = await fetch("https://shieldmx-core-api.slotboard-v.workers.dev/v1/verify", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.SHIELDMX_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) {
      return Response.json({ error: "We could not verify your email right now." }, { status: 503 });
    }

    const verdict = (await res.json()) as { recommendation: string; reason: string };
    if (verdict.recommendation === "REJECT") {
      return Response.json({ error: "We couldn't create an account with that address." }, { status: 422 });
    }
    return Response.json({ ok: true, review: verdict.recommendation === "REVIEW" }, { status: 201 });
  },
};

Supabase Edge Function

Deno
// supabase/functions/signup/index.ts
// Deploy with: supabase secrets set SHIELDMX_API_KEY=smx_live_...
Deno.serve(async (req) => {
  const { email } = await req.json();

  const res = await fetch("https://shieldmx-core-api.slotboard-v.workers.dev/v1/verify", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${Deno.env.get("SHIELDMX_API_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email }),
    signal: AbortSignal.timeout(3000),
  });
  if (!res.ok) {
    return Response.json({ error: "We could not verify your email right now." }, { status: 503 });
  }

  const verdict = await res.json();
  if (verdict.recommendation === "REJECT") {
    return Response.json({ error: "We couldn't create an account with that address." }, { status: 422 });
  }
  return Response.json({ ok: true, review: verdict.recommendation === "REVIEW" }, { status: 201 });
});

FastAPI

main.py
# pip install fastapi httpx
import os
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

BASE = "https://shieldmx-core-api.slotboard-v.workers.dev"
app = FastAPI()

class Signup(BaseModel):
    email: str

@app.post("/signup", status_code=201)
async def signup(body: Signup):
    try:
        async with httpx.AsyncClient(timeout=3) as client:
            r = await client.post(
                f"{BASE}/v1/verify",
                headers={"Authorization": f"Bearer {os.environ['SHIELDMX_API_KEY']}"},
                json={"email": body.email},
            )
            r.raise_for_status()
            verdict = r.json()
    except httpx.HTTPError:
        raise HTTPException(503, "We could not verify your email right now. Please try again.")

    if verdict["recommendation"] == "REJECT":
        raise HTTPException(422, "We couldn't create an account with that address.")
    # REVIEW: create the account, but start the trial only after a card or captcha.
    return {"ok": True, "review": verdict["recommendation"] == "REVIEW"}

Agent setup prompt

Paste the prompt below into your coding agent, such as Claude Code, Cursor or Codex, in the project where your signup lives. It tells the agent where the contract is, what to build, and how to handle every verdict and failure.

Prompt for your coding agent
You are integrating ShieldMX into this project to stop free-trial abuse at signup.

Read these first:
- https://shieldmx-core-api.slotboard-v.workers.dev/openapi.json   (the API contract)
- https://shieldmx-core-api.slotboard-v.workers.dev/llms.txt       (behaviour summary)

Then do this:

1. Add SHIELDMX_API_KEY to the server environment. Never expose it to the browser, a mobile bundle, or a commit.

2. Create one server-side function, checkEmail(email), that calls
   POST https://shieldmx-core-api.slotboard-v.workers.dev/v1/verify
   with headers "Authorization: Bearer " and "Content-Type: application/json",
   and the body {"email": email}. Use the platform fetch with a 3 second timeout.
   Do not add a dependency.

3. Call checkEmail in the signup handler, before the account or the free trial is created.

4. Map the verdict on "recommendation":
   - ALLOW: continue.
   - REVIEW: continue, but require one extra step before the free trial starts
     (a card on file or a captcha). Do not refuse these.
   - REJECT: refuse the signup with a generic message such as "We couldn't create an account with that address."
     Log the "reason" field and the domain. Never log the full address.

5. Failures: on 429, 503, a timeout or a network error, retry twice with backoff (500 ms, then 1500 ms).
   If it still fails, show "We couldn't verify your email right now. Please try again." Do not let the signup start a free trial.
   On 401, stop and report it: the key is wrong or revoked.

6. Add tests for the three verdicts. Use a test key (it starts with smx_test_) so tests do not use quota.
   Stub the HTTP call in unit tests.

7. Keep the change small. Tell me which files you touched and how to set the environment variable.

Give your agent the contract

Agents work best from the machine-readable files. Point them at these:

  • openapi.json: the full OpenAPI 3.1 contract, for codegen and client generation.
  • llms.txt: a short behaviour summary written for language models.
  • This page: the reference you are reading now.

Accuracy

We test the verdict logic against a labeled set of known cases, and the set runs as part of the test suite on every build. Here are the current results.

CheckResultCases
Disposable domains rejected100%200
Well-known providers wrongly rejected or reviewed0%41
Privacy alias domains sent to REVIEW100%6
Typos given the right suggestion100%6
What these numbers cover. They measure the blocklist, the domain rules and the typo and alias logic. The mail-server check is off in this test, so the results do not depend on DNS. The cases are known domains, chosen so the answer can be checked. They are not a sample of real signups, so they do not predict your rate. A disposable service that is not on the blocklist yet is missed until it is added.

Changelog

1.1.0

  • New REVIEW verdict for privacy alias services.
  • Customer allow and deny rules, with subdomain matching.
  • risk_score, reason, alias, matched_domain and suggestion on every verdict.
  • Batch endpoint: up to 100 addresses per call.
  • Test-mode keys, which do not count toward quota.
  • Mail-server checks, with null MX detection and a one-hour cache.

1.0.0

  • Single-address verify with syntax, disposable-domain and mail-server checks.
  • Per-key rate limit and monthly quotas.
Questions about the API? Email heysharad@proton.me. © 2026 ShieldMX