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¶
ueiis 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.
realtimeworks but won't surface changes faster than the underlying data refresh.
Related¶
- Vendor watchlist — track new contracts for known UEIs (the contract-side complement to this recipe)
- Webhooks user guide — protocol details, payload format, retry/circuit-breaker, security
- Entities API reference — full filter list for
query_type=entity - Recipes index