A key kept off the papers is the large shape, with a card coming back along an arc while the stack stays put apart from it.

Email verification API workflow for list jobs

THE MAILCHECKUP JOURNAL

If you are a developer building list verification into an application, treat the work as a job with its own failure states. That is the email verification API workflow. Check that the workspace can pay, create a list without starting verification, add addresses 500 at a time, start the run once, then poll or accept webhooks. Unknown is not invalid. Stopping a run releases unused credit holds and leaves consumed credits consumed. The published executable interfaces are the MCP server at https://cp.mailcheckup.net/mcp and webhooks. Pages under /docs are not a REST contract. This guide does not invent a base URL or a JSON schema.

MAILCHECKUP / BLOG / EMAIL VERIFICATION API WORKFLOW↘

Who should follow this run order

You already hold the addresses. An import worker, an agent, or a backend job has to run the check. This is not a signup-form layout and not a marketer's cleanup list.

Create the workspace key in portal Settings. It is shown once, starts with mck_, and sees only that workspace. Keep it in server-side or local client config. Do not put it in chat, a browser bundle, a ticket, or a screenshot.

On this production host, verify_email and verify_emails are refused when live stations are required. The tool says to use a list, and it does not name a separate error code for that refusal. verify_emails caps at 25 anyway. The marketing sample checker never contacts a mail server, so it cannot prove your job works.

What a finished check is allowed to mean

Verification is a time-bound technical assessment. It is not identity, consent, inbox placement, or permission to send. Deliverable means the mailbox server accepted the recipient during the check. It does not mean a later message will reach the inbox, that a person will read it, or that you should contact them.

The public check order is fixed. Syntax is first. Broken syntax is invalid. A typo suggestion stays separate from the submitted address, and that address is not rewritten. Disposable domains and role addresses, such as info@ and support@, are classifications, not bad syntax. Whether you mail a role address is your product decision.

Domain mail capability is next. No mail server, or one that cannot be reached, is inconclusive, not a confident invalid person. Recipient acceptance is last. Accept means deliverable. Reject means undeliverable. Accept-everything means catch-all. A full mailbox is mailbox full. Greylist, a block, a timeout, or no clear answer means unknown. Unknown is not invalid.

Those names are product classifications. Published MCP tools and webhook email events do not document a raw SMTP reply field. Do not invent one, and do not store a guessed 550 next to the row.

RFC 5321 classes SMTP replies as positive (2yz), transient negative (4yz), and permanent negative (5yz). A positive reply ends that SMTP step. It does not promise later filtering, and a permanent failure can be a policy rejection rather than "no such person." RFC 5322 defines addr-spec syntax, which is not proof a mailbox exists. RFC 1035 defines MX. RFC 7505 says a null MX, preference 0 and exchange ".", does not accept mail. No MailCheckup code is named null MX. mx_unreachable is free, not invalid. Store the code you receive. Google's email sender guidelines cover authentication and sender spam-rate expectations. They are not a verification verdict, and a deliverable code does not satisfy them.

A finished chargeable result costs one credit: valid, deliverable, undeliverable, dead, invalid, disposable, catch_all, role_account, or mailbox_full. Risky groups disposable, catch-all, role, and mailbox full. It is not another charge and not an address code. Free, and not invalid: unknown, retry_later, greylisted, mx_unreachable, provider_blocked, and system_error. A system error did not finish, so the hold is released. Not checked has no finished result and stays free until a chargeable one exists. Duplicates in one file are stored once and billed once. A recheck of one group is a new finished check and can cost another credit. Saved results on the rest of the list stay. Unknown or mailbox full can be rechecked without redoing rows that already finished.

Marketing docs are not an executable contract

The /docs pages explain integration principles. They are not a finished REST contract. Idempotency key format, a REST base URL, and a REST request schema are not finalized there. This guide invents none of those. A sample that is not the published MCP client configuration is labeled conceptual. It is not a client you can point at a host.

That boundary is what keeps a 401 or list_importing out of the address column. A parser aimed at a made-up path stores the failure as invalid. The address never got a verdict.

