How to verify emails in HubSpot with a workflow custom code action
· 4 min read · Tutorials
To verify emails in HubSpot, build a contact-based workflow with a custom code action that sends the contact's email to a verification API, returns the result as output fields, and copies those outputs into contact properties with an Edit record action. Then filter lists and sequences on the new "verification status" property. This tutorial does it with pingutil's /v1/verify, but the pattern works with any verifier that takes an email and returns JSON.
Everything below comes from HubSpot's custom code action documentation. Custom code actions need Data Hub Professional or Enterprise.
1. Create the contact properties
In Settings, Properties, create these contact properties:
| Property | Type | Values |
|---|---|---|
| Email verification status | Dropdown select | valid, invalid, risky, unknown |
| Email verification reason | Single-line text | e.g. mailbox_not_found, accept_all |
| Email verification score | Number | 0-100 |
| Email suggestion | Single-line text | A corrected address, if any |
Keeping the raw reason matters. "Risky because the domain is catch-all" and "risky because it's info@" lead to different decisions.
2. Store the API key as a secret
In the custom code action, add a secret named PINGUTIL_API_KEY. HubSpot exposes secrets to your code as environment variables, so the key never appears in the code itself.
3. Build the workflow
- Create a contact-based workflow. Enroll contacts when
Emailis known andEmail verification statusis unknown, so each contact is checked once. Add re-enrollment later if you want periodic re-checks. - Add a Custom code action, language Node.js.
- Under "Property to include in code", add
emailmapped to the contact's Email property. - Paste the code below.
- Under Data outputs, declare
verify_status(string),verify_reason(string),verify_score(number) anddid_you_mean(string). - Add an Edit record action after it for each property, using the code action's outputs as the values.
HubSpot's docs say outputs can feed the Edit record action directly, which saves a separate API call to update the contact.
4. The code
The custom code runtime includes axios, so we use it rather than relying on a global fetch.
const axios = require('axios');
exports.main = async (event, callback) => {
const email = event.inputFields['email'];
if (!email) {
callback({
outputFields: { verify_status: 'unknown', verify_reason: 'no_email', verify_score: 0, did_you_mean: '' },
});
return;
}
try {
const res = await axios.post(
'https://pingutil.com/v1/verify',
{ email },
{
headers: { Authorization: `Bearer ${process.env.PINGUTIL_API_KEY}` },
timeout: 15000,
},
);
const r = res.data;
callback({
outputFields: {
verify_status: r.status, // valid | invalid | risky | unknown
verify_reason: r.reason, // e.g. mailbox_not_found, accept_all, greylisted
verify_score: r.score, // 0-100
did_you_mean: r.did_you_mean || '',
},
});
} catch (err) {
const code = err.response ? err.response.status : 0;
if (code === 400) {
// The API rejected the request body (e.g. an over-long value). Record it, don't retry.
// A malformed address is not a 400: it comes back as status "invalid", reason "invalid_syntax".
callback({
outputFields: { verify_status: 'unknown', verify_reason: 'bad_request', verify_score: 0, did_you_mean: '' },
});
return;
}
// 429 (rate limit or daily quota), 5xx and network errors: throw so HubSpot retries.
throw err;
}
};
Two details from HubSpot's docs shape this code. Custom code actions must finish within 20 seconds, so the request timeout is 15 seconds. And if the action throws after a 429 or 5xx response from axios, HubSpot retries for up to three days, starting a minute after the failure. That fits pingutil's limits: 60 requests a minute per account returns 429 rate_limited, and the daily budget of 300 verifications returns 429 quota_exceeded until it resets at 00:00 UTC. A large enrollment simply drains over the following days instead of failing.
5. Use the result
Now the property does the work:
- Marketing sends and sequences: exclude
Email verification statusisinvalid. - Catch-all and unknown:
riskywith reasonaccept_allmeans the server accepts every address, so the mailbox can't be confirmed. Leave these in nurture but out of high-volume sends.unknown(greylisting, blocks, timeouts) deserves a re-check later; a second workflow with a delay and re-enrollment handles that. More on why in catch-all email domains. - Typos: when
Email suggestionis set, create a task for the owner to confirm with the contact. Don't overwrite the email automatically.
A no-code alternative and its limits
HubSpot's Send a webhook action supports POST and GET, authentication by request signature or API key, and response data that "can be used for certain actions". It's fine for posting contact data to your own endpoint. For a third-party API that expects a Bearer header and a specific JSON body, the custom code action is easier to get right. If you'd rather run your own service between HubSpot and the API, the webhook enrichment tutorial shows that pattern for domain lookups.
Before you turn it on
- Test the workflow on a few contacts, including a known-bad address and a role address like
info@. - Expect
validto mean "the server accepted this address now", not a guarantee. See what SMTP verification can't tell you. - Check pingutil's response fields in the verify API reference, or paste a few addresses into the email verification demo first.
Questions people ask
- Which HubSpot plan do I need for custom code actions?
- HubSpot's developer docs say custom code actions are available on Data Hub Professional or Enterprise. The Send a webhook workflow action is listed for the same tiers.
- What happens if the verification API is rate limited?
- If your custom code throws after a 429 or 5xx response from axios, HubSpot retries the action for up to three days, starting one minute after the failure, with gaps of up to eight hours between tries. That covers short per-minute limits and daily quotas that reset.
- Should a HubSpot workflow delete invalid contacts?
- No. Write the verification status to a property and use it to exclude contacts from marketing sends and sequences. Deleting loses the history of where the bad address came from.