Cleaning a list after the fact catches problems that already happened. Verifying at signup prevents them from happening at all — a typo'd address never enters your funnel, a disposable signup never counts toward your trial metrics. Here's how to actually wire that up.
Why Real-Time Beats Post-Signup Cleanup
Bulk verification and real-time verification solve different problems. Bulk cleaning is for lists you already have — existing contacts, CRM imports, integration-sourced lists. Real-time verification is for the moment someone is actively signing up, when catching a bad address costs you nothing and catching it later means an onboarding sequence already went nowhere. Most teams end up using both: real-time checks at the point of signup, and periodic bulk re-verification for everything already on the list.
A Basic Request and Response
Zuhal's single-email endpoint accepts a POST request with the address to check and returns a result in real time:
curl -X POST https://zuhal.io/api/v1/verify \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
The response returns the verification result along with your remaining credit balance:
{
"status": "success",
"message": "Email verified successfully.",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"email_status": "valid",
"credits_used": 1,
"remaining_credits": 4999
}
email_status returns one of five values: valid, invalid, catch-all, disposable, or unknown. What you do with each depends on your signup flow — most teams block invalid and disposable outright, allow valid through immediately, and either allow with monitoring or flag for review on catch-all and unknown.
This works from any backend language as a plain HTTP call — no SDK required. A minimal Python example using the standard requests library:
import requests
response = requests.post(
"https://zuhal.io/api/v1/verify",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"email": "user@domain.com"},
)
data = response.json()
print(data["email_status"]) # "valid"
Handling Debounce on the Frontend
If you're checking an address as the user types (rather than only on submit), don't fire a request on every keystroke. Wait a short delay after the last keystroke before calling the API, and cancel any pending call if the user keeps typing — this avoids burning credits on every intermediate, incomplete address and reduces load on your own backend. A delay in the range of 400–800 milliseconds after the last keystroke is a reasonable starting point; tune it based on how your specific form behaves.
Handling Rate Limits and Errors
Requests that exceed your account's rate limit return an HTTP 429 status with a message indicating you're sending requests too quickly — back off and retry rather than hammering the endpoint. Build your integration to handle non-200 responses gracefully: if verification fails or times out, decide in advance whether your signup flow fails open (allows the signup, flags it for later review) or fails closed (blocks the signup until verification succeeds) — this is a product decision, not something the API dictates for you.
When to Also Run Bulk Verification
Real-time checks only cover addresses that come through your signup flow. Anything already on your list before you added this check — CRM imports, integration-sourced contacts, old exports — needs a bulk pass instead. See converting an existing list to CSV or TXT for bulk cleanup, or connect a platform directly to skip the export step entirely.
Address format alone isn't enough to catch what real-time verification catches — see why syntactically valid isn't the same as deliverable. Full endpoint reference, including bulk upload and error codes, is in the API documentation and docs.
Ready to clean your email list?
Verify up to 100 emails free — no credit card required. Bulk lists, real-time API, instant results.
Get 100 Free Credits →