Two interfaces are published as executable. MCP is streamable HTTP and JSON-RPC 2.0 at https://cp.mailcheckup.net/mcp. The server name is mailcheckup. Named protocol versions are 2025-06-18, 2025-03-26, and 2024-11-05. Webhooks are the other interface. MailCheckUP POSTs JSON when a list starts, moves, or finishes. An owner or admin adds an HTTPS URL in the app. An API key cannot create a webhook. Members cannot either.

The MailCheckup MCP server page is the tool reference for arguments, limits, and codes. This run order sits beside that page. It does not replace it and it does not quietly add tools.

Drive the list with MCP

Clients POST JSON-RPC. GET /mcp and DELETE /mcp return 405. A missing or revoked key returns 401. A disallowed browser Origin returns 403. Send the key as Authorization: Bearer mck_... or x-api-key: mck_.... Samples use mck_your_key. That string is not a live key.

notifications/initialized returns HTTP 202 and no body. The server is stateless. Authenticate every call. Do not treat the empty 202 as a list id or a verdict.

Tools return JSON text. Errors look like {"error":"<code>"}. list_id and export_id are UUIDs. There is no full field schema on the public page, and there is no find_email tool. The server will not guess addresses, score leads, or promise inbox placement. Verify addresses you already hold.

Published configuration shape for Cursor, Claude, and other remote MCP clients:

{
  "mcpServers": {
    "mailcheckup": {
      "url": "https://cp.mailcheckup.net/mcp",
      "headers": {
        "Authorization": "Bearer mck_your_key"
      }
    }
  }
}

That block is configuration, not a REST request. Other clients use the same URL and the same bearer key inside their own wrapper. Do not drop the live key into a chat so a model can "try it."

Call account_status before a large run. It reports credits available, reserved, and consumed, the plan code, max list rows, max active lists, and whether a run can start. Verification does not start when available credits are 0. Trust that report over a number copied out of a table months ago.

list_webhooks is read-only. It returns the URL, enabled flag, events, retry count, and gap. It does not return the signing secret. Creating an endpoint is an owner or admin action on the app webhook page. If the job needs push and no endpoint exists, stop and ask a person. Calling the read tool again will not create one.

Hear the list with webhooks

Poll when the job can read list_results and write your database. Take a webhook when another system should react as rows finish. The webhook contract holds the signature checks and the event reference. What follows is only how those events sit on a list run.

Plain HTTP is rejected. With no events selected, the subscription is list.completed only. One whsec_ secret signs every endpoint. Samples may use only whsec_your_secret. Each URL has its own events, try count, and gap. Disabling a URL does not rotate the secret.

POST application/json as MailCheckUP-Webhook. Headers are x-mailcheckup-event, x-mailcheckup-delivery (new every retry), and x-mailcheckup-signature: sha256=<hex>. Sign with HMAC-SHA256 over the raw body and compare in constant time. Re-serializing parsed JSON breaks the signature. Timeout is 8 seconds. Any 200-299 succeeds. Redirects fail and are not followed. Envelope: {"id":"evt_…","type":"list.completed","created_at":"…","data":{}}. The id is stable. Store it before 2xx, then do slow work. Tries are 1 to 10, default 6. Gap is 10 to 86400 seconds, default 60, and it stays fixed.

Not called and not retried: localhost, .local, .internal, private or link-local, non-http(s), a URL with a username or password, and DNS that points at a private address. A test has "test": true, is not retried, and must not enter a live audience.

list.started fires once. jobs is the queue at that moment, not the file size. Later addresses can still join. list.progress fires at most once a minute, and only after the checked count moves, with totals, in_progress, and a status such as verifying. list.completed fires once for completed or completed_with_warnings. Nothing is replayed for a list that finished before the endpoint existed. Counts cover rows, unique, checked, credits, not_checked, and each result bucket through unknown. Unknown spent no credit. No warning enum is published. Do not invent one.

