# Validate Emails in HubSpot Workflows

URL: https://email-validation.org/docs/guides/hubspot/
Updated: 2026-11-03

> Documentation index: https://email-validation.org/llms.txt

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](https://email-validation.org/app/api-keys/) with the permission "Validate
  addresses", and give it a [daily credit cap](https://email-validation.org/docs/authentication/#limits) 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](https://email-validation.org/docs/statuses/): `valid`, `invalid`,
  `catch_all`, `unknown` or `disposable`.

## Add the custom code action

1. Create or open a **contact-based** workflow, for example one that enrolls contacts when they are created.
2. Add the action **Custom code** and choose the **Node.js** language (the newest version HubSpot offers).
3. Under **Secrets**, add a secret named `EV_API_KEY` and paste your API key. The key stays out of the code.
4. Under **Properties to include in code**, add the contact's **Email** property with the name `email`.
5. Replace the code with the [script below](#script).
6. Under **Data outputs**, add five outputs of type **String**: `status`, `reason`, `action`, `did_you_mean` and
   `error_code`.
7. Test the action on a contact whose email is a [test address](https://email-validation.org/docs/test-addresses/), such as
   `accepted@sandbox.email-validation.org`: it returns `valid` and 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

```js
// 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](https://email-validation.org/pricing/), 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](https://email-validation.org/docs/errors/). 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 `unknown` are free, and so are the
  [test addresses](https://email-validation.org/docs/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](https://email-validation.org/docs/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](https://email-validation.org/docs/bulk/) instead and import the results into HubSpot.
