AFFNHelp centerSign in

Sale Defend & REST API

Pull your conversion reports and reverse fraudulent or refunded sales from Sale Defend, your store or any backend — no browser session needed.

1. Create an API key

  1. 1
    Sign in as a brand and open API & Sale Defend in the sidebar.
  2. 2
    Name the key (e.g. Sale Defend) and click Create key.
  3. 3
    Copy the key right away — it starts with affn_live_ and is shown only once. AFFN stores only a hash.
  4. 4
    Paste it into Sale Defend (or your server's secret store). Revoke it on the same page at any time; revoked keys stop working immediately.

2. Authentication

Send the key as a Bearer token on every request. Keep it on servers only — never in website code, apps or URLs.

Authorization: Bearer affn_live_xxxxxxxxxxxxxxxx

A key only sees and changes conversions on your own brand's offers. Missing, wrong or revoked keys get 401 {"error":"Invalid or missing API key"}.

3. Report conversions

GET https://affn.io/api/public/v1/conversions returns newest first.

FilterFormatMeaning
statusapproved · pending · rejected · reversedOnly that status
created_afterISO 8601 with timezoneOn or after this time
created_beforeISO 8601 with timezoneStrictly before this time
min_risk_score0–100Sale Defend score at or above
order_idtextExact order / reference
click_idUUIDExact AFFN click
limit1–500 (default 100)Page size
offset0+ (default 0)Rows to skip
curl "https://affn.io/api/public/v1/conversions?status=approved&min_risk_score=50&created_after=2026-01-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer $AFFN_API_KEY"

Example response:

{ "data": [{ "id": "9b1c…", "click_id": "4f2a…", "order_id": "1001", "sku": "TEE-01", "event": "sale", "status": "approved", "sale_amount": 80, "commission": 8, "risk_score": 72, "risk_flags": ["fast_conversion"], "reversal_reason": null, "reversed_at": null, "created_at": "2026-02-03T14:22:10Z", "offer_id": "…", "partner_id": "K7Q2M9XA", "clicked_at": "2026-02-03T14:21:58Z", "referrer": "https://blog.example.com/review", "sub1": "summer", "sub2": null, "sub3": null, "sub4": null, "sub5": null }], "limit": 100, "offset": 0 }

Bad filters return 400 with details naming each invalid field. Visitor IPs are never returned.

4. Pagination

Page with limit + offset. Keep requesting with offset += limit until data has fewer rows than limit. For stable syncs, fix a time window with created_before so new sales don't shift pages mid-sync.

offset=0 while :; do page=$(curl -s "https://affn.io/api/public/v1/conversions?created_after=2026-02-01T00:00:00Z&created_before=2026-02-02T00:00:00Z&limit=500&offset=$offset" \ -H "Authorization: Bearer $AFFN_API_KEY") count=$(echo "$page" | jq '.data | length') echo "$page" | jq -c '.data[]' [ "$count" -lt 500 ] && break offset=$((offset + 500)) done

5. Reverse or reject conversions (clawbacks)

POST https://affn.io/api/public/v1/conversions/reverse with a JSON body:

  • order_ids and/or click_ids — up to 1,000 each; at least one is required.
  • status — "reversed" (default; refunds, returns, cancellations) or "rejected" (fraud, policy breach).
  • reason — up to 300 characters, shown to the brand and partner (default "Reversed via API").
curl -X POST "https://affn.io/api/public/v1/conversions/reverse" \ -H "Authorization: Bearer $AFFN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"order_ids":["1001","1002"],"status":"rejected","reason":"Sale Defend: cookie stuffing"}'
{ "updated": 2, "status": "rejected", "conversions": [{ "id": "9b1c…", "click_id": "4f2a…", "order_id": "1001" }, …] }

How reversals behave:

  • Conversions are never deleted — status changes and the reason + timestamp are recorded, so history stays auditable.
  • Reversed or rejected sales drop out of Payouts Owed immediately. If already paid, the amount offsets the partner's future earnings.
  • Safe to retry: conversions already in the requested status are skipped, so repeat calls return updated: 0.
  • IDs that don't exist or belong to another brand are silently ignored — compare updated with what you sent.
  • Reversals can be undone only by the brand in the dashboard (Conversions → Approve), not over the API.

Partner compliance sync

Sale Defend can send what it knows about a partner — including behaviour seen on other networks — so it appears in your Conversions review panel. Partners are matched by their website domain. Records are private to your brand.

curl -X POST "https://affn.io/api/public/v1/partners/intel" \ -H "Authorization: Bearer $AFFN_API_KEY" -H "Content-Type: application/json" \ -d '{"partners":[{"domain":"apexmedia.com","status":"non_compliant","risk_score":82, "violations":[{"type":"coupon_hijack","label":"Toolbar injection on 3 programs"}], "networks":["Impact","CJ"],"note":"Flagged Oct 2026"}]}'

status is one of compliant, watch, non_compliant, unknown. Sending the same domain again updates it. GET https://affn.io/api/public/v1/partners/intel lists your records.

6. Safe testing

  • Create a separate key named e.g. Testing and revoke it when done.
  • Start read-only: call GET /conversions?limit=5 and check the rows match your dashboard.
  • Create a test conversion first (Integration → Send test, or a postback with a made-up order_id like TEST-001), then reverse only that order ID.
  • Never send bulk reversals until a single-ID test returns updated: 1 and appears under Conversions with your reason.
  • In Sale Defend, start with a high threshold (e.g. min_risk_score=80) and review flagged sales manually before turning on automatic clawbacks.
  • If a key leaks, revoke it right away and create a new one.

Status codes

  • 200 success · 400 invalid filters or body · 401 bad key · 500 server error (retry with backoff).

Sending conversions in? See Custom / server postbacks.

Test your setup
  • API key created and stored only on your server or in Sale Defend
  • GET /conversions returns your recent sales
  • A single test order reversed and shown under Conversions
  • Test key revoked