Email posts carry up to 50 addresses, with the next batch about 20 seconds later. An address already delivered to that endpoint is not sent again. Items have email, code, list_id, and list_name. email.valid is valid or deliverable. email.undeliverable is undeliverable, invalid, or dead. Disposable, catch-all, role, and mailbox full are separate events. Each of those costs one credit. email.valid is not inbox placement. email.unknown is the free family and is not a delete event.

Run the list in this order

Skip a step and the failure table starts matching you for ordinary reasons: a start during import, a second start, a delete while the run is busy.

Eight-step list run from account_status through optional export_status. create_list does not start verification. The export link is emailed.
Published list order. Poll get_list outside the tool. The export link is emailed, not returned by export_list.

The diagram is that sequence. The paragraphs are the refusals.

Ask whether a run can start

Call account_status. If available credits are 0, stop. start_verification returns insufficient_credits and does not start. Creating the list anyway spends an active-list slot.

Pricing copy checked on 6 October 2026: Free is 100 credits, 2 lists, 5,000 rows; Starter 5,000, 10, and 50,000; Growth 25,000, 25, and 250,000; Business 100,000, 50, and 500,000; Scale 500,000, 100, and 1,000,000. Monthly credits expire with the paid period and do not roll over. New workspaces start on Free, and those 100 credits expire about 30 days after signup. Credit packs do not expire. Published rates run from 30 a minute on Free to 300 a minute on Scale. No MCP error code for that rate exists in the public tool list. Do not invent one. account_status is the number for the key you hold.

Create the list before you start it

create_list needs a name. Description is optional. You may pass at most 500 emails on the create call. Creation does not start verification. At the active-list cap, the error is list_limit.

list_lists returns recent lists, limit 1 to 50, default 20: id, name, status, row counts, and credits used. Check it before creating a duplicate. rename_list updates the name (max 120 characters), the description (max 500), or both. A rename does not start or stop a run.

Add addresses in chunks of 500

add_emails takes the list id and 1 to 500 emails. Call it again for the next chunk. Duplicates already stored are skipped. New rows join the current run if verification is already going. You may add addresses while a list is verifying. Do not open a second list for that reason, and do not abandon the job because the first list is still moving.

Illustrative batching, not a timing result: 1,200 addresses at a cap of 500 means three calls, sized 500, 500, and 200. The public pages do not say how many minutes those checks take. Leave duration out until you measure the workspace you are using.

remove_emails (1 to 500) and clear_list_emails refuse during import or verification, release unfinished holds, and do not refund consumed credits. list_rows pages stored addresses, including unchecked ones, 100 at a time. Optional q is an email prefix.

Start once

start_verification holds credits in chunks and starts the SMTP run. The three named errors are in the failure table: insufficient_credits, list_already_started, and list_importing. Queueing also stops when the workspace cannot pay, with no extra published code. Read get_list before you treat unfinished rows as judged.

Poll outside the tool

get_list returns the list and, once checks exist, the report. Do not sleep inside a tool, and do not fill missing rows with a guess. No poll interval is published. Use a slow one, back off when the counts stall, and stop on a finished status you stored. Webhooks are how you drop the loop.

Page deliverable, and keep the other segments

list_results takes a segment (default all), an offset, and a limit of at most 100. It returns has_more. For the accepted set, use segment deliverable, limit 100, and raise the offset until has_more is false. Those addresses were accepted during the check. They were not placed in an inbox.

Segments for paging and export: all, deliverable, undeliverable, catch_all, disposable, role_account, mailbox_full, risky, unknown, and not_checked. Put unknown and not_checked on a review queue. Do not suppress them in the same write as undeliverable.

pull-valid-results pages deliverable until has_more is false. verify-a-list creates, adds by 500, starts, polls, and summarizes get_list. keep-valid-only runs only after a stop. It removes undeliverable, disposable, role, mailbox-full, and catch-all, and it leaves unknown and not_checked. Do not run it on a live list, and do not use it to delete unknown.

Export only when a person needs a file

