API v1

API documentation

One request per address, or batches up to a million. Same engine and same credits as the dashboard. Keys come from your account.

Authentication

Create a key in Dashboard → API keys and send it in the X-API-Key header (or as Authorization: Bearer mk_live_…). Keys start with mk_live_, are shown once, and can be revoked at any time. Base URL: https://api.mailok.io.

Verify one address

POST /v1/verify with { "email" }, or GET /v1/verify?email=. Answers in a few seconds; a real SMTP conversation can take up to 60 s on slow servers.

curl https://api.mailok.io/v1/verify \
  -H "X-API-Key: mk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'
Response
{
  "email": "[email protected]",
  "status": "valid",
  "catch_all": false,
  "disposable": false,
  "role": false,
  "mailbox_full": false,
  "mx_found": true,
  "mx_record": "mx1.northwindtraders.com",
  "domain": "northwindtraders.com",
  "account": "sarah",
  "credits_remaining": 9999
}

Statuses

ValidThe server confirmed the mailbox. Safe to send.1 credit
InvalidBad syntax, no MX record, or the server rejected the mailbox.1 credit
RiskyAccepted but not confirmable: catch-all domain, full mailbox or disposable provider. Look at the flags.1 credit
UnknownThe server did not answer in time (greylisting, timeouts).0 credits

Flags on every result: catch_all, disposable, role, mailbox_full, mx_found, mx_record. credits_remaining is your balance after the call.

Batches

Send a list, poll until ready, page through the results. Duplicates inside a list are marked and free. Lists also appear in your dashboard.

POST /v1/batches
{ "emails": ["[email protected]", "[email protected]"], "name": "October import" }

→ { "uid": "k3d9x1qzpa", "name": "October import", "status": "pending", "total": 2, "completed": 0,
    "stats": { "valid": 0, "invalid": 0, "risky": 0, "unknown": 0, "duplicate": 0 }, "created_at": "…" }
GET /v1/batches/k3d9x1qzpa        → same shape, "status": "ready" when done
GET /v1/batches/k3d9x1qzpa/results?page=1&page_size=1000&status=valid
→ { "items": [ { "email": "[email protected]", "status": "valid", "checked_at": "…" } ], "total": 1, "page": 1, "page_size": 1000 }

Statuses: pending → processing → completed → ready; paused when the account runs out of credits (resume from the dashboard after topping up); failed with an error message. GET /v1/batches lists your batches.

Account

GET /v1/account returns credits, subscription_credits, credits_remaining, plan and your rate_limit_per_minute. Handy for alerts before a campaign.

Webhooks

Pass a callback_url (public https or http host) when creating a batch and we POST a batch.ready event when it is done. Answer with any 2xx within 10 s; otherwise we retry after 1, 5 and 30 minutes, then give up. Every attempt is listed in Dashboard → API keys, where the signing secret lives too.

POST <your callback_url>
X-MailOK-Event: batch.ready
X-MailOK-Timestamp: 1760000000
X-MailOK-Signature: sha256=…
X-MailOK-Delivery: 42

{ "event": "batch.ready", "created_at": "…",
  "data": { "uid": "k3d9x1qzpa", "name": "October import", "status": "ready", "total": 2, "completed": 2,
            "stats": { "valid": 1, "invalid": 1, "risky": 0, "unknown": 0, "duplicate": 0 },
            "created_at": "…", "completed_at": "…", "error": null } }

Verify the signature before trusting the payload: HMAC-SHA256 of "{timestamp}.{raw body}" with your secret, compared in constant time. Reject timestamps older than a few minutes to block replays.

import crypto from 'node:crypto'

// signature = "sha256=" + HMAC_SHA256(secret, timestamp + "." + rawBody)
export function verify(rawBody, headers, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(headers['x-mailok-timestamp'] + '.' + rawBody).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headers['x-mailok-signature'] ?? ''))
}

Errors and limits

Every error has the same shape and a stable code:

{ "error": { "code": "insufficient_credits", "message": "No credits left. Buy credits or choose a plan in the dashboard." } }
400invalid_email, empty_list, list_too_large, invalid_callback_url, invalid_pageFix the request.
401unauthorizedMissing, revoked or unknown key.
402insufficient_creditsNo credits left. Nothing was verified.
404not_foundThe batch does not exist or belongs to another account.
429rate_limitedOver your per-minute limit; wait for Retry-After.
503verifier_unavailableVerification engine unreachable; retry in a minute. Nothing was charged.

Rate limit: 60 requests per minute per key by default (X-RateLimit-Limit header on every answer); monthly plans can carry a higher limit. Need more? Write to us.