Skip to content

Track entity changes — monitor SAM.gov registration updates

Get a webhook when a vendor in your pipeline updates their SAM.gov registration — status flips (Active ↔ Inactive), address moves, NAICS reassignment, socioeconomic re-certification, CAGE updates. These changes can make or break proposal eligibility, set-aside qualification, and partner-vetting workflows, and they're invisible to award-side monitoring until a contract actually posts.

This is the entity-record companion to the vendor watchlist recipe. Vendor-watchlist tracks new contracts for a known UEI (query_type=contract); this recipe tracks changes to the entity record itself (query_type=entity).

The 1-line answer

Create a filter alert on query_type=entity. Filters are the same query parameters you'd pass to /api/entities/.

curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "Pipeline vendor — ACME Corp registration",
    "query_type": "entity",
    "filters": { "uei": "ABC123XYZ4567" },
    "frequency": "daily"
  }'

That's it. Tango re-evaluates daily and POSTs alerts.entity.match events when the entity record changes (or first appears, if the UEI is brand-new in SAM).

One alert per vendor — uei is single-value

The /api/entities/ uei filter is exact-match, single-value only (lookup_expr="iexact"). To watch N vendors by UEI, create N alerts — one per UEI. Same constraint as the vendor-watchlist recipe.

For attribute-based watches (e.g. "all small businesses in 541512 in Maryland"), see the second example below — that pattern uses naics / socioeconomic / state instead of uei and only needs one alert.

Step 1 — Verify the filter against the entities API

curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/entities/?uei=ABC123XYZ4567"

If this returns the entity record you expect, the alert will fire on changes. If it returns nothing, double-check the UEI — Tango only ingests entities present in SAM.gov.

Step 2 — Create the alert (track a portfolio by UEI)

for uei in ABC123XYZ4567 DEF456UVW7890 GHI789RST0123; do
  curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
    -H "Content-Type: application/json" \
    "https://tango.makegov.com/api/webhooks/alerts/" \
    -d "{
      \"name\": \"Pipeline entity — $uei\",
      \"query_type\": \"entity\",
      \"filters\": { \"uei\": \"$uei\" },
      \"frequency\": \"daily\"
    }"
done
import os
from tango import TangoClient

client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

pipeline = [
    ("ACME Corp",       "ABC123XYZ4567"),
    ("Beta Industries", "DEF456UVW7890"),
    ("Gamma LLC",       "GHI789RST0123"),
]

for label, uei in pipeline:
    alert = client.create_webhook_alert(
        name=f"Pipeline entity — {label}",
        query_type="entity",
        filters={"uei": uei},
        frequency="daily",
    )
    print(alert.alert_id, alert.status, label)
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

const pipeline = [
  { label: "ACME Corp",       uei: "ABC123XYZ4567" },
  { label: "Beta Industries", uei: "DEF456UVW7890" },
  { label: "Gamma LLC",       uei: "GHI789RST0123" },
];

for (const { label, uei } of pipeline) {
  const alert = await client.createWebhookAlert({
    name: `Pipeline entity — ${label}`,
    query_type: "entity",
    filters: { uei },
    frequency: "daily",
  });
  console.log(alert.alert_id, alert.status, label);
}

daily is the right frequency for entity tracking — SAM.gov refreshes nightly, so re-checking more often just burns alert budget without surfacing more changes. Use realtime only if you specifically need same-cycle delivery once SAM data lands in Tango.

Step 3 — Filter by attribute, not identity

Sometimes you want to discover new entities matching a profile, not watch a known list. For example: "alert me when a new small business registers in NAICS 541512 in Maryland." That's one alert with no uei:

curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "New MD small businesses in 541512",
    "query_type": "entity",
    "filters": {
      "naics": "541512",
      "socioeconomic": "A2",
      "state": "MD"
    },
    "frequency": "daily"
  }'
alert = client.create_webhook_alert(
    name="New MD small businesses in 541512",
    query_type="entity",
    filters={
        "naics": "541512",
        "socioeconomic": "A2",   # SBA Small Business
        "state": "MD",
    },
    frequency="daily",
)
const alert = await client.createWebhookAlert({
  name: "New MD small businesses in 541512",
  query_type: "entity",
  filters: {
    naics: "541512",
    socioeconomic: "A2",   // SBA Small Business
    state: "MD",
  },
  frequency: "daily",
});

Available EntityFilter keys (see the Entities API reference for the full list):