export_list takes a segment and an optional format: csv, txt, or xls. The download link is emailed. It is not in the tool result. export_status returns status only, with no file and no token. If you cannot read that mailbox, page list_results instead. Resources beside the tools are mailcheckup://guide, mailcheckup://result-codes, mailcheckup://account, and mailcheckup://lists.

Route failures before you touch the address

Three failure lanes: HTTP transport errors, named tool error codes, and result codes where unknown stays distinct from invalid. Stop releases unused holds.
HTTP statuses and tool error codes are not address verdicts. Unknown stays off the invalid path. Stop releases unused holds.

Each row is documented list or MCP behavior. If a response matches nothing here, leave the address unmarked and log the HTTP status or the error string you actually got.

SignalWhere it shows upWhat it meansWhat to doWhat it is not
HTTP 401MCP responseMissing or revoked keyReplace the key. Stop the jobNot an invalid address
HTTP 403MCP responseDisallowed browser OriginCall from a server or local clientNot an invalid address
HTTP 405GET or DELETE /mcpClients POST JSON-RPCPOST. Keep the key out of the queryNot an invalid address
insufficient_creditsstart_verification returns {"error":"insufficient_credits"}Credits available are 0, so the run does not start. Mid-run queueing stops when the workspace cannot pay, with no extra published codeRead account_status. Leave unchecked rows unmarkedNot a verdict
list_limitcreate_listActive-list cap reachedStop or finish a list, delete only when idle, then createNot a bad file
list_busydelete_list during import or verifyDelete refusedStop first. Consumed credits stay. Unused holds releaseNot permission to clear rows
list_already_startedstart_verificationThis list is already runningKeep the list id. add_emails joins the same runNot a reason for a second list
list_importingstart_verificationImport still runningStart after it clears. No wait interval is publishedNot a failed check
Unknown versus invalidResult codes and email eventsInvalid is broken syntax and, on the webhook, sits with undeliverable and dead. The free family is not invalidSuppress only named codes. Review or recheck unknownNot one bad-email bucket
Holds released on stopstop_verificationUnfinished jobs cancel. Unused holds release. Finished chargeable results stay billedStop before remove, clear, or deleteNot a refund of finished checks

remove_emails and clear_list_emails use the same busy window as delete: they refuse while importing or verifying, they release unfinished holds, and they do not refund consumed credits. The tool table prints list_busy on delete_list. It does not print a second code for remove and clear. Branch on the string you received. Do not rewrite it into list_busy if that is not what came back.

Caller failures and job-control codes

A 401 means the key is missing or revoked. Replace it, and treat a log that still contains it as part of the incident. A 403 means a browser Origin was disallowed, so the page must not hold the key. A 405 means the verb was GET or DELETE. POST JSON-RPC. No initialize body is specified here. None of those statuses describes the address.

You pay when a chargeable answer finishes. Unknown, greylist, a blocked check, an unreachable server, and a system error cost nothing, and the hold is released. Do not resubmit a file that failed with insufficient_credits. list_limit is the active-list cap: stop or finish before delete. list_already_started means keep the id. Because no idempotency header is finalized, store that id in your database when create returns. list_importing means wait on an interval you chose. No backoff number is published.

Unknown stays unknown

Invalid, in the check order, is broken syntax. On email.undeliverable it travels with undeliverable and dead, one credit each. Role, disposable, catch-all, and mailbox full are chargeable classifications, not invalid. Catch-all did not identify this mailbox. The risky segment is that group. Do not relabel it.

email.unknown is the free family named above, and it is not a delete event. A system error did not finish. Recheck unknown or mailbox full without redoing rows that already have a result. A new chargeable code can cost another credit. Another unknown stays free. keep-valid-only leaves unknown and not_checked, and only after a stop. Not checked stays free until a chargeable result exists.

Stop releases unused holds

A credit hold ends as a consumed chargeable result, a released free result, a released unfinished check, or a stop that drops unused holds.
Finished chargeable results stay consumed. Free and unfinished checks release the hold. Stop releases unused holds.

stop_verification cancels unfinished jobs and releases unused holds. Finished chargeable results stay billed. Consumed credits are not refunded. Remove, clear, and delete release unfinished holds only once the list is idle. Delete first and the error is list_busy.

