Check an address at signup
Catch typos and addresses that do not exist before you save a new user.
When someone signs up, your app can ask Mailyte whether their email address has a real mailbox
before it saves them. A typo like ada@gmail.con is caught while the person is still on the page,
and an address that does not exist never reaches your database, so it can never bounce later.
Each answer uses one check from the account. Nothing is sent to the person: we ask their mail server whether the mailbox exists, the way a sending server would, and stop before any message goes out.
Two ways to do it
| You want | Call | Key needs |
|---|---|---|
| An answer, and nothing saved at Mailyte | POST /api/v1/verifications/check |
verification:write |
| The person saved as a contact, checked in the same call | POST /api/v1/contacts or PUT /api/v1/contacts with "check": "wait" |
contacts:write |
The first works on every account, whether or not Bounce protection is switched on. The second checks
only while Bounce protection is switched on for the account (the owner turns it on under
Sending › Bounce protection); with it off, the contact is saved as not_checked.
Call both from your server, never from the browser. The key is a secret, and a browser cannot read our response headers from another site anyway.
Check one address
curl -X POST "$MAILYTE_BASE/api/v1/verifications/check" \
-H "Authorization: Bearer $MAILYTE_API_KEY" \
-H "Idempotency-Key: signup-7f3a9c" \
-H 'Content-Type: application/json' \
-d '{ "email": "ada@gmail.con" }'{
"message": "Address checked",
"data": {
"object": "verification_result",
"email": "ada@gmail.con",
"result": "invalid",
"reason": "misspelt_domain",
"flags": [],
"suggestion": "ada@gmail.com",
"checks_used": 1
},
"success": true,
"code": 200
}We answer in the same call when we can. Most answers take a second or two; a mailbox we have not seen before can take up to about 8 seconds. Give your HTTP client a timeout of about 10 seconds.
The reply carries X-Mailyte-Checks-Remaining: the checks the account has left after this one.
Send an Idempotency-Key header, so a retried request is never charged twice.
What to do with each answer
result |
reason |
At signup |
|---|---|---|
valid |
mailbox_exists |
Save the person. |
invalid |
misspelt_domain |
Ask "Did you mean suggestion?" and let them pick. |
invalid |
mailbox_does_not_exist, domain_cannot_receive_mail, invalid_syntax |
Ask for another address. Mail to this one can never arrive. |
risky |
role_address, accepts_all_addresses, disposable_address, inbox_full |
Save the person. A throwaway address (disposable_address) is your call. |
unknown |
provider_does_not_confirm, could_not_confirm |
Save the person. This answer is free. |
pending |
checking |
Save the person. This answer is free. |
checks_used says what the answer cost: 1 when it used a check, 0 when it was free. Free answers
are "couldn't confirm" (unknown), "still checking" (pending), "not an email address"
(invalid_syntax), and a shared address such as info@ that the provider would not confirm
(risky, role_address, checks_used: 0). Read checks_used, not result, to know the cost.
suggestion is set only for misspelt_domain. Everywhere else it is null.
flags adds detail to risky: role (info@, sales@), disposable (a throwaway inbox), catch_all
(the domain accepts any address) and full (the mailbox has no space).
A full inbox is a real address. risky with inbox_full means the person exists but their
mailbox has no space right now, so mail to it bounces until they clear it. Let them sign up. If they
become a contact, Mailyte holds marketing to them for 7 days and then checks again, for free. Your
app's own emails, such as the welcome email, still go.
Never block a signup on us
A check is a guard, not a gate. Let the person sign up whenever we cannot answer:
- No checks left. The reply is a
402witherror_code: "PLAN_LIMIT_EXCEEDED"anddata.limit_type: "checks".data.quotesays how many checks are needed and which pack covers them. The account owner is emailed, and thechecks.ran_outwebhook fires (both once per cycle: a new month or a purchase starts the next one). - The checker is unavailable. The reply is a
503. Nothing was used. - Your timeout ran out. Save the person. Nothing was used unless an answer came back.
async function checkAtSignup(email) {
try {
const res = await fetch(`${process.env.MAILYTE_BASE}/api/v1/verifications/check`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAILYTE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) return { ok: true }; // 402, 503, anything else: let them in.
const { data } = await res.json();
if (data.result !== 'invalid') return { ok: true, risky: data.result === 'risky' };
return { ok: false, suggestion: data.suggestion };
} catch {
return { ok: true }; // Timeout or network error: let them in.
}
}Save the contact and check it in one call
If your app keeps its users as Mailyte contacts, add "check": "wait" to the write that creates them:
curl -i -X PUT "$MAILYTE_BASE/api/v1/contacts" \
-H "Authorization: Bearer $MAILYTE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"email": "ada@example.com",
"name": "Ada Lovelace",
"status_if_new": "subscribed",
"check": "wait"
}'We check the address before we answer, for up to about 8 seconds, so the contact's email_status in
the reply is already the answer: valid, risky or invalid. An invalid contact's state
becomes bounced, and the address is suppressed, so nothing is ever sent to it.
"check": "wait" acts only when the write creates the contact, changes its address, or opts in a
contact that was unconfirmed and never checked (its status becomes subscribed). It acts only
while Bounce protection is switched on. Any other value of check is ignored, so sending it never
makes a request fail. The write itself never fails because of a check.
The reply can still say pending. That happens when the checker is slow, when the checker could not
confirm it yet, when several waits are already running for your account, or when no checks are left.
The contact is then checked in the
background, and the
contact.checked webhook tells you the
answer when it lands. If no checks were left, it becomes not_checked instead, and
checks.ran_out fires once per cycle.
The contact reply has no reason and no suggestion. To offer "Did you mean…?", call
POST /verifications/check first.
The reply carries X-Mailyte-Checks-Remaining, and X-Mailyte-Checks-Warning when the checks are
running_low or have ran_out. See
Checking contacts added through the API.
Next
Checking contacts added through the API —
email_status, the checks headers, the webhooks, and what a full inbox means for your app.