Heatrail

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

Three stages from an empty shop to a scored checkout, and the response the third one returns.Three boxes sit in a row, joined by arrows. The first is step one, credentials, listing key ID, API key secret and network salt, issued from the partner portal. The second is step two, install, listing the WooCommerce plugin, the PHP SDK or your own client, with a health call to prove the keys. The third is step three, the first check, which hashes in your server, posts to slash v1 slash check once per order and is bounded at 800 ms. An arrow drops from the third box into a panel showing a response with a score of 72 and two readable reasons, carrying no identifier, no hash and no other shop. To the left of that panel a note says that if no answer arrives the order goes on without a signal and the check is queued for replay.From an empty shop to a scored checkoutSTEP 1Credentialskey IDAPI key secretnetwork saltfrom the partner portalSTEP 2InstallWooCommerce pluginor the PHP SDKor your own clienthealth call proves the keysSTEP 3First checkhash in your serverPOST /v1/checkone call per orderbounded at 800 ms"score": 72"reasons": [ two readable lines ]no identifier, no hash, no other shopIf no answer arrivesthe order goes on without a signaland the check is queued for replay
Two steps of setup, then the call that runs on every order and the response it returns.
01

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.

02

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.

03

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.

04

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.

05

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.

06

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 null when 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 true only on the SDK’s fail-open shape. Store it. Treat it as “we do not know”, never as “low risk”.
test
true when 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.

07

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.

08

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.

09

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.

Heatrail

This site is not public yet

Heatrail is in a private pre-launch period. Enter the access password to continue.

Enquiries: hello@heatrail.eu