Email validation webhook: getting verification results without polling the API
A validation webhook is an HTTP request the verification service sends to your server on its own once a job is done. Instead of asking the API “finished yet?” every few seconds, you leave an address and the service tells you.
It is the usual pattern for slow operations. Checking a list takes minutes, longer for a big one, and there is no point holding a connection open the whole time. The service accepts the job, hands back an ID and gets to work; the result comes later as a separate request going the other way.
Webhook or polling
Polling is as simple as it gets. Your code hits a status endpoint every few seconds; the uChecker API docs suggest every 5 to 10 seconds. A job that runs for twenty minutes, polled every ten seconds, means roughly 120 requests, and only the last one tells you anything.
A webhook is one request, and it arrives the moment the job finishes rather than on the next poll. That does not make polling wrong. It is easier to debug, needs no public URL, works from behind NAT or from a laptop, and a script that cleans a list once a month will never need more. Webhooks pay off when you run many jobs or when an automated chain waits on the result.
How it works in uChecker
You pass the address in the validation request itself, in the webhook_url field. Both endpoints accept it: POST /api/v1/validate/bulk for a list and POST /api/v1/validate/single for one address. There is no account-wide setting, so every job can point somewhere different.
Single-address checks are asynchronous too. The response returns a task_id straight away, and the check itself takes anywhere from a few seconds to two minutes depending on the domain. So a webhook is useful for a signup form as well, not just for big lists.
When the job completes, uChecker sends a POST with Content-Type: application/json and five fields in the body.
task_id: the job ID.status: completed, failed or unknown.summary: counts for total, good, bad and unknown.finished_at: completion time in ISO 8601.error_text: the reason if the job failed, otherwise null.
The addresses themselves are not in the body, even for a one-address job. The webhook only says the results are ready; you fetch them with GET /api/v1/tasks/{task_id}/results and your API key. One quirk worth knowing: per the docs, the webhook summary counts addresses flagged as risky under bad, unlike the task analytics. If you want those under unknown, read GET /api/v1/tasks/{task_id}/analytics.
Retries and what your endpoint should return
A delivery counts when your server answers with a 2xx code within 30 seconds; the docs ask for a 200. Any other code, a dropped connection or a timeout triggers a retry after 30 seconds, then after one, five and fifteen minutes. That is five attempts in total. If your server stays down for more than twenty-odd minutes, the notification is lost, but the job and its results are still there to fetch by polling.
Two rules follow for the handler. Return 200 only after the data is stored. If the database write fails, a 500 and a retry beat acknowledging the request and losing the result.
And the handler has to cope with the same request twice. Sometimes you process everything and the response never makes it back (say, processing took longer than 30 seconds), so the service treats the attempt as failed and sends it again. The job ID is in the body and in the X-Webhook-Task-Id header; check it before doing anything. The attempt number is in X-Webhook-Attempt.
Heavy lifting such as downloading results or updating the CRM is better done after you respond: accept the request, push the job ID onto your own queue, return 200.
Securing the endpoint
A webhook URL is reachable from the internet, so anyone can POST to it. How much that matters depends on how far your handler trusts the request body. The usual defences:
- HMAC signature. The sender signs the body with a shared secret and puts the signature in a header; you compute it yourself and compare. GitHub does this with X-Hub-Signature-256, and its docs insist on a constant-time comparison instead of a plain ==.
- A secret in the URL. A random string in the path that only you and the service know. Weaker than a signature, since URLs end up in proxy logs, but far better than nothing.
- IP allowlisting. Works until the provider changes its addresses, so it should never be the only measure.
- HTTPS. Not authentication, but without it the request, along with the job ID, the summary and the secret token in the URL, can be read by anything along the route.
uChecker does not sign the body. The setup that works is a secret token in webhook_url, HTTPS, and a handler that does not trust the numbers in the body. On each notification it calls the API with its own key and reads the status and results from there. A forged request can at worst make you fetch data one extra time; it cannot change that data.
uChecker will not call private or internal addresses (localhost, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and the like), so a test handler has to be reachable from outside, through a tunnel if nothing else.
Where it fits
- Cleaning a list before a send. A script exports a CRM segment, submits it and exits. The webhook kicks off the next step: fetch the results, suppress the bad addresses, post to the team chat that the list is ready.
- Checking at signup. The account is created at once with an “email unverified” status while the address is checked in the background. If the result comes back bad, the user is asked for another address. This does not replace instant feedback in the form itself, which is the job of syntax checks and typo suggestions; see the guide to validation in signup forms.
- A stream of leads. Contacts from partner forms and imports collect in a queue, each batch goes out for checking, and the webhook decides where it goes next.
ESP webhooks for deliveries, bounces and opens work in a similar way but are a separate topic, covered in the email event webhook guide.
Sources
Your API key is in the dashboard. Connect uChecker. The first 100 checks are free.
Mark Avrilov · Author
Published
