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.
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
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.
| Signal | Where it shows up | What it means | What to do | What it is not |
|---|---|---|---|---|
| HTTP 401 | MCP response | Missing or revoked key | Replace the key. Stop the job | Not an invalid address |
| HTTP 403 | MCP response | Disallowed browser Origin | Call from a server or local client | Not an invalid address |
| HTTP 405 | GET or DELETE /mcp | Clients POST JSON-RPC | POST. Keep the key out of the query | Not an invalid address |
insufficient_credits | start_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 code | Read account_status. Leave unchecked rows unmarked | Not a verdict |
list_limit | create_list | Active-list cap reached | Stop or finish a list, delete only when idle, then create | Not a bad file |
list_busy | delete_list during import or verify | Delete refused | Stop first. Consumed credits stay. Unused holds release | Not permission to clear rows |
list_already_started | start_verification | This list is already running | Keep the list id. add_emails joins the same run | Not a reason for a second list |
list_importing | start_verification | Import still running | Start after it clears. No wait interval is published | Not a failed check |
| Unknown versus invalid | Result codes and email events | Invalid is broken syntax and, on the webhook, sits with undeliverable and dead. The free family is not invalid | Suppress only named codes. Review or recheck unknown | Not one bad-email bucket |
| Holds released on stop | stop_verification | Unfinished jobs cancel. Unused holds release. Finished chargeable results stay billed | Stop before remove, clear, or delete | Not 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
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.
- MailCheckup product truth, including public copy for MCP, webhooks, pricing, and the check order.
- RFC 5321, SMTP reply classes: https://www.rfc-editor.org/rfc/rfc5321
- RFC 5322, address syntax: https://www.rfc-editor.org/rfc/rfc5322
- RFC 1035, MX records: https://www.rfc-editor.org/rfc/rfc1035
- RFC 7505, null MX: https://www.rfc-editor.org/rfc/rfc7505
- Google email sender guidelines, cited only as sender rules, not as a verification result: https://support.google.com/mail/answer/81126








