---
updatedAt: 2025-12-09T19:09:59.000Z
agentTools:
  projectIndex: https://docs.castle.io/llms.txt
---

# Protecting the transaction

> 🚧
>
> Make sure you've first [integrated your app](https://docs.castle.io/docs/integration-guide) in order to generate the "request tokens" required for each call to the Risk and Filter API.

The`$transaction` activity is sent to the [Risk API](https://reference.castle.io/#operation/risk) whenever a user makes a monetary transaction on your platform. The inline response can then be used to determine when to step up verification or to outright block the transaction.

The activity can be sent at different stages of the transaction, which is denoted by setting the `status` field:

* `$attempted` – The transaction was posted but hasn't yet been validated by the gateway
* `$succeeded` –  The transaction was accepted by the gateway
* `$failed` – The transaction was rejected by the gateway

> 📘
>
> You need to generate and forward the `request_token` string from your frontend by using the [Browser SDK](https://docs.castle.io/docs/sdk-browser) or a [Mobile SDK](https://docs.castle.io/docs/sdk-mobile).

```ruby
castle = ::Castle::Client.new

begin
  token = request.params['castle_request_token']
  context = Castle::Context::Prepare.call(request)

  res = castle.risk(
    type: '$transaction',
    status: '$succeeded',
    request_token: token,
    context: {
      ip: context[:ip],
      headers: context[:headers]
    },
    user: {
      id: 'ca1242f498', # Required. A unique, persistent user identifier
      email: 'mgray@example.com' # Recommended
      # See the registration or login guides for more attributes
    },
    transaction: {
      id: '32301', # Required. Your local, unique reference to the transaction
      type: '$purchase', # Required
      base_amount: '99.99', # Optional
      amount: { # Optional
        type: '$fiat', # Optional. Defaults to $fiat. Can also be $crypto
        value: '99.99', # Required
        currency: 'USD' # Required
      },
      payment_method: {
        type: "$card", # Required
        fingerprint: "Xt5EWLLDS7FJjR1c", # Unique cross-user ID
        holder_name: "John Smith",
        country_code: "US", # The origin of the card or bank account
        bank_name: "Capital One",
        card: {
          bin: "457173", # A string of 6 or 8 digits
          last4: "4242", # A string of 4 digits
          exp_month: 8,
          exp_year: 2022,
          network: "$visa",
          funding: "$credit",
        },
        billing_address: {
          line1: "150 Earl St",
          line2: "Suite 482",
          city: "San Francisco",
          postal_code: "94111",
          region_code: "CA",
          country_code: "US" # Required
        }
      },
      shipping_address: {
        line1: "150 Earl St",
        line2: "Suite 482",
        city: "San Francisco",
        postal_code: "94111",
        region_code: "CA",
        country_code: "US" # Required
      }
    }
  )

  if res[:policy][:action] == 'deny'
    # IMPLEMENT: Deny attempt
  end

rescue Castle::InvalidRequestTokenError
  # Deny attempt. Likely a bad actor bypassing fingerprinting
rescue Castle::Error => e
  # Allow attempt. Data missing or invalid, or a server or timeout error
end
```
```php
// NOTE: See the Ruby example for a more comprehensive set of parameters

try {
  $token = $_POST['castle_request_token'];

  $res = Castle::risk([
    'type' => '$transaction',
    'status' => '$succeeded',
    'request_token' => $token,
    'context' => [
      'ip' => Castle_RequestContext::extractIp(),
      'headers' => Castle_RequestContext::extractHeaders()
    ],
    'user' => [
      'id' => $user->id,
      'email' => $user->email
    ]
  ]);

  if ($res->risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }

} catch (Castle_InvalidRequestTokenError $e) {
  // Deny attempt. Likely a bad actor bypassing fingerprinting
} catch (Castle_Error $e) {
  // Allow attempt. Data missing or invalid, or a server or timeout error
}
```
```python
# NOTE: See the Ruby example for a more comprehensive set of parameters

try:
    token = request.form['castle_request_token'] # Using Flask
    context = ContextPrepare.call(request)
    client = Client()

    res = client.risk({
        'type': '$transaction',
        'status': '$succeeded',
        'request_token': token,
        'context': {
          'ip': context['ip'],
          'headers': context['headers']
        },
        'user': {
            'id': user.id,
            'email': user.email
        }
    })

    if res['risk'] > 0.9:
        # IMPLEMENT: Deny attempt

except InvalidRequestTokenError:
    # Deny attempt. Likely a bad actor bypassing fingerprinting
except CastleError as e:
    # Allow attempt. Data missing or invalid, or a server or timeout error
```
```java
// NOTE: See the Ruby example for a more comprehensive set of parameters

String token = request.getParameter("castle_request_token");

Castle castle = Castle.initialize();
CastleContextBuilder context = castle.contextBuilder().fromHttpServletRequest(request)

try {
  CastleResponse response = castle.client().risk(ImmutableMap.builder()
    .put("type", "$transaction")
    .put("status", "$succeeded")
    .put(Castle.KEY_CONTEXT, ImmutableMap.builder()
      .put(Castle.KEY_IP, context.getIp())
      .put(Castle.KEY_HEADERS, context.getHeaders())
      build()
    )
    .put(Castle.KEY_USER, ImmutableMap.builder()
      .put("id", user.getId())
      .put("email", user.getEmail())
      .build()
    )
    .put(Castle.KEY_REQUEST_TOKEN, token)
    .build()
  );
  
  float risk = response.json().getAsJsonObject().get("risk").getAsFloat();

  if (risk > 0.9) {
    // IMPLEMENT: Deny attempt
  };
  
} catch (CastleApiInvalidRequestTokenException requestTokenException) {
    // IMPLEMENT: Deny attempt. Likely a bad actor
} catch (CastleRuntimeException runtimeException) {
    // Data missing or invalid. Needs to be fixed
}
```
```javascript Node
// NOTE: See the Ruby example for a more comprehensive set of parameters

try {
  const token = request.body["castle_request_token"]; // Using Express

  const castle = new Castle({ apiSecret: 'YOUR SECRET HERE' });
  const context = ContextPrepareService.call(request, {}, castle.configuration);

  const res = castle.risk({
    type: '$transaction',
    status: '$succeeded',
    request_token: token,
    user: {
      id: user.id,
      email: user.email
    },
    context: {
      ip: context.ip,
      headers: context.headers
    }
  });

  if (res.risk > 0.9) {
    // IMPLEMENT: Deny attempt
  }
} catch (e) {
  if (e instanceof InvalidRequestTokenError) {
     // IMPLEMENT: Deny attempt. Likely a bad actor
  } else if (e instanceof APIError) {
     // Allow attempt. Data missing or invalid, or a server or timeout error
  }
}
```

### The `transaction` object

The `transaction` object is available exclusively for `$transaction` type events.

The `type` field can have one of the following values:

* `$purchase` – The user purchased physical or digital goods from you or from another user on your platform
* `$sale` – The user sold physical or digital goods to you or to another user on your platform
* `$withdrawal` – The user withdrew balance into a bank account or debit card
* `$deposit` – The user deposited balance into their account on your platform
* `$transfer` – The user transferred balance between accounts on your platform
* `$reward` – The user was rewarded by adding balance to their account

Note that `base_amount` and `value` are float values encapsulated in a string in order to maintain the precision regardless of transportation or storage.

The `currency` field needs to be sent in the [ISO 4217 format](https://sv.wikipedia.org/wiki/ISO_4217) if the type is `$fiat`, but can be any value for `$crypto`.

> 📘
>
> **Tip** Use the `properties` object to highlight more granular information about the profile update, for instance which specific user properties that were updated. This helps the risk analyst better understand the severity of a suspicious profile update.

### The `payment_method` object

#### `type`

The `type` field can be set to any of the following values:

`$card`, `$crypto_wallet`, `$sepa`, `$wire`, `$ach`, `$aba`, `$amazon_pay`, `$android_pay`, `$apple_pay`, `$google_pay`, `$samsung_pay`, `$paypal`, `$boleto`, `$blinc`, `$fps`, `$sen`, `$signet`, or `$other`.

#### `fingerprint`

The `fingerprint` field is generated by your backend can be of any format, but typically a hashed version of all the details that uniquely identifies the payment method across users. This can be used to find users linked via the same credit card or bank account.

#### `card`

The `card` object can be populated whenever you have access to the credit or debit card information. This would for example be for when the `type` is set to the more generic `$card` value, or when an e-wallet was used, e.g. `$google_pay` or `$apple_pay`.

#### `card.network`

The `card.network` field can bet set to any of the following values:

`$amex`, `$cartes_bancaires`, `$diners`, `$discover`, `$interac`, `$jcb`, `$mastercard`, `$unionpay`, `$visa`, or `$other`.

#### `card.funding`

The `card.funding` field be set to any of the following values:

`$credit`, `$debit`, `$prepaid`, or `$other`.

### The `merchant` object

Merchant object provides a way to capture detailed merchant information when monitoring card transactions. The *optional* `merchant` object consists of the following fields:

* `id` - merchant's unique identifier (*optional*)
* `name` - merchant's name (*optional*)
* `category`: (*optional*)
  * `code` - merchant's category code (mcc) in four-digit [ISO 18245](https://en.wikipedia.org/wiki/ISO_18245) format
  * `description` - merchant's category description. If not provided, then Castle will automatically fill it out based on the merchant's category code

This information can be used to discover high-risk MCC codes or an unusual category description, and build rules or blocklists for them.

## Taking action

The response from the API call to Risk can then be used to take different actions. Typically, for transaction, you'd want to ask the user to step up the authentication to a stronger factor if you're concerned about an account takeover, put the transaction into manual review, or when the risk is above 90 outright block the transaction.