# Test Compliance Risk Checks

Exercising risk screening is hard to arrange on demand. For inbound payments you would normally need access to a wallet that already carries a risk score. In both directions it is not obvious which addresses correspond to which risk categories. To let you build and test your compliance handling against every outcome, Tesser provides two testing mechanisms in **sandbox**:

- **Outbound**: send a payout to one of the public [test destination addresses](#outbound-test-destination-addresses) below. Each address corresponds to a different risk severity.
- **Inbound**: call [`POST /v1/payments/simulate-inbound`](#test-inbound-risk-checks) and choose the `mocked_risk_status` you want the screening verdict forced to.

With both mechanisms you get the same [`payment.risk_updated`](/webhooks/payment-updates) webhook, `risk_status` transitions, and [manual-review flow](#review-a-payment-awaiting-a-decision) as in production. See [Compliance and Risk Management](/overviews/compliance-and-risk) for how screening works and the [Funds Movement Lifecycle and Data Model](/overviews/funds-movement-lifecycle-and-data-model#risk-statuses) for the full `risk_status` taxonomy.

:::note{title="Sandbox only"}
These are testing tools for your sandbox integration. `POST /v1/payments/simulate-inbound` is blocked in production (returns [`payments-3053`](/overviews/errors)). The outbound test addresses are public on-chain addresses that Tesser does not control. Use them as payout beneficiaries in sandbox and never send meaningful value to them.
:::

## Test outbound risk checks

Outbound screening targets the **beneficiary wallet**, the on-chain address behind `desired.to.account_id`. To trigger a given outcome, create a payout whose destination account's `crypto_wallet_address` is one of the test addresses. A payout does not execute until risk is approved (see [Payout risk review](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout#payout-risk-review)).

### Outbound test destination addresses

These are EVM addresses and can be used on **Ethereum Sepolia, Base Sepolia, and Polygon Amoy**. In sandbox, use one as the beneficiary of a stablecoin payout.

<table className="wrap-first-col">
  <thead>
    <tr>
      <th>Severity → <code>risk_status</code></th>
      <th>Destination address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Medium → <code>automatically_approved</code></td>
      <td><code>0xd3f72b3e91c8124dfc1b4273e93594bb00c9be6f</code></td>
    </tr>
    <tr>
      <td>High → <code>awaiting_decision</code></td>
      <td><code>0x0a5b2bf3ccfb44c1d22f07eed9553ecba752d4ad</code></td>
    </tr>
    <tr>
      <td>Severe → <code>automatically_rejected</code></td>
      <td><code>0xac4cc4b68ea24bbfaac8fd127b67ed445accce22</code></td>
    </tr>
  </tbody>
</table>

### Steps

1. **Create a beneficiary account at a test address.** Create a [counterparty](/how-tos/create-a-counterparty) and a stablecoin [account](/how-tos/create-an-account) whose `crypto_wallet_address` is the test address for the severity you want to exercise. Payouts to this account screen to the corresponding outcome.
2. **Create a payout to that account.** Follow [Create a Payout](/how-tos/send-a-stablecoin-payout/create-a-stablecoin-payout) and set `desired.to.account_id` to the beneficiary account. A minimal request:

   ```json
   {
     "funding_account_id": "2113b166-5873-42fd-85fe-5b1a02940d43",
     "desired": {
       "from": {
         "account_id": "6de8a7e9-be79-4885-9b65-b25b11d38078",
         "amount": "1",
         "currency": "USDC",
         "network": "ETHEREUM"
       },
       "to": {
         "account_id": "53b7aabd-97bc-4a4f-9f9c-1c1c69474889",
         "currency": "USDC",
         "network": "ETHEREUM"
       }
     }
   }
   ```

3. **Observe the verdict.** Once `desired.to.account_id` is set, Tesser screens the beneficiary and emits a [`payment.risk_updated`](/webhooks/payment-updates) webhook carrying the mapped `risk_status`:

   ```json
   {
     "risk_status": "automatically_rejected",
     "risk_status_reasons": [
       {
         "risk_status_reason_type": "OWNERSHIP",
         "risk_status_reason_category": "Sanctions",
         "risk_status_reason_message": "Address has OWNERSHIP activity for Sanctions",
         "risk_status_reason_severity": "Severe",
         "risk_status_reason_participant_id": null,
         "risk_status_reason_significance": "DECISIVE",
         "volume_usd_percent": "100.00",
         "volume_usd_percent_threshold": "1",
         "volume_usd_amount": "56051.10",
         "volume_usd_amount_threshold": null,
         "confidence_score": null,
         "confidence_score_threshold": null
       }
     ]
   }
   ```

## Test inbound risk checks

[`POST /v1/payments/simulate-inbound`](/api/payments#simulate-an-inbound-payment-with-a-chosen-risk-status-non-production-only) sends a real, tiny test-USDC transfer from a Tesser-controlled faucet wallet into one of your managed wallets. It then forces the screening verdict to the `mocked_risk_status` you choose. The inbound itself is genuine. It is detected on-chain through the normal path, and balances, lifecycle, and webhooks all behave exactly as they would for a real inbound. Only the screening verdict is fabricated. It carries risk indicators that match your `mocked_risk_status`, so `risk_status_reasons` is populated the same way as in production. See [Receive an Inbound Payment to a Wallet](/how-tos/receive-an-inbound-payment-to-a-wallet) for the flow this exercises.

### Prerequisites

- A **managed `stablecoin_ethereum` wallet** with an on-chain address to receive the inbound (`to_account`). Provision one via [Create an Account](/how-tos/create-an-account). An account that exists but is not an eligible managed wallet returns [`payments-3055`](/overviews/errors).

### Request

| Field | Required | Description |
| --- | --- | --- |
| `to_account` | Yes | Managed account ID that should receive the simulated inbound. |
| `mocked_risk_status` | Yes | The `risk_status` the screening verdict is forced to: `automatically_approved`, `awaiting_decision`, or `automatically_rejected`. |
| `network` | No | EVM testnet to send on: `BASE_SEPOLIA` (default), `ETHEREUM_SEPOLIA`, or `POLYGON_AMOY`. Any other network returns [`payments-3054`](/overviews/errors). |

The transfer is a fixed `1` USDC per call. The API returns the actual value.

```bash
curl -X POST https://sandbox.tesserx.co/v1/payments/simulate-inbound \
  -H "Authorization: Bearer your-access-token" \
  -H "Content-Type: application/json" \
  -d '{
    "to_account": "7a3d8f2e-6b4c-4a91-b8e5-2f9c1d7e3a0b",
    "mocked_risk_status": "awaiting_decision",
    "network": "BASE_SEPOLIA"
  }'
```

The `mocked_risk_status` you send is exactly the `risk_status` the resulting inbound screens to. If you choose `awaiting_decision`, record a decision afterward (see [Review a payment awaiting a decision](#review-a-payment-awaiting-a-decision)).

### Response

Once the inbound payment is detected and screened, the endpoint returns the full payment with `risk_status` set to your `mocked_risk_status`:

```json
{
  "data": {
    "id": "9f3b7e1c-4a2d-4f69-b5a8-0c6e2d9f1a3b",
    "direction": "inbound",
    "risk_status": "awaiting_decision",
    "risk_status_reasons": [
      {
        "risk_status_reason_type": "OWNERSHIP",
        "risk_status_reason_category": "High-Risk Exchange",
        "risk_status_reason_message": "Address has OWNERSHIP activity for High-Risk Exchange",
        "risk_status_reason_severity": "High",
        "risk_status_reason_participant_id": null,
        "risk_status_reason_significance": "DECISIVE",
        "volume_usd_percent": "100.00",
        "volume_usd_percent_threshold": "1",
        "volume_usd_amount": "56051.10",
        "volume_usd_amount_threshold": null,
        "confidence_score": null,
        "confidence_score_threshold": null
      }
    ],
    "balance_status": null
  }
}
```

If the on-chain transfer is broadcast but not yet detected and screened within the request's wait budget, the endpoint returns `202 Accepted` instead, with the transaction hash and (once known) the `payment_id`. Poll [`GET /v1/payments/{paymentId}`](/api/payments#get-a-payment) for the screened payment:

```json
{
  "data": {
    "tx_hash": "0xabcdef0123",
    "network": "BASE_SEPOLIA",
    "status": "pending",
    "payment_id": null
  }
}
```

Inbound risk is advisory and does not gate the credit (see [Spending the Inbound Funds](/how-tos/receive-an-inbound-payment-to-a-wallet#spending-the-inbound-funds)).

## Review a payment awaiting a decision

Whenever a payment lands in `awaiting_decision`, record a decision through the [risk review decision API](/api/payments#submit-risk-review) or in the Tesser dashboard:

```bash
curl -X POST https://sandbox.tesserx.co/v1/payments/{paymentId}/review \
  -H "Authorization: Bearer your-access-token" \
  -H "Content-Type: application/json" \
  -d '{
    "is_approved": true
  }'
```

After the decision is recorded, `risk_reviewed_by` and `risk_reviewed_at` are populated. A `payment.risk_updated` webhook fires with `risk_status` transitioned to `manually_approved` (when `is_approved` is `true`) or `manually_rejected` (when `false`).
