Protecting the transaction
Make sure you've first integrated your app 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
You 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
transaction
objectThe 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
payment_method
objecttype
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
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
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
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
card.funding
The card.funding
field be set to any of the following values:
$credit
, $debit
, $prepaid
, or $other
.
The merchant
object
merchant
objectMerchant 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 formatdescription
- 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.
Updated 5 months ago