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]"}'{
"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
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." } }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.
