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
- 1Sign in as a brand and open API & Sale Defend in the sidebar.
- 2Name the key (e.g.
Sale Defend) and click Create key. - 3Copy the key right away — it starts with
affn_live_and is shown only once. AFFN stores only a hash. - 4Paste 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_xxxxxxxxxxxxxxxxA 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.
| Filter | Format | Meaning |
|---|---|---|
status | approved · pending · rejected · reversed | Only that status |
created_after | ISO 8601 with timezone | On or after this time |
created_before | ISO 8601 with timezone | Strictly before this time |
min_risk_score | 0–100 | Sale Defend score at or above |
order_id | text | Exact order / reference |
click_id | UUID | Exact AFFN click |
limit | 1–500 (default 100) | Page size |
offset | 0+ (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))
done5. Reverse or reject conversions (clawbacks)
POST https://affn.io/api/public/v1/conversions/reverse with a JSON body:
order_idsand/orclick_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
updatedwith 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.
Testingand revoke it when done. - Start read-only: call
GET /conversions?limit=5and check the rows match your dashboard. - Create a test conversion first (Integration → Send test, or a postback with a made-up
order_idlikeTEST-001), then reverse only that order ID. - Never send bulk reversals until a single-ID test returns
updated: 1and 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
200success ·400invalid filters or body ·401bad key ·500server error (retry with backoff).
Sending conversions in? See Custom / server postbacks.
- 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