Help Center / Dashboard & Reports / Sending results to your own systems (webhooks, postbacks & API)

Sending results to your own systems (webhooks, postbacks & API)

Have TrafficDefenseIQ call your backend or your tracker when an upload finishes scoring, or pull your data by API.

Under Settings → Integrations you can add destinations that TrafficDefenseIQ calls automatically the moment an upload finishes scoring. Anyone on your account can manage them. There are two kinds.

Webhooks — every plan

A webhook sends one JSON summary per upload to a URL you provide: the upload's ID and filename, its row count, the low/medium/high counts, and a link to the results. Each request is signed — verify it by computing an HMAC-SHA256 of "<timestamp>.<body>" with your signing secret and comparing it to the X-TDIQ-Signature header. The secret is shown once when you create the webhook, so copy it then.

Example payload — POST to your URL

{
  "event": "upload.completed",
  "upload": {
    "id": "5b1f6e9e-2f5b-4e7a-9c7e-9b6f2b2b6a11",
    "filename": "everflow-conversions-2026-01-15.csv",
    "row_count": 842,
    "results_url": "https://app.trafficdefenseiq.com/dashboard/reports/leads?upload=5b1f6e9e-2f5b-4e7a-9c7e-9b6f2b2b6a11"
  },
  "counts": {
    "low": 701,
    "medium": 118,
    "high": 23
  }
}

The request also carries an X-TDIQ-Signature header like t=1758200000,v1=<hex>. Recompute the HMAC over "<t>.<raw body>" with your signing secret, compare it to v1, and reject anything with a timestamp more than a few minutes old (replay protection). Postbacks below are plain GET requests and aren't signed.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody must be the exact request body string, before JSON parsing.
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && a.length === b.length && timingSafeEqual(a, b);
}

Python

import hmac, hashlib, time

# raw_body must be the exact request body, before JSON parsing.
def verify(raw_body: str, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(
        secret.encode(), (parts["t"] + "." + raw_body).encode(), hashlib.sha256
    ).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts.get("v1", ""))

Postbacks — High plan

A postback calls a URL once per lead, auto-syncing your review decisions back to your tracker — the way trackers like Everflow, CAKE, and HasOffers expect. Creating a postback destination needs the High plan; Basic and Medium can still create webhook destinations above. You write the URL as a template using macros, for example https://track.example.com/postback?tid={transaction_id}&risk={risk_level}&score={score}. Available macros: transaction_id, lead_id, upload_id, risk_level, score, flagged, email, phone, ip, payout, affiliate_id, affiliate_name, offer_id, offer_name, converted_at, event. You don't have to send every lead: add conditions to a postback and only leads that pass all of them are sent. You can filter on fraud score (for example, 80 or higher), risk level, whether a phone, email, IP, or transaction ID is present, payout, a specific affiliate or offer, and whether a particular detection rule flagged the lead. Use Edit on a destination to change its conditions later.

To identify a conversion, map your file's transaction or click ID column when you upload (we auto-detect common names like transaction_id, click_id, and tid). Leads with no transaction ID are still sent, with that value left empty.

Postback examples by tracker

Starting a postback destination from a preset below fills in a working template for that tracker automatically — this is the same template it uses.

Everflow — Replace YOUR-TRACKING-DOMAIN with your Everflow tracking domain. event_id=0 reverses only the base conversion event -- change it only if you understand that transaction_id alone reverses every event tied to that session (installs, upsells, and all). Everflow also supports an optional verification_token parameter if you've enabled one on your account.

URL template

https://YOUR-TRACKING-DOMAIN/event?status=rejected&transaction_id={transaction_id}&event_id=0

Filled in for a sample High-risk lead

https://YOUR-TRACKING-DOMAIN/event?status=rejected&transaction_id=TEST-TRANSACTION-123&event_id=0

TUNE / HasOffers — Replace YOUR_NETWORK_ID and YOUR_NETWORK_TOKEN with your TUNE network's own values. The token is a real API credential embedded directly in the URL -- treat this destination as sensitive, the same as a password.

URL template

https://YOUR_NETWORK_ID.api.hasoffers.com/Apiv3/json?NetworkToken=YOUR_NETWORK_TOKEN&Target=Conversion&Method=updateStatus&transaction_id={transaction_id}&data[status]=rejected

