## Early Fraud Warnings (EFWs)

Early Fraud Warnings (EFWs) are real-time alerts issued by card networks when a cardholder reports potential fraud on a transaction. They arrive **before** a formal dispute (chargeback) is filed, giving you a window to proactively refund a suspicious charge and potentially avoid a chargeback entirely.

---

## Table of Contents

- [Overview](https://docs.withflex.com/early-fraud-warnings#overview)
- [How EFWs Differ from Disputes](https://docs.withflex.com/early-fraud-warnings#how-efws-differ-from-disputes)
- [The EarlyFraudWarning Object](https://docs.withflex.com/early-fraud-warnings#the-earlyfraudwarning-object)
- [Fraud Type Reference](https://docs.withflex.com/early-fraud-warnings#fraud-type-reference)
- [Webhook Events](https://docs.withflex.com/early-fraud-warnings#webhook-events)
- [Recommended Response Strategy](https://docs.withflex.com/early-fraud-warnings#recommended-response-strategy)
- [End-to-End Testing Guide](https://docs.withflex.com/early-fraud-warnings#end-to-end-testing-guide)

---

## Overview

```
Card issuer flags transaction as potentially fraudulent
       │
       ▼
Card network fires radar.early_fraud_warning.created
       │
       ▼
Flex receives event → resolves charge → creates EFW record
       │
       ▼
Partner receives webhook event: radar.early_fraud_warning.created
       │
       ├─── actionable: true  → Consider proactive refund to prevent chargeback
       └─── actionable: false → Warning is informational only (already refunded or disputed)
```

**There is no REST API to list or retrieve EFWs.** All EFW data is delivered exclusively via webhook events. Subscribe to `radar.early_fraud_warning.created` and `radar.early_fraud_warning.updated` in your webhook configuration.

### Key facts

- EFWs are **read-only** — Flex does not expose an API to query or acknowledge them.
- An EFW does **not** mean a dispute has been filed. It is a signal that the cardholder may have flagged the transaction.
- A single charge may receive multiple EFWs (one created, then updated as circumstances change).
- `actionable: true` means the charge has not yet been refunded or disputed — act now.
- `actionable: false` means the charge has already been fully refunded or a dispute has been filed. No action needed.

---

## How EFWs Differ from Disputes

|  | Early Fraud Warning | Dispute (Chargeback) |
| --- | --- | --- |
| **Trigger** | Card issuer files fraud report with card network | Cardholder formally disputes with their bank |
| **Impact on funds** | No immediate fund withdrawal | Funds withdrawn immediately |
| **Fee** | No fee | Dispute fee charged |
| **Flex API** | Webhook only | Full REST API |
| **Recommended action** | Consider proactive refund | Submit evidence or accept loss |
| **Time pressure** | Respond before a formal dispute is filed | Evidence deadline (`due_by`) typically 7–21 days |

---

## The EarlyFraudWarning Object

This is the object delivered inside every `radar.early_fraud_warning.*` webhook event.

```
{
  "early_fraud_warning_id": "fefw_01jx3q8kt1b9a7n2c4d5e6f7g8",
  "charge_id": "fch_01jx3q6ab1c7d2e3f4g5h6j7k8",
  "payment_intent_id": "fpi_01jx3q7ab1c8d2e3f4g5h6j7k8",
  "actionable": true,
  "fraud_type": "card_never_received",
  "client_reference_id": "order_12345",
  "test_mode": false,
  "created_at": "2026-02-15T10:20:00Z"
}
```

### Field Reference

| Field | Type | Description |
| --- | --- | --- |
| `early_fraud_warning_id` | string | Flex EFW ID (`fefw_` prefix). |
| `charge_id` | string  null | Flex charge ID (`fch_`) for the flagged transaction. |
| `payment_intent_id` | string  null | Flex payment intent ID (`fpi_`) linked to the charge. |
| `actionable` | boolean | `true` if the charge is still refundable and no dispute has been filed. `false` once refunded or disputed. |
| `fraud_type` | string | Fraud category from the card issuer. |
| `client_reference_id` | string  null | The `client_reference_id` you passed when creating the checkout session, if any. |
| `test_mode` | boolean | `true` for test-mode transactions. |
| `created_at` | string | ISO 8601 timestamp when Flex received the EFW. |

---

## Fraud Type Reference

| Value | Description |
| --- | --- |
| `card_never_received` | Physical card was never received by the cardholder (e.g. intercepted in mail). |
| `fraudulent_card_application` | Card was obtained through a fraudulent application. |
| `made_with_counterfeit_card` | Physical counterfeit card was used. |
| `made_with_lost_card` | Lost card was used without authorisation. |
| `made_with_stolen_card` | Stolen card was used without authorisation. |
| `unauthorized_use_of_card` | Card was used without the cardholder’s authorisation (general). |
| `misc` | Other / unclassified fraud type. |

`unauthorized_use_of_card` and `card_never_received` are the most common in ecommerce.

---

## Webhook Events

### `radar.early_fraud_warning.created`

Fired when Flex receives a new early fraud warning for one of your charges.

```
{
  "event_id": "fevt_01jx3q8kt1b9a7n2c4d5e6f7g8",
  "event_type": "radar.early_fraud_warning.created",
  "event_dt": 1770000000,
  "object": {
    "early_fraud_warning_id": "fefw_01jx3q8kt1b9a7n2c4d5e6f7g8",
    "charge_id": "fch_01jx3q6ab1c7d2e3f4g5h6j7k8",
    "payment_intent_id": "fpi_01jx3q7ab1c8d2e3f4g5h6j7k8",
    "actionable": true,
    "fraud_type": "card_never_received",
    "client_reference_id": "order_12345",
    "test_mode": false,
    "created_at": "2026-02-15T10:20:00Z"
  }
}
```

### `radar.early_fraud_warning.updated`

Fired when an existing EFW changes — most commonly when `actionable` transitions from `true` to `false` (because the charge was refunded or a formal dispute was filed).

```
{
  "event_id": "fevt_01jx3q9ab1c2d3e4f5g6h7j8k9",
  "event_type": "radar.early_fraud_warning.updated",
  "event_dt": 1770003600,
  "object": {
    "early_fraud_warning_id": "fefw_01jx3q8kt1b9a7n2c4d5e6f7g8",
    "charge_id": "fch_01jx3q6ab1c7d2e3f4g5h6j7k8",
    "payment_intent_id": "fpi_01jx3q7ab1c8d2e3f4g5h6j7k8",
    "actionable": false,
    "fraud_type": "card_never_received",
    "client_reference_id": "order_12345",
    "test_mode": false,
    "created_at": "2026-02-15T10:20:00Z"
  }
}
```

### Webhook signature verification

All Flex webhooks include a `flex-signature` header. Verify it using your webhook signing secret from the Flex dashboard. See the Webhooks documentation for signature verification details.

### Idempotency

EFWs are upserted on `early_fraud_warning_id` — receiving the same event twice will not create duplicate records. Your webhook handler should be idempotent: processing the same `event_id` twice should have no side-effects.

---

## Recommended Response Strategy

When you receive `radar.early_fraud_warning.created` with `actionable: true`:

### 1. Assess the risk

Check the `fraud_type` and correlate the `charge_id` or `client_reference_id` to the order in your system.

- Is this order already shipped? Shipped orders are harder to recover.
- Is the `fraud_type` high-risk (`card_never_received`, `made_with_stolen_card`)? These have a higher probability of becoming a formal chargeback.

### 2. Decide: refund or wait

| Scenario | Recommendation |
| --- | --- |
| Order not yet shipped | Cancel and refund immediately. |
| Order shipped, high-value, high-risk fraud type | Proactive refund may still be cheaper than losing a dispute + fee. |
| Order shipped, low-value | Weigh refund cost vs dispute fee (~$15) + chargeback rate impact. |
| `actionable: false` | No action needed — already resolved. |

### 3. Issue a refund (if decided)

Use the Flex Refunds API to issue a full or partial refund for the `charge_id` in the EFW. Once refunded, the EFW will be updated to `actionable: false` and you will receive a `radar.early_fraud_warning.updated` event.

### 4. Track your chargeback rate

A high chargeback rate (typically >1%) can result in penalties from card networks. EFWs are an early signal to detect patterns — monitor `fraud_type` across EFWs to identify systemic fraud patterns in your customer base.

---

## End-to-End Testing Guide

> **Note**: EFW events cannot be triggered via test card payments alone — card networks do not issue EFWs in response to standard test transactions. Use the synthetic webhook method below to simulate the full flow end-to-end against your local Flex server.

### Prerequisites

- Flex API key and webhook signing secret (from your Flex dashboard)
- Your webhook endpoint URL (must be reachable — use [ngrok](https://ngrok.com/) for local development)
- Python 3 or any HMAC-SHA256 capable language

### Step 1 — Register a webhook endpoint

In your Flex dashboard, register your endpoint URL and subscribe to:

- `radar.early_fraud_warning.created`
- `radar.early_fraud_warning.updated`

### Step 2 — Create a charge

Create a checkout session and complete a payment. Note the `charge_id` (`fch_...`) and `payment_intent_id` (`fpi_...`) from the checkout session response. Optionally include a `client_reference_id` in your checkout session request to verify it is propagated to EFW events:

```
POST /v1/checkout/sessions
Authorization: Bearer fsk_...
Content-Type: application/json

{
  "checkout_session": {
    "line_items": [{ "price": "fprice_...", "quantity": 1 }],
    "client_reference_id": "order_12345",
    "mode": "payment",
    "success_url": "https://example.com/success",
    "cancel_url": "https://example.com/cancel"
  }
}
```

### Step 3 — Send a synthetic EFW event

Use the script below to generate a correctly signed event and POST it to your local Flex server. Flex will process it and forward the resulting `radar.early_fraud_warning.created` event to your registered webhook endpoint.

```
import hmac
import hashlib
import json
import time
import urllib.request

# Your webhook signing secret (from your .env or Flex dashboard)
WEBHOOK_SECRET = "whsec_your_signing_secret_here"
FLEX_WEBHOOK_URL = "http://localhost:8000/v1/webhooks/stripe"

CHARGE_ID = "ch_..."   # The underlying charge ID — see note below
PI_ID = "pi_..."       # The underlying payment intent ID — see note below

timestamp = int(time.time())

payload = {
    "id": "evt_synthetic_efw_001",
    "object": "event",
    "api_version": "2023-10-16",
    "created": timestamp,
    "livemode": False,
    "pending_webhooks": 1,
    "request": {"id": None, "idempotency_key": None},
    "type": "radar.early_fraud_warning.created",
    "data": {
        "object": {
            "id": "efw_synthetic_001",
            "object": "radar.early_fraud_warning",
            "actionable": True,
            "charge": CHARGE_ID,
            "created": timestamp,
            "fraud_type": "card_never_received",
            "livemode": False,
            "payment_intent": PI_ID
        }
    }
}

body = json.dumps(payload, separators=(',', ':'))
signed_payload = f"{timestamp}.{body}"
mac = hmac.new(
    WEBHOOK_SECRET.encode('utf-8'),
    signed_payload.encode('utf-8'),
    hashlib.sha256
)
sig_header = f"t={timestamp},v1={mac.hexdigest()}"

req = urllib.request.Request(
    FLEX_WEBHOOK_URL,
    data=body.encode('utf-8'),
    headers={
        'Content-Type': 'application/json',
        'Stripe-Signature': sig_header,  # required header name for webhook signature
    },
    method='POST'
)

with urllib.request.urlopen(req) as resp:
    print(f"Status: {resp.status}")  # expect 200
```

> **Where to find the charge and payment intent IDs**: After completing a payment in the Flex checkout, look at the `checkout_session` response — the `payment_intent` field contains the payment intent ID (`pi_...`). The corresponding charge ID (`ch_...`) can be retrieved by fetching the payment intent from the Flex API.

### Step 4 — Verify the event arrives at your endpoint

Your webhook handler should receive:

```
{
  "event_id": "fevt_...",
  "event_type": "radar.early_fraud_warning.created",
  "event_dt": 1770000000,
  "object": {
    "early_fraud_warning_id": "fefw_...",
    "charge_id": "fch_...",
    "payment_intent_id": "fpi_...",
    "actionable": true,
    "fraud_type": "card_never_received",
    "client_reference_id": "order_12345",
    "test_mode": false,
    "created_at": "2026-02-15T10:20:00Z"
  }
}
```

Verify:

- `early_fraud_warning_id` has `fefw_` prefix (Flex ID)
- `charge_id` and `payment_intent_id` are Flex IDs (`fch_`, `fpi_`)
- `client_reference_id` matches what you set on the checkout session (if applicable)
- `actionable: true`

### Step 5 — Test the updated event

Re-send with `actionable: false` and `type: "radar.early_fraud_warning.updated"`, keeping the same `id` in `data.object`:

```
payload["type"] = "radar.early_fraud_warning.updated"
payload["data"]["object"]["actionable"] = False
```

Verify:

- Your endpoint receives `radar.early_fraud_warning.updated`
- The same `early_fraud_warning_id` is in the event (upserted, not duplicated)
- `actionable` is now `false`

### Step 6 — Test idempotency

Resend the same `radar.early_fraud_warning.created` event (same `id` in `data.object` — e.g. `efw_synthetic_001` — with a different event timestamp). Verify your handler processes it without creating a duplicate record.

### Step 7 — Verify `client_reference_id` propagation

If you created the checkout session with a `client_reference_id`:

- Confirm it appears in the `object` of the EFW event
- This lets you correlate the EFW directly to your internal order without any additional lookups

---

## Checklist

| Scenario | Expected outcome |
| --- | --- |
| `created` event received and parsed | `fefw_` ID in `object`, all fields populated |
| `client_reference_id` set on checkout session | Appears in EFW event `object.client_reference_id` |
| `updated` event changes `actionable: false` | Same `fefw_` ID, `actionable` updated |
| Duplicate `created` event (same EFW ID) | No duplicate — idempotent upsert |
| Missing signature header | `400 Bad Request` from Flex |
| Wrong signing secret | `400 Bad Request` from Flex |
| EFW referencing a charge not in Flex | Event accepted (200), EFW not persisted, error logged |
