Validate emails in HubSpot workflows
A HubSpot workflow can check each contact's email address as the contact enrolls, and save the result in a contact property. This recipe uses HubSpot's Custom code workflow action, so it works today with your API key and needs no app to install.
Before you start
- HubSpot plan. The Custom code action needs Data Hub Professional or Enterprise.
- An API key for HubSpot. Create one on the API keys page with the permission "Validate addresses", and give it a daily credit cap to limit what a busy workflow can spend.
- A contact property for the result. In HubSpot, create a single-line text contact property such as Email validation status. It will hold one of the five result statuses:
valid,invalid,catch_all,unknownordisposable.
Add the custom code action
- Create or open a contact-based workflow, for example one that enrolls contacts when they are created.
- Add the action Custom code and choose the Node.js language (the newest version HubSpot offers).
- Under Secrets, add a secret named
EV_API_KEYand paste your API key. The key stays out of the code. - Under Properties to include in code, add the contact's Email property with the name
email. - Replace the code with the script below.
- Under Data outputs, add five outputs of type String:
status,reason,action,did_you_meananderror_code. - Test the action on a contact whose email is a test address, such as
accepted@sandbox.email-validation.org: it returnsvalidand costs nothing.
Save the status on the contact
After the custom code action, add HubSpot's Edit record action and set your Email validation status property to the custom code's status output. HubSpot fills the value in from the action, with no second API call.
When a check cannot finish, status is empty and error_code says why. To leave the property unchanged in that case, put Edit record on an If/then branch that continues only when error_code is empty, and use the other branch to notify someone or to retry later. You can also branch on action (send, send_with_caution, remove or retry_later) to decide what happens to the contact next.
The script
// email-validation.org: check the enrolled contact's email (HubSpot workflow "Custom code" action, Node.js 24).
// Secret: EV_API_KEY. Property to include in code: email.
// Data outputs (all String): status, reason, action, did_you_mean, error_code.
const axios = require('axios');
// EU data residency: https://api-eu.email-validation.org/v1/validate
const API_URL = 'https://api.email-validation.org/v1/validate';
// Busy or rate limited: worth another try later (as is a 429 or 503 without our error body). Others are final.
const RETRY_LATER = ['rate_limited', 'abuse_throttled', 'service_unavailable', 'upstream_capacity'];
exports.main = async (event, callback) => {
const outputs = { status: '', reason: '', action: '', did_you_mean: '', error_code: '' };
const email = String(event.inputFields.email || '').trim();
if (!email) return callback({ outputFields: { ...outputs, error_code: 'no_email' } });
try {
const { data } = await axios.post(
API_URL,
{ email },
{
headers: { Authorization: `Bearer ${process.env.EV_API_KEY}` },
timeout: 18000,
maxRedirects: 0,
},
);
callback({
outputFields: {
...outputs,
status: data.status,
reason: data.reason,
action: data.action,
did_you_mean: data.did_you_mean || '',
},
});
} catch (err) {
const status = err.response?.status;
const code = err.response?.data?.error?.code;
// Rethrowing a 429 or 503 makes HubSpot run this action again later (for up to three days).
// The error first loses the request it carries, so the key never reaches the action's logs.
if (RETRY_LATER.includes(code) || (!code && (status === 429 || status === 503))) {
delete err.config;
delete err.request;
if (err.response) delete err.response.request;
throw err;
}
const fallback = err.response ? 'unexpected_response' : 'connection_error';
callback({ outputFields: { ...outputs, error_code: code || fallback } });
}
};For EU data residency, change API_URL to the address in the comment above it: the same key works on both.
Errors and retries
error_code | What it means | What to do |
|---|---|---|
| (the action runs again) | The API was busy or the key hit its per-second rate limit | Nothing: the script hands the error back to HubSpot, which runs the action again later, for up to three days |
no_email | The contact has no email address | Nothing was checked or charged |
unauthorized | The key in EV_API_KEY was refused | Check the secret; create a new key if it was revoked |
insufficient_credits | The account has no credits left | Buy a pack, then re-enroll the contacts |
key_daily_cap | The key reached its daily credit cap | Re-enroll the contacts the next day, when the key works again |
connection_error | No answer arrived in time | Re-enroll the contact; the script does not resend by itself, because the check may already have counted |
unexpected_response | The API answered something the script does not expect | Re-enroll the contact; if it repeats, contact support |
Other codes come from the error list. Every error code is returned as a value of the error_code output, so a branch can act on it.
Costs and limits
- Each enrolled contact is one check. Results with the status
unknownare free, and so are the test addresses. - HubSpot stops a custom code action after 20 seconds; the script gives the API 18 of them.
- A workflow that enrolls many contacts at once can reach the API's rate limits. HubSpot retries those runs by itself; to avoid them, set a rate limit on the custom code action in the workflow editor.
- For a whole list at once, upload it as a bulk job instead and import the results into HubSpot.