emailverifier.dev
Posts

Disposable Email Detection API: Block Temporary Addresses at Signup

| 4 min read | Usama Ejaz
Simple envelope and block symbol for a disposable email detection API tutorial.

A disposable email detection API belongs at the server boundary where your application decides whether to create an account. The browser can check whether text looks like an email address, but it should not contain the project key or make the final signup decision.

This tutorial builds a dependency-free Node.js signup endpoint. It makes exactly one verification request, blocks confirmed failures and disposable providers, preserves uncertain results for review, and leaves a clear account-creation boundary.

Make the disposable-email request

Create an emailverifier.dev account and project, then copy the project’s API key. The request goes to POST /api/v1/verify:

curl https://emailverifier.dev/api/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_PROJECT_KEY" \
  -d '{"email":"signup@mailinator.com"}'

A known temporary provider produces a result shaped like this:

{
  "email": "signup@mailinator.com",
  "status": "risky",
  "action": "block",
  "flagged": true,
  "signals": ["disposable_address"],
  "suggestion": null
}

Branch on action. Keep status and signals for the response or audit log so the reason is not lost.

Create the runnable Node.js endpoint

Create an empty directory and add package.json:

{
  "name": "disposable-signup-gate",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22" },
  "scripts": { "start": "node --env-file=.env server.mjs" }
}

Add .env and keep it out of version control:

EMAILVERIFIER_API_KEY=ev_your_project_key
PORT=3000

Create server.mjs:

import { createServer } from 'node:http'

const apiKey = process.env.EMAILVERIFIER_API_KEY
const port = Number(process.env.PORT || 3000)

if (!apiKey) throw new Error('EMAILVERIFIER_API_KEY is required')

function sendJson(response, status, body) {
  response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
  response.end(JSON.stringify(body))
}

async function readJson(request) {
  const chunks = []
  let size = 0

  for await (const chunk of request) {
    size += chunk.length
    if (size > 10_000) throw new Error('Request body is too large')
    chunks.push(chunk)
  }

  return JSON.parse(Buffer.concat(chunks).toString('utf8'))
}

async function verifyEmail(email) {
  const response = await fetch('https://emailverifier.dev/api/v1/verify', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': apiKey
    },
    body: JSON.stringify({ email }),
    signal: AbortSignal.timeout(5000)
  })

  if (!response.ok) {
    throw new Error(`Verification failed with HTTP ${response.status}`)
  }

  return response.json()
}

const server = createServer(async (request, response) => {
  if (request.method !== 'POST' || request.url !== '/signup') {
    sendJson(response, 404, { error: 'Not found' })
    return
  }

  try {
    const body = await readJson(request)
    const email = typeof body.email === 'string' ? body.email.trim() : ''

    if (!email || email.length > 320) {
      sendJson(response, 400, { error: 'A valid email field is required' })
      return
    }

    const verification = await verifyEmail(email)

    if (verification.action === 'block') {
      sendJson(response, 422, {
        error: verification.signals.includes('disposable_address')
          ? 'Use a long-term email address'
          : 'Check the email address and try again',
        verification
      })
      return
    }

    if (verification.action === 'review') {
      sendJson(response, 202, {
        created: false,
        next: 'confirm_email_or_review',
        verification
      })
      return
    }

    // Create the account here after every required application check passes.
    sendJson(response, 201, { created: true, verification })
  } catch (error) {
    console.error(error)
    sendJson(response, 503, {
      error: 'Signup checks are temporarily unavailable. Try again.'
    })
  }
})

server.listen(port, () => {
  console.log(`Signup endpoint: http://localhost:${port}/signup`)
})

Start it:

npm start

Send a signup:

curl http://localhost:3000/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"person@example.com"}'

The endpoint returns 201 only for action: allow. A block becomes 422, an inconclusive or reviewable address becomes 202, and a verification outage becomes 503. The example does not create a user row, so the marked success branch is the only place that needs your database call.

Handle every verification status

StatusWhat it meansDefault signup action
deliverableThe available checks passed and mailbox evidence was positive.Allow unless a separate signal requires review.
riskyA material risk signal such as a disposable provider or likely typo was found.Follow action; disposable addresses normally return block.
undeliverableA confirmed syntax, routing, or mailbox failure was found.Block and ask for a correction.
unknownThe address was not confirmed bad, but mailbox evidence was inconclusive.Review or continue with email confirmation.

A timeout or policy block must not be converted into “invalid.” Returning 503 for the verification service failure also keeps infrastructure errors separate from address decisions.

Test the domain before spending a credit

The free disposable email checker is useful when you need to inspect one provider manually. It checks the domain classification without claiming that a specific mailbox exists.

For production signup traffic, keep the project key on the server and use the API result. The disposable-address signup policy covers when a temporary address should block immediately and when a lower-risk product can allow it with limits.

Continue reading