Protecting the transaction

🚧

Make sure you've first integrated the client-side integration 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 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

:notebook-with-decorative-cover: You will need to generate and forward the request_token string from your frontend by using the Browser SDK or a Mobile SDK.

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: '[email protected]' # 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
// 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
}
# 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
// 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
}
// 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 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 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.