Skip to content

Tango – Webhooks Partner Guide

Welcome! This guide walks you through enabling and consuming outbound webhooks from Tango so your application can react to fresh federal-spending data without polling our API.

Jump to:


1. What you get

Tango pushes a JSON payload to your server whenever new data matches a saved filter — a filter alert. Today this includes:

  • Awards / contracts
  • Opportunities
  • Entities
  • Grants
  • Forecasts
  • Near–real-time: you'll be notified minutes after Tango's ingestion cycle finishes.
  • Lightweight: one payload lists the IDs that changed and a small summary per match — pull full details from the existing REST API as needed.
  • Filter-based: define a saved query (the same filters you'd pass to the API) and receive an event whenever new or updated rows match.
  • Reliability features: automatic circuit breaker pattern for endpoint health management and intelligent retry strategies.

Note (Awards): For correctness, awards webhooks are published after award bundling/materialization so a webhook will not arrive before the corresponding award is queryable via the API.

1.1 When events are published ("data is ready")

We intentionally publish events only once the underlying data is queryable via the API.

  • Contracts (alerts.contract.match): published once new contracts are queryable via the API.
  • Opportunities (alerts.opportunity.match): published after each opportunities refresh.
  • Entities (alerts.entity.match): published after each entities refresh (including DSBS updates).
  • Grants (alerts.grant.match): published after each grants refresh.
  • Forecasts (alerts.forecast.match): published after each forecasts refresh.

1.2 Filter subscriptions (alerts)

Filter subscriptions let you create persistent monitoring rules based on query filters. For example, "alert me when new opportunities matching NAICS 541512 appear at HHS" or "notify me of new contracts awarded to small businesses in Virginia."

You define a query type (the resource to monitor) and a set of filters (the same query parameters you would use on the corresponding API endpoint). Tango periodically evaluates your filters against new and updated data; when matches are found, Tango delivers a webhook to your endpoint with alerts.<query_type>.match events.

Available query types

Query type Resource monitored Filters from
opportunity Contract opportunities /api/opportunities/ filters
contract Contracts /api/contracts/ filters
idv IDVs /api/idvs/ filters
ota OTAs /api/otas/ filters
otidv OTIDVs /api/otidvs/ filters
entity Entities /api/entities/ filters
grant Grants /api/grants/ filters
forecast Forecasts /api/forecasts/ filters

Alert event types

When a filter subscription matches, the delivered event uses the alerts.* namespace. There are five:

  • alerts.opportunity.match
  • alerts.contract.match — also emitted for idv, ota, and otidv query types
  • alerts.entity.match
  • alerts.grant.match
  • alerts.forecast.match

If you subscribe to idv, ota, or otidv, matches arrive as alerts.contract.match events; inspect query_type on the event to distinguish.

Frequency options

Frequency Description Tier requirement
realtime Evaluated after each ingestion cycle All tiers
daily Evaluated once per day All tiers
weekly Evaluated once per week All tiers
custom Custom cron expression (5-field) Pro+ (Micro and above)

Tier limits

Filter subscriptions are available to all tiers, including Free:

Tier Max filter subscriptions
Free 1
Micro 3
Small 5
Medium 10
Large 25
Enterprise custom

Convenience API: /api/webhooks/alerts/

The alerts API provides a simpler shape for creating and managing filter subscriptions:

Create an alert:

POST /webhooks/alerts/
Content-Type: application/json
{
  "name": "HHS IT services opportunities",
  "query_type": "opportunity",
  "filters": {
    "agency": "7500",
    "naics": "541512"
  },
  "frequency": "daily",
  "endpoint": "<your-endpoint-id>"
}

Returns 201 Created with the alert details. If a subscription with the same query_type + filters already exists, returns 200 OK with the existing alert (dedup).

When endpoint is required

The endpoint field is required if your account has more than one webhook endpoint configured (the server returns 400 otherwise so deliveries can't go to the wrong receiver). Accounts with a single endpoint may omit it — Tango routes to that endpoint automatically. List your endpoints with GET /api/webhooks/endpoints/ or client.list_webhook_endpoints() in the SDKs.

Tip — multi-value filters: Use | (pipe) to match any of several values. The same syntax works on every filter that accepts multiple values (naics, psc, agencies, set-asides, etc.).

"filters": {"naics": "541511|541512"}

One known exception: /api/contracts/'s uei filter is single-value only. To watch multiple vendors, create one alert per vendor — see the vendor watchlist recipe.

List alerts:

GET /webhooks/alerts/

Response:

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "alert_id": "e4c4...",
      "name": "HHS IT services opportunities",
      "query_type": "opportunity",
      "filters": {"agency": "7500", "naics": "541512"},
      "frequency": "daily",
      "cron_expression": null,
      "status": "active",
      "created_at": "2026-03-14T12:00:00Z",
      "last_checked_at": null,
      "match_count": 0
    }
  ]
}