Unfinished rows after a stop are not invalid. not_checked means no finished result yet. If credits run out mid-run, queueing stops. That is not a verdict, and no refund call is published on top of the hold release.

Keep one list id

New addresses join the running list. Two list ids mean two hold pools and two webhook streams, and your job table will not know which one it meant.

Store one business key and fill the list id when create_list returns. Crash after create: resume that id. Crash during add_emails: send the chunk again. Stored duplicates are skipped, so a repeated chunk is safer than a new list. Crash after start: list_already_started means you are already in the run. Poll, or wait for list.completed.

Do not collapse failure into a boolean. Store the HTTP status, the error code, or the result code. A boolean puts 401 and invalid on the same branch.

Conceptual state in your application

This block is conceptual. It is not a MailCheckup client, not a JSON schema, and not a request.

Conceptual application states. Not an API schema.

job_created
  -> account_blocked      when account_status says a run cannot start
  -> list_created         after create_list returns a list id
  -> list_filled          after the last add_emails chunk is accepted
  -> run_started          after start_verification is accepted
  -> run_already_going    when the error is list_already_started
  -> waiting_on_import    when the error is list_importing
  -> blocked_credits      when the error is insufficient_credits
  -> blocked_list_cap     when the error is list_limit
  -> blocked_auth         when the HTTP status is 401 or 403
  -> stopping             after stop_verification
  -> results_paged        when has_more is false for the segments you store

Anything off that list should log and halt. There is no edge from an auth or credit block to a result code, and no edge from waiting_on_import to delete.

On the webhook side, store evt_… before you return 2xx. The same id is a repeat, not a second write. x-mailcheckup-delivery changes every try, so it is a weak key. A handler past 8 seconds creates its own retry. Upsert on list id plus email. A recheck is a new check, not a replay.

One writer for each row

If a poller and a webhook both write a row, pick one rule. Webhooks can apply increments. list_results reconciles when list.completed arrives, or on a timer if you never subscribed to progress. Page until has_more is false. Do not delete a row because one page omitted it.

jobs on list.started is not the final size. A subscription with no events selected receives only list.completed. Non-zero unknown or not_checked means the file is neither all accepted nor all rejected.

Three failed deliveries in a row email owners and admins once, then stay quiet for 24 hours. The endpoint remains. Fix the URL, send the test, and reconcile with list_results. The log keeps 30 days. Clearing it does not resend. Checks that finished before the endpoint existed are not backfilled.

What belongs in the log

Keep the list id, any export id, the event id, the result code, and the time you stored it. For a failure, keep the HTTP status or the error string, plus your attempt count. Leave the API key, the webhook secret, and the full file out of those logs. If a hold looks stuck, open a ticket in the app or email support@mailcheckup.net with the list id, not the key. A score on your own dashboard is your formula. The tools do not return one.

Where the short pages stop

The email verification API page says to keep transport errors out of the verdict, keep keys on a server, and treat a long list as a job. It does not name list_importing, list_busy, or the hold released on stop.

The separate errors from verdicts page says to test failures and not to store a timeout as proof of an invalid address. It lives under /docs, so it is a principle page, not the executable contract. It does not list the status codes or tool strings in the table above.

The developer workflows page is the design stance: separate failure types, retry only documented behavior, and test unknown on purpose. It does not walk account_status through export_status.

This page is that walk, plus the branch table. Argument limits stay on the MCP page so they have one place to change.

Key takeaways

List verification in an application is the MCP server, plus webhooks when you want push. Marketing /docs pages are not a REST contract. No base URL and no request schema are invented here.

Run account_status, then create_list without starting, then add_emails by 500, then start_verification, then poll get_list outside the tool, then page list_results. Export only if someone needs a file. The link arrives by email.

HTTP 401, 403, and 405 are caller problems. The five tool codes are job control. Unknown is not invalid, and email.unknown is not a delete. Stop releases unused holds. Consumed credits stay consumed. Remove, clear, and delete wait until the list is idle.