Filled in for a sample High-risk lead

https://YOUR_NETWORK_ID.api.hasoffers.com/Apiv3/json?NetworkToken=YOUR_NETWORK_TOKEN&Target=Conversion&Method=updateStatus&transaction_id=TEST-TRANSACTION-123&data[status]=rejected

Affise (needs adjustment before use) — Affise identifies a conversion by its own clickid, which TrafficDefenseIQ doesn't capture today -- this template won't work as-is until you map that in yourself. status=3 is Affise's numeric code for Declined.

URL template

https://YOUR-AFFISE-DOMAIN/postback?clickid=YOUR_CLICK_ID&action_id={transaction_id}&status=3

Filled in for a sample High-risk lead

https://YOUR-AFFISE-DOMAIN/postback?clickid=YOUR_CLICK_ID&action_id=TEST-TRANSACTION-123&status=3
CAKE: CAKE doesn't document a plain GET-postback for rejecting a conversion the way the other three do -- conversion status changes go through CAKE's dashboard or their API with an API key. Check with CAKE support for their current REST API details; there's nothing to prefill here.

Testing, retries, and activity

  • Send test fires one sample call so you can confirm your endpoint works before real uploads depend on it.
  • Calls are sent within moments of an upload finishing. A call counts as delivered when your server answers with a 2xx status; redirects aren't followed.
  • A failed call is retried automatically — after 1 minute, then 5 minutes, 30 minutes, and 2 hours — up to 5 attempts. Open a destination's Activity to see every call and its result, and use Retry now or Resend to send one again immediately.
  • Pause a destination to stop sending without deleting it.
For security, destinations must use public https URLs — addresses on private or internal networks are rejected.

Read-only API — High plan

Under Settings → API keys, an account owner on the High plan can create a read-only, account-scoped API key for pulling your own uploads and scored leads into your BI or CRM tools. The full key is shown once at creation — copy it then, since it isn't stored anywhere you can retrieve it again. Nothing in this API accepts writes; it only reads what's already in your account. Revoke a key any time from the same page.

Two endpoints, both GET, both authenticated with Authorization: Bearer <your key>, both paginated (page and page_size, up to 500 per page) and rate-limited to 600 requests an hour per key.

GET /api/v1/leads — filter by status, risk, or upload_id

curl "https://app.trafficdefenseiq.com/api/v1/leads?risk=high&page_size=50" \
  -H "Authorization: Bearer tdiq_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Response

{
  "leads": [
    {
      "id": "8f0a1b2c-3d4e-4f56-8a7b-9c0d1e2f3a4b",
      "email": "j.martinez88@gmail.com",
      "phone": "+13055550142",
      "affiliate_id": "20017",
      "affiliate_name": "Click 2 Convert",
      "offer_id": "881",
      "offer_name": "Home Warranty Quote",
      "ip_address": "184.51.211.9",
      "transaction_id": "TX-100201",
      "payout": 12.5,
      "converted_at": "2026-09-10T14:04:41.000Z",
      "status": "active",
      "review_state": "unreviewed",
      "fraud_verdict": null,
      "upload_id": "5b1f6e9e-2f5b-4e7a-9c7e-9b6f2b2b6a11",
      "score": 92,
      "risk_level": "high",
      "rule_results": {
        "duplicate_detection": {
          "score": 100,
          "flagged": true,
          "reason": "Same email or phone appears 4 times in this upload."
        }
      }
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 23
}

GET /api/v1/uploads

curl "https://app.trafficdefenseiq.com/api/v1/uploads?page_size=20" \
  -H "Authorization: Bearer tdiq_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Response

{
  "uploads": [
    {
      "id": "5b1f6e9e-2f5b-4e7a-9c7e-9b6f2b2b6a11",
      "filename": "everflow-conversions-2026-01-15.csv",
      "status": "completed",
      "row_count": 842,
      "flagged_count": 141,
      "created_at": "2026-01-15T09:02:11.000Z",
      "completed_at": "2026-01-15T09:04:37.000Z"
    }
  ],
  "page": 1,
  "page_size": 20,
  "total": 37
}

Didn't answer it? Contact us.