Update an alert:

PATCH /webhooks/alerts/{alert_id}/

You can update name, frequency, cron_expression, and is_active. Filters and query type cannot be changed after creation (delete and recreate instead).

Delete an alert:

DELETE /webhooks/alerts/{alert_id}/

Dedup behavior

If you create a filter subscription with the same query_type and filter parameters as an existing one (for the same endpoint), the API returns the existing subscription instead of creating a duplicate. The dedup is based on a SHA-256 hash of the canonical filter representation.

Current status

The filter subscription API is fully functional for creating, listing, updating, and deleting alerts. A background task evaluates filter subscriptions on their configured schedule (realtime, daily, weekly, or custom cron) and delivers matches automatically.


2. Access requirements

Filter subscriptions are available to all tiers, including Free, with per-tier caps on how many you can create simultaneously (see the table in §1.2 — Tier limits). No tier upgrade is required to start using webhooks.


3. On-boarding checklist

  1. Provide a callback URL — a publicly reachable https:// endpoint that accepts HTTP POSTs. Recommended path: /tango/webhooks.
  2. Receive your shared secret — Tango generates a 32-byte hex secret (64 hex chars) and shares it out-of-band. You'll use this to verify signatures.
  3. Test your endpoint — use the test delivery endpoint (§5.5) to verify connectivity.
  4. Create one or more filter alerts — define a query_type + filters per saved query you want to monitor. See §1.2 (Convenience API) and the recipes.

Note We create the initial Webhook Endpoint record for you. All subsequent management of subscriptions/alerts is done via the Subscription API below.


4. Security & authenticity

Every POST from Tango includes an HMAC-SHA-256 signature:

X-Tango-Signature: sha256=<hex digest>

The digest is computed over the raw request body using your secret. Verify it like so (Python snippet):

import hmac, hashlib, os, flask, json

SECRET = os.environ["TANGO_WEBHOOK_SECRET"]
app = flask.Flask(__name__)

@app.post("/tango/webhooks")
def recv():
    body = flask.request.get_data()
    sig  = flask.request.headers.get("X-Tango-Signature", "")[7:]  # strip "sha256="

    if not hmac.compare_digest(
        hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest(), sig):
        return "Invalid signature", 401

    payload = json.loads(body)
    # ... handle events ...
    return "ok", 200

If you respond with any 2xx status, Tango marks the delivery as successful. Non-2xx responses and time-outs are retried (at-least-once delivery). Current retry limits are error-aware:

  • 4xx: up to 2 attempts total
  • 5xx: up to 5 attempts total
  • network errors/timeouts: up to 7 attempts total

5. Subscription API

Base URL: https://tango.makegov.com/api/webhooks/ Auth: API Key – send X-API-KEY: <your-key> (or an OAuth2 bearer token via Authorization: Bearer <token>)

5.0 Discover supported event types

Tango exposes a discovery endpoint so clients can validate configurations without hard-coding event types:

GET /webhooks/event-types/

Response:

{
  "event_types": [
    {
      "event_type": "alerts.contract.match",
      "description": "A new or modified contract matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.opportunity.match",
      "description": "A new or modified opportunity matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.entity.match",
      "description": "A new or modified entity matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.grant.match",
      "description": "A new or modified grant matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.forecast.match",
      "description": "A new or modified forecast matched a saved filter alert.",
      "schema_version": 1
    }
  ]
}

Note: Only some event types may be actively emitted at any given time. If you create an alert for a query type that has no matching new data, you simply won't receive events until matches appear.

5.1 List current alerts / filter subscriptions

GET /webhooks/alerts/

Response

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "alert_id": "e4c4…",
      "name": "Track DOD IT services > $1M",
      "query_type": "contract",
      "filters": {"funding_agency": "DOD", "naics": "541512", "obligated_gte": "1000000"},
      "frequency": "realtime",
      "cron_expression": null,
      "status": "active",
      "created_at": "2024-06-01T12:00:00Z",
      "last_checked_at": "2024-06-01T18:00:00Z",
      "match_count": 14
    }
  ]
}

5.3 Update an existing alert

PATCH /webhooks/alerts/{alert_id}/

Updatable fields: name, frequency, cron_expression, is_active. filters and query_type are immutable — delete and recreate to change them.

5.4 Delete an alert

DELETE /webhooks/alerts/{alert_id}/

If you delete every alert, you will stop receiving webhooks until you create at least one new one.

Note: You must maintain at least one active alert to receive webhooks.

