Protecting the transaction

🚧

For ideal performance, you should complete the guide on Protecting the login before you implement this guide.

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
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]' # Required
    },
    transaction: {
      id: '32301', # Required. Your local, unique reference to the transaction
      type: '$purchase', # Required
      amount: { # Required
        type: '$fiat', # Optional. Defaults to $fiat. Can also be $crypto
        value: '99.99', # Required
        currency: 'USD' # Required
      }
    }
  )

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

rescue Castle::Error => e
  # Handle 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_Error $e) {
  // Handle 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 CastleError as e:
     # Handle 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(Castle.KEY_USER_ID, user.getId())
      .put(Castle.KEY_EMAIL, user.getEmail())
      .put("username", user.getUsername())
      .build()
    )
    .put(Castle.KEY_REQUEST_TOKEN, token)
    .build()
  );
} catch (CastleRuntimeException runtimeException) {
    // Handle errors
}

float risk = response.json()
   .getAsJsonObject()
   .get("risk")
   .getAsFloat();

if (risk > 0.9) {
  // IMPLEMENT: Deny attempt
};
// 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) {
  console.error(e);
}

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

Note that the value is a float value 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.

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.


Did this page help you?