Skip to content
Agentmart
Submit a tool

suppress-hard-bounced

Identify and suppress hard-bounced contacts to protect email sender reputation.

Skill by tomgranot

IndexedMIT★ 77
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/tomgranot-suppress-hard-bounced

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Identify and suppress hard-bounced contacts to protect email sender reputation. Hybrid approach: API for discovery and audit, manual UI for suppression (hs_marketable_status is read-only via API).

Use cases: design, backend, security, productivity

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md and code. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found
Runs as
Python package

Network hosts it talks to 2

  • api.hubapi.com · found in code
  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/suppress-hard-bounced

What's inside

Skill name
suppress-hard-bounced
Description
Identify and suppress hard-bounced contacts to protect email sender reputation. Hybrid approach: API for discovery and audit, manual UI for suppression (hs_marketable_status is read-only via API).
License
MIT
Metadata
author: tomgranot · version: 1.1 · category: database-hygiene
Instructions
335 lines in SKILL.md · view on GitHub

Before installing

  • Ships executable scripts
  • Instructions call the network

Files (3)

  • SKILL.md
  • after.py · scripts/
  • before.py · scripts/

SKILL.md (first lines)

# Suppress Hard-Bounced Contacts

## Purpose

Hard-bounced contacts have permanently undeliverable email addresses. Every email sent to them fails, wastes send volume, and actively damages sender reputation with ISPs like Gmail, Microsoft, and Yahoo. This skill identifies all hard-bounced contacts, exports an audit trail, creates a HubSpot active list for ongoing monitoring, and guides the user through manual suppression in the UI.

## Prerequisites

- A HubSpot service key (`HUBSPOT_ACCESS_TOKEN` in `.env`; an existing legacy private app token also works) with `crm.objects.contacts.read` and `crm.lists.read`/`crm.lists.write` scopes
- Python 3.10+ with `uv` for package management
- A `.env` file containing `HUBSPOT_ACCESS_TOKEN`
- Super Admin or Marketing Hub Admin permissions for the manual UI suppression step

## Scripts

Script paths are relative to this skill's directory. Run them from your project directory so `.env`, `data/`, and `reports/` resolve there: prefix each path with wherever this skill is installed (`skills/suppress-hard-bounced/` in a clone of the repo).

| Stage | Script | Run with |
|-------|--------|----------|
| Before | [`scripts/before.py`](./scripts/before.py) | `uv run scripts/before.py` |
| After | [`scripts/after.py`](./scripts/after.py) | `uv run scripts/after.py` |

There is no execute script: the marketing-status change itself must happen via a HubSpot workflow or the UI (see Key Constraint and Stage 3).

## Key Constraint

**`hs_marketable_status` is read-only via the API.** You cannot set a contact to non-marketing programmatically. The API is used for discovery, analysis, and audit trail generation. The actual suppression must happen in the HubSpot UI.

## Execution Pattern

This skill follows a 4-stage execution pattern: **Plan -> Before -> Execute -> After**.

### Stage 1: Plan

Before writing any code, confirm with the user:

1. **Understand the impact**: Suppressed contacts remain in the CRM but stop counting toward the marketing contact billing tier. They cannot receive marketing emails.
2. **Non-marketing processing timing**: HubSpot processes non-marketing status changes at the start of the next billing cycle. Billing savings are not immediate.
3. **High-bounce contacts**: Contacts with 3+ bounces are the most severe reputation risk. Ask whether the user wants a separate review list for potential deletion.

### Stage 2: Before

Discover all hard-bounced contacts, break down by bounce reason, and generate an audit CSV.

```python
"""
Before State: Count and audit hard-bounced contacts.
Creates:
  1. A HubSpot active list for ongoing monitoring
  2. A local CSV audit log of all affected contacts
"""
import os
import csv
import time
import requests
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
BASE = "https://api.hubapi.com"
headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
}

url = f"{BASE}/crm/v3/objects/contacts/search"

# --- Step 1: Paginated search for all hard-bounced contacts ---
search_payload = {
    "filterGroups": [
        {
            "filters": [
                {
                    "propertyName": "hs_email_hard_bounce_reason_enum",
                    "operator": "HAS_PROPERTY",
                }
            ]
        }
    ],
    "properties": [
        "email", "firstname", "lastname",
        "hs_email_hard_bounce_reason_enum",
        "hs_email_bounce", "lifecyclestage",
        "hs_marketable_status", "createdate",
    ],
    "limit": 100,
}

all_contacts = []
after = None

while True:
    payload = search_payload.copy()
    if after:
        payload["after"] = after

    resp = requests.post(url, headers=headers, json=payload)
    resp.raise_for_status()
    data = resp.json()

    for contact in data.get("results", []):
        props = contact.get("properties", {})
        all_contacts.append({
            "id": contact["id"],
            "email": props.get("email", ""),
            "firstname": props.get("firstname", ""),
            "lastname": props.get("lastname", ""),
            "hard_bounce_reason": props.get("hs_email_hard_bounce_reason_enum", ""),
            "bounce_count": props.get("hs_email_bounce", ""),
            "lifecycle_stage": props.get("lifecyclestage", ""),
            "marketable_status": props.get("hs_marketable_status", ""),
            "createdate": props.get("createdate", ""),
        })

    paging = data.get("paging", {})
    after = paging.get("next", {}).get("after")
    if not after:
        break

From TomGranot/hubspot-admin-skills, MIT.

More from TomGranot/hubspot-admin-skills

Signals

Stars
77
Forks
18
Open issues
0
Last commit (this folder)
Oct 12, 2026 (16 days ago)

From GitHub (fetched Oct 24, 2026).

Details

Version
1.1
License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from tomgranot

Cite this page

suppress-hard-bounced skill. Agent Mart. https://agentmart.dev/skills/tomgranot-suppress-hard-bounced (last updated 2026-10-24)