Filter Notes
uei Exact UEI (iexact, single value).
cage_code (alias cage) CAGE code (iexact). Pick one or the other — sending both is rejected.
name Substring match on legal_business_name (min length 2).
naics NAICS code; matches against the entity's registered NAICS list.
psc PSC code; matches against the entity's registered PSC list.
socioeconomic Business-type code (e.g. A2 small business, 27 woman-owned, XX 8(a)).
state Physical-address state code (e.g. MD, VA).
zip_code Physical-address ZIP (exact).
purpose_of_registration_code SAM purpose-of-registration code (e.g. Z2 All Awards).
total_awards_obligated_gte / _lte Lifetime contract+subaward total (USD); IDV/OTA excluded.
search Full-text search across name + UEI + CAGE (min length 2).

Step 4 — Receive alerts.entity.match events

Tango POSTs a signed JSON batch to your endpoint. Entity events split matches into new (first-time-registered UEIs that match your filter) and modified (existing records whose SAM data changed since the last check):

{
  "timestamp": "2026-05-12T06:05:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.entity.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "entity",
      "filters": { "uei": "ABC123XYZ4567" },
      "matches": {
        "new_count": 0,
        "modified_count": 1,
        "new": [],
        "modified": [
          {
            "id": "ABC123XYZ4567",
            "legal_business_name": "ACME Corporation",
            "registration_status": "Active"
          }
        ]
      },
      "checked_at": "2026-05-12T06:05:12.000Z"
    }
  ]
}

The summary fields delivered for query_type=entity are: id (the UEI), legal_business_name, and registration_status. To see what changed (which field flipped), pull the full record from /api/entities/{uei}/ (or client.get_entity(uei) / client.getEntity(uei)) and diff against your last-known copy on the receiver side — Tango doesn't ship a field-level diff in the alert payload.

For the full receiver implementation (signature verification, idempotency, fast 2xx, error handling), see Stream contract awards in real time — the receiver shape is identical, only the event type and summary fields change.

Step 5 — Manage the alert

# List your alerts
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/"

# Pause a vendor without deleting
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/" \
  -d '{"is_active": false}'

# Remove a vendor permanently
curl -X DELETE -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/"

name, frequency, cron_expression, and is_active are updatable. query_type and filters are immutable — to change the watched UEI, delete and recreate.

Troubleshooting

Symptom Cause What to do
Alert re-fires every day with the same modified UEI and nothing visibly changed SAM bumps modified timestamps on full-record refresh even when no field flipped. The evaluator picks up modified >= last_checked_at regardless of what changed. Diff the full record against your last-known copy on the receiver side; if no fields you care about changed, drop the event.
First-ever evaluation fires for a UEI you've watched before in another tool The matches.new bucket on the first run reflects records created in Tango since the lookback cutoff (default 24h on first eval) — not "first time you've ever seen this entity." A UEI freshly ingested into Tango lands in new even if it's been in SAM for years. Treat matches.new on a brand-new alert as "Tango first saw this," not "SAM first saw this." For the latter, check entity.created_date from the full record.
Status flip happens twice within one check window (Active → Inactive → Active) The evaluator sees the net state at evaluation time, not the intermediate flip. Two flips in 24h with frequency=daily look like no change at all. If you need every transition, use frequency=realtime (5-minute eval cadence) and accept the higher alert-budget burn.
cage and cage_code rejected together The filter accepts cage as an alias for cage_code; sending both at once is rejected with 400. Pick one. They filter the same column.
state=MD returns nothing for a vendor you know is in Maryland state filters on the physical address state, not the mailing address. Some entities register the two separately. Verify with curl ".../api/entities/?uei=<uei>" and check physical_address.state_or_province_code.
Alert never fires even though SAM clearly updated Check last_checked_at on the alert (GET /api/webhooks/alerts/<alert_id>/). If it's recent but no events were delivered, the SAM ingestion pipeline may not have refreshed yet — Tango ingests SAM nightly. Wait one full ingestion cycle, then re-check. If still nothing, ping support with the alert_id.

Limitations

  • uei is single-value. One alert per vendor when watching by UEI. See the warning at the top.
  • No field-level diff in the payload. You get the post-change summary; compute the diff on your side from the full record.
  • Tier caps apply. Free 1 / Micro 3 / Small 5 / Medium 10 / Large 25 simultaneous alerts across all alert types combined.
  • Daily is usually the right cadence. SAM refreshes nightly. realtime works but won't surface changes faster than the underlying data refresh.