# Integration Modes

Pick how much of the payment you want us to run. The mode lives on your API key,
so it is decided once at key creation. No request field can change it, not for you and
not for anyone who gets hold of your key.

Before you ship: [Integration hard rules](./27-integration-hard-rules.md) (IP, 3DS,
idempotency, token, device signals).

| | **Atomic** | **Fraud only** | **Payment only** |
|---|---|---|---|
| `POST /v1/check` | yes | yes | no (`mode_not_allowed`) |
| `POST /v1/charges` | yes | no (`mode_not_allowed`) | yes |
| Who moves the money | us | your own acquirer | us |
| Fraud engine at charge time | full | n/a | full |
| Decision token issued | yes | yes | — |
| Decision token accepted | yes | n/a | yes |
| Typical integrator | most | already has a PSP | rare |

---

## Atomic: one call

Send the buyer's details once; we screen, decide, step up to 3DS if the risk
calls for it, and charge.

```bash
curl -X POST https://api.worthypayments.com/v1/charges \
  -H "X-Api-Key: sk_live_…" \
  -H "Idempotency-Key: order_1234" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4999,
    "currency": "usd",
    "card": { "number": "ev:…", "cvv": "ev:…", "exp_month": "12", "exp_year": "2030" },
    "customer": { "email": "buyer@example.com" },
    "device": { "fingerprint": "fp_9a1c2b" }
  }'
```

Three outcomes. Branch your UI **only** on these; full JSON shapes are in
[Charges](./03-charges.md#responses).

| `status` / HTTP | What to do |
|---|---|
| `succeeded` | Done. Show the receipt. |
| `requires_action` | Run the 3DS challenge, then resubmit the same charge with `three_ds.session_id` added and nothing else changed. See [3-D Secure](./07-3ds.md). The Payment Element drives this itself (the challenge opens inside the Element's frame, which grows to fit it), so a browser integration never sees this status. |
| `{ "error": … }` (HTTP 402) | Declined. Show a generic decline, never the reason. |
| 400 / other error | Fix the request (missing `Idempotency-Key`, bad token, validation). See [Errors](./10-errors.md). |

This is the right default. One request, one decision, nothing to keep in sync.

**Client IP on charge:** not a body field. A charge built on your server comes from your
egress address, which we record as your server's and never score or blocklist, so it cannot
get your backend blocked. To score the charge on the buyer's IP and browser, send the
[`device_session`](./24-device-data-collection.md#device-session-the-buyers-ip-and-browser)
the buyer's browser created. The [Payment Element](./08b-payment-element.md) confirms from
the browser, so there the peer is the buyer. Details:
[Charges → Client IP](./03-charges.md#client-ip-fraud-signal-not-a-body-field).

---

## Two-step: you own the decision UI

Same Atomic key, two calls. Use this when you want to show your own interstitial
before charging: your own decline page, your own "verifying your card" screen,
your own review queue.

**1. Evaluate**

```bash
curl -X POST https://api.worthypayments.com/v1/check \
  -H "X-Api-Key: sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@example.com",
    "fingerprint": "fp_9a1c2b",
    "card_bin": "424242",
    "last4": "4242",
    "amount": 4999,
    "currency": "usd",
    "ip": "203.0.113.42"
  }'
```

> **`ip` is the shopper’s address, not yours.** When this call is made from your server,
> omit it and we score your egress IP instead, which is wrong. See [Fraud checks → Client IP](./23-fraud-checks.md#client-ip-effectively-required).

```json
{
  "object": "fraud_check",
  "status": "clean",
  "action": "allow",
  "requires_3ds": true,
  "reasons": [],
  "risk_score": 12,
  "check_id": "2a5aed3a-…",
  "risk_decision_token": "cmR0MXxtYV8x….9f8e7d…"
}
```

**2. Decide**

| `action` | Meaning |
|---|---|
| `deny` | Stop. Do not charge. |
| `review` | Charge, but flag it for your ops team. |
| `allow` | Proceed. If `requires_3ds` is true, expect a challenge. |

**3. Charge**, echoing the token:

```jsonc
{
  "amount": 4999,
  "currency": "usd",
  "card": { "…": "…" },
  "device": { "fingerprint": "fp_9a1c2b" },
  "risk_decision_token": "cmR0MXxtYV8x….9f8e7d…"   // optional, strictly checked
}
```

See [Risk Decision Token](./26-decision-token.md) for what the token binds and
which errors it can produce.

> **The charge is not "pre-approved" by the check.** The full engine runs again
> at charge time, on the data the charge actually carries. That is deliberate:
> blocklists, velocity counters and rules change in the minutes between the two
> calls, and the charge is the moment the money moves.

---

## Fraud only: decision without the money

For merchants who already have an acquirer and want our screening. `/v1/check`
and `/v1/check/recheck` work; `/v1/charges` returns `403 mode_not_allowed` on
every request, so a leaked key cannot move money.

The token is still issued. It is your audit artefact: proof of what we said and
when, for a chargeback file.

---

## Payment only: charge without the advisory surface

`/v1/charges` works, `/v1/check` returns `403`. Note this does **not** mean
"skip fraud". The engine runs on every charge in every mode. It means you do not
get the standalone pre-checkout evaluation.

---

## Choosing

Start with **Atomic**. Move to two-step when you need control over what the buyer
sees between decision and payment. Choose **Fraud only** when the money already
goes somewhere else.

Ask support to change your mode; it is a property of the key, so in practice you
are issued a new one.
