Validate emails in ActiveCampaign

Check the addresses of your ActiveCampaign contacts and tag each contact with the result, so your segments and automations can leave out the addresses you should not mail. Two small pieces do the work, and you can use either or both:

  • A sync command checks every contact of a list (or of the whole account) that has no result yet, in one bulk list.
  • A webhook relay checks each new contact within seconds.

Both run in your own accounts with your own keys: the command with Node, the relay as a Cloudflare Worker. Nothing is in the ActiveCampaign apps marketplace yet, so there is nothing to install there.

Before you start

  • An API key. Create one on the API keys page. The relay needs the permission "Validate addresses"; the sync command also needs "Bulk lists", which is off by default. A daily credit cap limits what a busy list can spend.
  • Your ActiveCampaign URL and key. In ActiveCampaign: Settings → Developer. Copy the URL exactly as it is shown there, such as https://<account>.api-us1.com; the pieces send requests only to that host. The Key belongs to your user and can do what that user can do, so keep it secret. The user needs to view and edit contacts and manage tags.
  • Node 22.18 or later for the sync command, and a Cloudflare account for the relay.
  • The files, which are built from our tested code:
    • sync.mjs: the sync command, one Node module;
    • relay.js: the webhook relay, one Cloudflare Worker module;
    • webhook.json: the webhook's settings, for ActiveCampaign's API.

The tags

TagMeaning
ev:valid, ev:invalid, ev:catch_all, ev:unknown, ev:disposablethe result status; a contact has exactly one, and a new check replaces the old one
ev:removethe action is remove: do not mail this contact

Spam traps and complainers are ev:valid with ev:remove, so build your segment on ev:remove: Tag is ev:remove holds every contact you should not mail, and an automation can start when the tag is added. The pieces create their six tags the first time they need them and never touch your own tags.

Clean a list

A list's id is the number in the page's address while the list is open (Contacts → Lists), such as 3 in …listid=3.

export EV_API_KEY=…  ACTIVECAMPAIGN_URL=https://<account>.api-us1.com  ACTIVECAMPAIGN_API_TOKEN=…
node sync.mjs --list 3 --dry-run    # counts what it would check
node sync.mjs --list 3              # checks and tags
node sync.mjs --list 3 --suppress   # also unsubscribes from list 3 the contacts whose action is remove
node sync.mjs --all                 # every contact of the account, on any list or none (tags only)

It lists the contacts, skips those that already have a status tag other than ev:unknown (add --revalidate to check them again), checks the rest in one bulk list and tags each contact. It prints contact ids and counts, never addresses.

If a run stops for any reason, such as no credits or a closed laptop, run the same command again. It keeps the bulk job's id in a small state file (ids only, no addresses) and resumes that job, so a list is never paid for twice. Exit code 75 means the job was still running when the command stopped waiting: run it again later.

--suppress unsubscribes each contact whose action is remove from the list you passed. The contact keeps its data, history and other lists, and you can subscribe it again. ActiveCampaign runs that list's unsubscribe automations and notifications, so check them before the first run. Nothing is ever deleted.

Other options: --status active,unconfirmed for other list statuses, --region eu for EU data residency, --rate for ActiveCampaign requests per second (default 4, at most 5, which ActiveCampaign allows per account across all your apps), and --wait-minutes for how long to wait for a bulk list (default 240). ActiveCampaign sends no webhook for contact imports, so run the sync after an import, or on a schedule.

Check new contacts with the relay

  1. Deploy the relay to your Cloudflare account and give it its secrets:

    npx wrangler deploy relay.js --name ev-activecampaign-relay --compatibility-date 2026-10-01 \
      --var ACTIVECAMPAIGN_URL:https://<account>.api-us1.com
    npx wrangler secret put EV_API_KEY --name ev-activecampaign-relay
    npx wrangler secret put ACTIVECAMPAIGN_API_TOKEN --name ev-activecampaign-relay
    npx wrangler secret put RELAY_SECRET --name ev-activecampaign-relay

    RELAY_SECRET needs at least 32 random letters and digits, for example from openssl rand -hex 32. For EU data residency, add --var EV_REGION:eu to the deploy command.

  2. In ActiveCampaign, open Settings → Developer → Webhooks → Add:

    • URL the relay's address followed by / and your RELAY_SECRET, such as https://ev-activecampaign-relay.<your-subdomain>.workers.dev/<RELAY_SECRET>;
    • List the list whose new contacts to check, or all lists;
    • Type Contact added only; Initialize from all sources.

    To create the webhook with ActiveCampaign's API instead, send webhook.json, with your URL filled in, to POST /api/3/webhooks.

  3. Add a test contact with the address no_mailbox@sandbox.email-validation.org, a test address that costs nothing and receives no mail. Within a few seconds it has the tags ev:invalid and ev:remove. Then delete the test contact, and keep it out of campaigns until then.

The relay reads the contact by its id and checks the address stored on it, never an address in the delivery. The secret in the URL is what proves a delivery comes from your webhook, so anyone who can read that URL can send deliveries to the relay. Keep request logs that record URLs off for the Worker, and change the secret if it leaks.

Answers and retries

ActiveCampaign never delivers a webhook again after a failure, and the same delivery can arrive twice. So the relay tries a step once more itself when it meets a rate limit or an outage, and leaves anything it could not do to the next sync run:

The relay answersWhenWhat to do
200 savedThe contact was checked and taggedNothing
200 ignored or skippedNot a new contact, the contact is gone, or it already has a status tagNothing
200 not savedThe check ran, but ActiveCampaign did not take the tagsRun the sync command to catch up
503A rate limit or an outage before the checkRun the sync command to catch up
422Our API refused the key, there are no credits or the key reached its daily cap; or ActiveCampaign refused its keyFix the cause, then run the sync command
404The secret in the address is wrongCheck the webhook's URL

Nothing is charged for a delivery that answers 503 or 422. Before it checks, the relay reads the contact's tags and skips one that already has a status tag, so a repeated delivery costs nothing. If you rely on the relay, run the sync command on a schedule as well.

Costs and limits

  • Each checked address is one check. Results with the status unknown are free, and so are malformed addresses, duplicates in a list and the test addresses.
  • ActiveCampaign allows 5 API requests a second per account. The sync command needs about one request per contact it tags (two for ev:remove) at its default rate of 4 a second, so a large account takes time.
  • Our own rate limits apply as to any other client.

Privacy

The pieces send each address only to our API, for the check, and to your ActiveCampaign account's API for that contact's tags. Their answers and output never contain an address or a key, and the relay writes no logs. The sync command's state file holds job ids only.

Updated · Maintained by the email-validation.org team