5.5 Test webhook delivery

POST /webhooks/endpoints/test-delivery/

Sends a test webhook to your endpoint to verify connectivity. Returns detailed information about the delivery attempt.

Response (success):

{
  "success": true,
  "status_code": 200,
  "response_time_ms": 145,
  "message": "Test delivery successful! Response time: 145ms. Your webhook endpoint is configured correctly."
}

Response (failure):

{
  "success": false,
  "status_code": 500,
  "message": "Test delivery failed with status 500. Please check your endpoint implementation and ensure it returns 2xx status codes.",
  "response_body": "<first 1000 chars of response>"
}

5.6 Get sample payload

GET /webhooks/endpoints/sample-payload/?event_type=alerts.contract.match

Returns a sample webhook payload for testing your handler implementation.

Notes:

  • GET /webhooks/endpoints/sample-payload/ (no params) returns samples for all supported event types.
  • GET /webhooks/endpoints/sample-payload/?event_type=<event_type> returns a single sample.

Response:

{
  "event_type": "alerts.contract.match",
  "sample_delivery": {
    "timestamp": "2026-05-13T00:36:46.157804",
    "events": [
      {
        "event_type": "alerts.contract.match",
        "created_at": "2026-05-13T00:36:46.157779",
        "subscription_id": "11111111-2222-3333-4444-555566667777",
        "new_ids": ["MATCH_001", "MATCH_002"],
        "modified_ids": []
      }
    ]
  },
  "signature_header": "X-Tango-Signature: sha256=<hmac_sha256_signature>",
  "note": "The signature is generated using HMAC-SHA256 with your endpoint's secret key."
}

Preview vs production payload shape

The /webhooks/endpoints/sample-payload/ endpoint emits a simplified preview — no delivery_id, no matches.new[] summary objects, just new_ids / modified_ids. Real deliveries from the dispatcher carry the richer shape documented in §6, including delivery_id, the matches.new / matches.modified arrays with per-record summary fields, and the echoed filters / query_type. Build your receiver against §6, not against this sample. (Server-side reconciliation of the two shapes is tracked separately.)


6. Payload format

A single delivery looks like:

{
  "timestamp": "2026-05-11T18:20:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.contract.match",
      "created_at": "2026-05-11T18:20:12.482Z",
      "alert_id": "e4c4...-...-...",
      "query_type": "contract",
      "filters": {"funding_agency": "DOD", "naics": "541512", "obligated_gte": "1000000"},
      "matches": {
        "new_count": 2,
        "modified_count": 0,
        "new": [
          {"id": "CONT_AWD_...", "piid": "W15QKN24C1234", "obligated": 1450000, "recipient_uei": "ABC123..."},
          {"id": "CONT_AWD_...", "piid": "FA8773-24-C-...", "obligated": 2300000, "recipient_uei": "DEF456..."}
        ],
        "modified": [],
        "new_ids": ["CONT_AWD_...", "CONT_AWD_..."],
        "modified_ids": []
      },
      "checked_at": "2026-05-11T18:20:12.000Z"
    }
  ]
}

The new_ids / modified_ids arrays are deprecated but still sent; prefer matches.new / matches.modified which carry summary objects per match.

6.1 Per-event fields

Field Type Notes
event_type string One of alerts.{contract,opportunity,entity,grant,forecast}.match
alert_id UUID Stable id of the alert that produced this match
query_type string contract / opportunity / entity / grant / forecast
filters object Echo of the alert's saved filters
matches.new array Newly-matched records (summary objects, not full API rows)
matches.modified array Existing records whose key fields changed
matches.new_count / matches.modified_count int Counts of the above
checked_at ISO8601 When the evaluator scanned for matches

Match summary objects always include the resource's primary id (id for contracts, opportunity_id for opportunities, uei for entities, grant_id for grants, forecast_id for forecasts) plus a small set of high-signal fields. Pull the full record from the corresponding /api/<resource>/ endpoint when you need more.

Key facts:

  • Batched — events are grouped per endpoint per dispatch run.
  • At-least-once — retries can cause duplicates; your handler must be idempotent. If you need a de-dupe key, prefer the top-level delivery_id (UUID, stable across retries of the same dispatch).
  • Server-side filtered — only events matching your alerts are sent.

Examples

These are copy/paste alert payloads for common use cases.

For full how-tos with receiver code and pitfalls, see the recipes.

Track DOD IT-services contracts over $1M

{
  "name": "DOD IT services > $1M",
  "query_type": "contract",
  "filters": {
    "funding_agency": "DOD",
    "naics": "541512",
    "obligated_gte": "1000000"
  },
  "frequency": "realtime"
}

Watch HHS opportunities in NAICS 541512