Questions that change the design

Is there a REST endpoint to call instead?

No published one. The /docs pages do not finalize a base URL or a request schema. Drive the list at https://cp.mailcheckup.net/mcp, or receive webhooks on an HTTPS URL an owner or admin added.

Should a signup form wait on this job?

No. The job pages, can keep queueing after list.started, and can sit on list_importing. A waiting form expires for reasons that are not about the person. Instant verify is refused on this host when live stations are required, and verify_emails caps at 25. Mailbox ownership is your own confirmation. Verification is not consent.

Can unknown rows be deleted as cleanup?

You can build a deleter. Unknown is free and inconclusive, not a dirty row. keep-valid-only leaves it and leaves not_checked, and only after a stop. A human suppression needs its own flag and date. Do not overwrite the result code.

What does stop do to credits?

Unused holds drop. Finished chargeable checks stay charged. The free family does not. Do not run stop twice expecting a credit to come back.

Create a key and run one list

Create the workspace key in the MailCheckup app, put it in the MCP client configuration, and run this order on a list you are allowed to check. Read account_status first. If the run cannot start, log the tool error and leave every address unmarked.

Create a workspace key in the MailCheckup app

Sources

Product copy was read in this repository on 6 October 2026. RFC and Google pages were not re-fetched during drafting. Re-open them before publication.

RELATED ARTICLES

More from the journal.

A short stack of blank cards is the large shape, with a shallow empty tray apart from it.Bulk Lists & Files
19 min read

Bulk email list verification before a send or CRM import

A file of addresses is not a send list and not a CRM update until each stored address has a branch: syntax, disposable, role, no usable mail server, catch-all, deliverable, or unknown.

Read article
A short stack of blank cards is the large shape, with an open door with a blank interior apart from it.MCP & AI Agents
19 min read

Email verification MCP: run a list without blocking the agent

An operator's sequence for the MailCheckup MCP server: read credits, load one list in chunks of 500, poll without blocking, leave unknown in place, and wait for the export email.

Read article
An open door with a blank interior is the large shape, with a short stack of blank cards apart from it.Deliverability & Hygiene
19 min read

Email verification fundamentals before a campaign

An email marketer with permission to mail still needs a way to separate sendable rows from fixes, reviews, and holds. This is the operating checklist and the before-and-after process for that decision.

Read article
A building with one mail slot is the large shape, with a short stack of blank cards apart from it.Protocols & Syntax
19 min read

What an SMTP accept response proves about an email address

SMTP verification answers whether a mailbox server accepted a recipient during a check. This guide shows how to read that reply, and how to map accept, reject, defer, and timeout onto categories you can operate.

Read article
A building with no mail slot is the large shape, with a building with one mail slot apart from it.Protocols & Syntax
19 min read

What an MX lookup can prove when a website already loads

A loading website proves that a web server answered. An MX lookup proves only the mail-exchange data DNS returned. This guide separates those facts from the assumptions people attach to them, including implicit MX, null MX, and an unreachable server.

Read article
A chute with one exit is the large shape, with a thin slip pulled out on its own apart from it.Catch-All & Disposable
19 min read

Catch-all email results: what to keep, review, or suppress

When a list comes back catch-all, deleting the segment and marking it valid are both the wrong default. Acceptance proves domain policy. Your records decide keep, review, or suppress.

Read article
A thin slip pulled out on its own is the large shape, with a shallow empty tray apart from it.Catch-All & Disposable
18 min read

Disposable email detection policy: block, review, or allow by workflow

Disposable is a classification, not proof of fraud. Block it as the only recovery path, step up a trial, send the receipt, keep it off the newsletter, and allow a low-risk download. The cost sketch counts waste and legitimate users. Every volume in it is illustrative.

Read article
An open door with a blank interior is the large shape, with a balance with one empty pan apart from it.Billing & Credits
15 min read

How published plan rates and credits fit a list run

Match a list run to published credits, list caps, row caps, and the rate column. Unknown is free. A run does not start when available credits are zero.

Read article