Developers
Quickstart.
Nine steps from an empty shop to a scored checkout. Credentials come first, because every later step is signed with them. The contract those steps implement is set out in the integration hub.
Figure 1
Credentials
Get the three secrets.
Credentials are issued per shop from the partner portal: a key ID, an API key secret for request signing, and the network salt for identifier hashing. Registration is where the Terms of Service and the Data Processing Agreement are accepted, and the three values are issued once the application has been reviewed and a subscription is active. What each value does, and what it must never be used for, is in the integration hub.
Put all three in your server environment. None of them may ever reach a browser, a build artefact or a client-side bundle.
HEATRAIL_API_BASE=https://api.heatrail.eu
HEATRAIL_KEY_ID=hk_9f4c1e08b7a2d631
HEATRAIL_API_SECRET=…64 hex characters…
HEATRAIL_NETWORK_SALT=…the network-wide hashing salt…The base URL above is the production host. Read it from configuration rather than hard-coding it: it is the one value that would otherwise have to change in every file at once.
Install
Take the SDK, or skip it.
On WooCommerce, stop reading here and go to the plugin page. The plugin bundles the SDK and does the whole of this page for you.
On a custom PHP checkout, use the SDK. It has no Composer dependencies, needs only ext-curl and ext-json, and runs on PHP 7.3 through 8.5. Extract the archive we send you and register a PSR-4 autoloader for it:
spl_autoload_register(function (string $class): void {
$prefix = 'Heatrail\\Sdk\\';
if (strncmp($class, $prefix, strlen($prefix)) !== 0) {
return;
}
$relative = substr($class, strlen($prefix));
$file = __DIR__ . '/vendor-sdk/' . str_replace('\\', '/', $relative) . '.php';
if (is_file($file)) {
require $file;
}
});In any other language you call the REST API directly. You then own the signing, the timeout budget and the fail-open behaviour, and the contract in the integration hub is what your client has to implement. Read it before you write the client.
Verify
Prove the credentials work before touching checkout.
GET /v1/health is signed like every other request, so a successful round trip proves your key ID, secret and clock are all correct. Do this first. A broken configuration found here costs a minute, and the same configuration found in checkout costs a day of silently degraded checks.
<?php
use Heatrail\Sdk\HeatrailClient;
$client = HeatrailClient::forShop(
getenv('HEATRAIL_KEY_ID'),
getenv('HEATRAIL_API_SECRET'),
getenv('HEATRAIL_API_BASE'),
['network_salt' => getenv('HEATRAIL_NETWORK_SALT')]
);
var_dump($client->health()); // ['status' => 'up']['status' => 'degraded'] means either the service is degraded or your request was not accepted. Health never returns 401, deliberately, so that it cannot be used to test whether a key is valid. If you get degraded here, check the key ID, the secret, and that your server clock is within five minutes of real time.
Hash
Hash the identifiers inside your own server.
hashEmail() and hashPhone() normalise the value and apply the stage-one HMAC with your network salt. They return null when there is no usable value, and null means omit the field. Do not send an empty string. Do not hash a fallback. The empty-hash rule explains what a shared dummy hash would do to the network.
$payload = ['order_ref' => 'ord-10241'];
$emailHash = $client->hashEmail($order->billingEmail());
if ($emailHash !== null) {
$payload['email_h'] = $emailHash;
}
// The country is your shop's base country, ISO 3166-1 alpha-2.
$phoneHash = $client->hashPhone($order->billingPhone(), 'EE');
if ($phoneHash !== null) {
$payload['phone_h'] = $phoneHash;
}The dependency-free SDK normalises Estonian numbers. A number it cannot parse returns null and the field is omitted rather than guessed.
Check
One call, where the order is created.
Add the client IP your server observed, which is required, and the coarse order context. Then call check() with a deterministic idempotency key, so that a replay of the same order can never be counted twice.
$payload['ip'] = $order->customerIp(); // required
$payload['context'] = [
'product_risk_class' => 'physical', // physical | digital | gift_card
'qty' => 3,
'amount_bucket' => '100-250',
'account_age_class' => 'existing', // new | existing
];
// Deterministic per order: same order, same key, on every retry.
$idempotencyKey = 'ord-10241-' . substr(
hash('sha256', 'ord-10241|' . getenv('HEATRAIL_KEY_ID')),
0,
16
);
$result = $client->check($payload, $idempotencyKey);check() never throws and is bounded at 800 ms. Anything that goes wrong returns the degraded shape, logs it, and queues the check for replay if you gave the client a queue. Your checkout continues either way.
Read
Read the response, and store it on the order.
{
"check_id": "3f7a1c02-5b4e-4a19-8c3d-9e0b6d2f4a71",
"score": 72,
"reasons": [
"Customer identifier matched a network-confirmed fraud trail, 2 shop(s) affected, 3 d ago",
"Same identifier seen in 3 shops within 24 h"
],
"trail_alias": "9c1d4e7b0a52f83e",
"test": false
}score- An integer from 0 to 100, produced by rules, not by a model. 70 and above is what Heatrail treats as worth alerting the shop about.
reasons- Human-readable evidence lines describing what fired, with counts and recency. They never contain an identifier, a hash, or another shop’s data.
trail_alias- An opaque per-shop handle for the matched trail, or
nullwhen the check matched nothing. The same trail shows a different alias to every shop, so it cannot be used to probe the network. degraded- Present and
trueonly on the SDK’s fail-open shape. Store it. Treat it as “we do not know”, never as “low risk”. testtruewhen the check was a simulated test-mode response. Test checks create no heat, touch no network graph and are never billed.
Persist the score, the reasons and the degraded flag against the order. Your operators will need to see what Heatrail said at the time, including the times when it said nothing at all.
Label
Send the outcome back.
A check that is never followed by an outcome teaches the network nothing. When an order turns out to be fraudulent, or a chargeback arrives, label it. Labelling is the one operation that throws on failure: a label is a human decision, and the SDK will never tell you a label was applied that the API did not confirm.
use Heatrail\Sdk\HeatrailLabelException;
try {
$confirmed = $client->label(
'ord-10241', // order_ref
'fraud', // fraud | chargeback | real
'anna.tamm', // who decided, for your own audit trail
'label-ord-10241-fraud', // idempotency key, deterministic
[
'reason_code' => 'stolen_card',
'evidence_type' => 'merchant_confirmation',
]
);
// ['ok' => true, 'incident_id' => '…']
} catch (HeatrailLabelException $e) {
// Show the operator that it failed. Never record it as applied.
}real creates no heat at all. It exists so a good customer is recorded as good and so you can correct a mistake. Only fraud and chargeback contribute a trace. A fraud label makes the buyer’s device and IP hot for every Pro shop at once. E-mail and phone carry full weight elsewhere only once they are verified or tied to that same device, and network heat needs two independently owned shops. An operator cannot write network heat.
Drain
Replay what failed open.
Give the client a queue store and every failed check is written to it instead of being lost. A background job replays them, probing health first so a recovering API is never hammered, re-signing each attempt with a fresh timestamp and nonce, and reusing the original idempotency key so a check that actually landed the first time is not counted twice.
use Heatrail\Sdk\Queue\FileQueueStore;
$queue = new FileQueueStore('/var/lib/heatrail/queue.jsonl');
$client = HeatrailClient::forShop($keyId, $secret, $baseUrl, [
'network_salt' => $salt,
'queue' => $queue,
]);
// From cron, every minute:
$drained = $client->drainQueue($queue);FileQueueStore is the reference implementation, single-host and unbounded. In production, back the same small QueueStore interface with your own table and add retention. The SDK page spells out exactly what that involves.
Test mode
See the red banner without waiting for real fraud.
Ask us to enable test mode for your shop. A checkout placed with the magic e-mail fraud-test@heatrail.eu or the magic phone number +372 55550666 then returns a simulated high score of 85, with reasons prefixed TEST MODE: and the test flag set.
Nothing else happens. A test check creates no heat, records no velocity, touches no network graph, sends no alert and is never billed. It behaves this way only while the flag is set on our side. Without it, those magic values are treated exactly like placeholders and discarded, so they cannot produce a false alert on a live shop by accident.