{
  "name": "HHS opportunities — IT consulting (541512)",
  "query_type": "opportunity",
  "filters": {
    "agency": "7500",
    "naics": "541512"
  },
  "frequency": "realtime"
}

Track entity registrations changing in your pipeline

{
  "name": "Pipeline entities — registration changes",
  "query_type": "entity",
  "filters": {
    "uei": "ABC123XYZ456"
  },
  "frequency": "daily"
}

(Entities currently take a single uei per alert — for multi-entity tracking, create one alert per UEI.)

Watch cybersecurity grants by CFDA

{
  "name": "DHS cybersecurity grants",
  "query_type": "grant",
  "filters": {
    "agency": "DHS",
    "cfda_number": "97.067"
  },
  "frequency": "daily"
}

Monitor upcoming Air Force IT acquisitions in the forecast

{
  "name": "Air Force forecasts — IT (541512 + nearby)",
  "query_type": "forecast",
  "filters": {
    "agency": "AF",
    "naics_starts_with": "5415"
  },
  "frequency": "weekly"
}

8. Best practices on your side

  1. Make your handler idempotent — Tango guarantees at-least-once delivery, so your endpoint may receive the same event more than once. Deduplicate on delivery_id (see Delivery guarantee).
  2. Return 200 quickly — enqueue the work in your own job queue and respond; do not block processing.
  3. Test your endpoint first — use the test delivery endpoint to verify connectivity before going live.
  4. Harden the endpoint — HTTPS only, accept POST only, max-payload 256 KB.
  5. Store the last successful timestamp — helps spot missed deliveries.
  6. Use exponential back-off when pulling details — Tango's public API has rate limits; stagger follow-up fetches if you receive a large batch.
  7. Tighten your filters — broad filters generate large match batches; narrow filters to what you actually act on.
  8. Monitor endpoint health — Tango uses circuit breaker patterns to protect both systems from cascading failures.

9. Reliability features

Delivery guarantee

Tango webhooks provide at-least-once delivery. This means:

  • Every event that matches one of your alerts will be delivered to your endpoint (assuming it is reachable).
  • In rare cases (e.g., a transient failure between sending the HTTP request and recording its success), the same event may be delivered more than once.

Each webhook request includes a stable delivery_id (UUID) that uniquely identifies the dispatch. Use this as an idempotency key on your side to safely deduplicate:

# Example: skip already-processed deliveries
delivery_id = payload["delivery_id"]
if already_processed(delivery_id):
    return HttpResponse(status=200)

process(payload)
mark_processed(delivery_id)

Why not exactly-once? Guaranteeing exactly-once delivery over HTTP is not possible without cooperation from the consumer. Tango prevents duplicate internal delivery records via database constraints, but the HTTP POST itself can be retried if acknowledgment is lost. Designing your webhook handler to be idempotent ensures correct behavior regardless of delivery count.

Circuit Breaker Pattern

Tango implements automatic circuit breaker protection for webhook endpoints:

  • Automatic failure detection — After repeated failures (currently: 5 consecutive failures), the circuit opens
  • Cool-down — While open, deliveries are skipped for a short period (currently: 5 minutes)
  • Recovery testing — After cool-down, Tango allows a single probe delivery (half-open)
  • Automatic recovery — Upon success, normal operation resumes; upon failure, the circuit re-opens (currently: 10 minute cool-down)
  • Resource protection — Prevents wasting resources on consistently failing endpoints

This ensures both Tango and your systems remain stable even during outages.

Retry Strategy

Failed webhooks are retried with:

  • Error-aware retry limits — Retry caps differ by error type:
    • Client errors (4xx): up to 2 attempts total
    • Server errors (5xx): up to 5 attempts total
    • Network errors/timeouts: up to 7 attempts total

10. Troubleshooting

Symptom Most likely cause Next steps
Receive 401 from Tango on the alerts API Missing/invalid API key Ensure X-API-KEY: <your-key> header (or Authorization: Bearer <token> for OAuth2)
Webhook payloads stop arriving Circuit breaker activated due to failures Fix endpoint issues; circuit will auto-recover after cool-down
Test delivery fails Endpoint connectivity issues Check HTTPS cert, firewall rules, response time
Signature mismatch Using wrong secret or modified body Re-sync secret; ensure you hash the raw bytes exactly
Not receiving expected events Filter doesn't match upstream data Run the same filter against /api/<resource>/ first — if it returns nothing there, no events will fire
Receiving too many events Filter too broad Tighten filters; lower frequency (e.g. daily instead of realtime); split into multiple narrower alerts

Need help? Email [email protected] with your endpoint URL & the approximate timestamp of the last delivery you saw.


Happy shipping! 🚀