> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinmarkt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Merchant anti-fraud

> Read and update anti-fraud settings and list the fraud audit log with a Bearer API key.

All routes require:

```txt theme={null}
Authorization: Bearer mk_live_<secret>
```

`requireMerchantAuth` reads `Authorization` and accepts only `Bearer` plus a token that starts with `mk_live_`. A missing header, a non-Bearer scheme, any other prefix, an unknown key, a revoked key, or a completed account deletion is `401` with `error` `Unauthorized` and `code` `UNAUTHORIZED`. An expired key is `401` with `error` `API key expired` and the same code.

The key must be allowed to access `{storeId}`: the key owner can access that shop, and a key limited to selected shops must list it. The key owner must have `manageBlacklist` on that shop. A restricted key must include the scope named on the method. `antifraud:write` also satisfies `antifraud:read`. A full-access key skips the scope list and still needs `manageBlacklist`. A failed shop, scope, or permission check is `403` with `error` `Forbidden` and `code` `FORBIDDEN`.

`storeId` in the JSON body is ignored. The shop is the `{storeId}` in the path.

Merchant routes share one limit of 120 requests per 60 seconds per client IP. Over the limit the response is `429` with `error` `Too many requests` and `code` `RATE_LIMITED`.

## Get settings

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/antifraud
```

Scope: `antifraud:read`

Status `200`. A shop the key cannot access, including a shop that does not exist, is `403` `Forbidden` from `requireMerchantAuth`. `404` with `error` `Store not found` and `code` `NOT_FOUND` is returned only when the store row is missing after those access checks pass.

```json theme={null}
{
  "blockVpnProxy": false,
  "emailValidation": "disabled",
  "allowedEmailDomains": [],
  "autoBlockOnChargeback": false,
  "kickBlockedFromDiscord": false,
  "discordGuildId": null
}
```

| Field | Values |
| - | - |
| `blockVpnProxy` | Boolean. `true` only when the stored value is strictly `true`; anything else is `false` |
| `emailValidation` | `disabled`, `block_disposable`, or `allowlist_domains`. Any other stored value is `disabled` |
| `allowedEmailDomains` | String array. Entries are trimmed, lowercased, and leading `@` characters are removed. Non-strings are dropped. A missing or non-array value is `[]` |
| `autoBlockOnChargeback` | Boolean. `true` only when the stored value is strictly `true` |
| `kickBlockedFromDiscord` | Boolean. `true` only when the stored value is strictly `true` |
| `discordGuildId` | Trimmed string, or `null` when missing, blank, or not a string |

These are the only fields returned. A missing or non-object stored config comes back as the defaults above (`blockVpnProxy` false, `emailValidation` `disabled`, empty domains, `autoBlockOnChargeback` false, `kickBlockedFromDiscord` false, `discordGuildId` null). A stored object still returns all six fields, with those defaults for missing or invalid values.

## Update settings

```http theme={null}
PATCH /api/merchant/v1/stores/{storeId}/antifraud
PUT   /api/merchant/v1/stores/{storeId}/antifraud
```

Scope: `antifraud:write`

`PUT` runs the same handler as `PATCH`. Status `200`. The response is the saved settings object, same fields as GET. A store row missing after the access checks is `404` `Store not found`, same as GET.

Send one or more of the fields below. Unknown keys are dropped. A body with no recognized fields returns `400` with `error` `No updatable anti-fraud fields provided` and `code` `BAD_REQUEST`. `Invalid JSON` and `JSON body must be an object` use the same code. Field errors are the Zod issues joined with `; `.

| Field | Rule |
| - | - |
| `blockVpnProxy` | Boolean. `true` requires Premium or Business |
| `emailValidation` | `disabled`, `block_disposable`, or `allowlist_domains` |
| `allowedEmailDomains` | Array, max 200 strings, each max 253. Saved trimmed, lowercased, leading `@` characters removed, blanks removed |
| `autoBlockOnChargeback` | Boolean |
| `kickBlockedFromDiscord` | Boolean |
| `discordGuildId` | String max 32, `""`, or null. Empty or whitespace becomes null |

Omitted fields stay as they are. `blockVpnProxy: true` when the plan has no VPN and TOR detection returns `403` with `error` `VPN & TOR detection requires Premium or Business. Upgrade in Billing.`, `code` `plan_vpn_tor`, `requiredPlan` `premium`, and `upgradePath` `/billing`. Setting it to `false`, or omitting it, does not hit that check. No other anti-fraud field is plan-gated.

```json theme={null}
{
  "blockVpnProxy": true,
  "emailValidation": "allowlist_domains",
  "allowedEmailDomains": ["shop.test"],
  "autoBlockOnChargeback": true,
  "kickBlockedFromDiscord": false,
  "discordGuildId": "123456789012345678"
}
```

## Audit log

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/antifraud/audit-log
```

Scope: `antifraud:read`

Query:

| Param | Rule |
| - | - |
| `category` | Optional string, max 64. Exact match. Omitted, or `all`, returns every category |
| `search` | Optional string, max 200. Case-insensitive contains on `subject`, `detail`, or `action` |
| `range` | `today`, `week`, `month`, or `all`. Omitted and `all` do not filter by time. `today` is since local midnight on the server. `week` is the last 7 days. `month` is the last 30 days |
| `limit` | Integer 1–200. Omitted uses `100` |

There is no `page` parameter. A `page` query is ignored. Rows are newest `createdAt` first. This route does not look up the store row: a shop the key cannot access is `403`, and an accessible shop with no matching rows returns `{ "logs": [] }`.

Categories this product records: `blacklist_rule`, `vpn_proxy`, `customer_blocked`, `ip_velocity`, `email_velocity`, `email_domain`, `chargeback`, `settings`. The filter matches the stored `category` string.

```json theme={null}
{
  "logs": [
    {
      "id": "cm...",
      "storeId": "cm...",
      "category": "settings",
      "action": "updated",
      "subject": null,
      "subjectType": null,
      "detail": "Anti-fraud settings saved",
      "createdAt": "2026-10-25T12:00:00.000Z"
    }
  ]
}
```

`subject`, `subjectType`, and `detail` are strings or null. `createdAt` is an ISO timestamp. `metadata` is not returned.

Invalid query values return `400`.

## Errors

| Status | Meaning |
| - | - |
| 400 | `Invalid JSON`, `JSON body must be an object`, `No updatable anti-fraud fields provided`, or Zod issues joined with `; `. `code` is `BAD_REQUEST` |
| 401 | `Unauthorized` or `API key expired`. `code` is `UNAUTHORIZED` |
| 403 | `Forbidden` (`code` `FORBIDDEN`), or `plan_vpn_tor` with `requiredPlan` `premium` and `upgradePath` `/billing` |
| 404 | `Store not found` (`code` `NOT_FOUND`) on GET, PATCH, and PUT settings. The audit log does not return 404 |
| 429 | `Too many requests` (`code` `RATE_LIMITED`) |
| 500 | `Failed to load anti-fraud settings`, `Failed to update anti-fraud settings`, or `Failed to load fraud audit log`. `code` is `INTERNAL_ERROR` and the body includes `errorId` |

Error JSON is `{ "success": false, "error": "...", "code": "..." }`. The VPN plan gate also returns `requiredPlan` and `upgradePath`. A 500 also returns `errorId`.

## Related

* [Merchant blacklist](https://docs.joinmarkt.com/api-reference/merchant-blacklist.md)
* [API keys](https://docs.joinmarkt.com/developers/api-keys.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.