Skip to content

REST API, JSON over HTTPS, versions v1 and v2

Developer documentation

Authenticate, send a transaction, and read back a score, a recommended decision and the reasons for it. Every example on this page follows the API as it runs today.

Overview

There are two ways to score a transaction. Version 2 accepts ordinary transaction fields such as amount, currency, merchant and device, and is the one to use for a new integration. Version 1 accepts a 38-element feature vector that you compute yourself.

The API authenticates with JWT bearer tokens obtained by signing in. Opaque fsk_live_ keys are not accepted as bearer credentials on the scoring endpoints.

Authentication

Every call except GET /health and GET /info requires a JWT bearer token. Tokens expire after 8 hours.

  1. 1

    Sign in

    POST https://remorant.com/api/v1/auth/login with your email and password.

  2. 2

    Read the token

    Take the token string from the response JSON.

  3. 3

    Send it with each request

    Add the header Authorization: Bearer <token>.

Roles and permissions

RoleScoreLabelRead statisticsAdmin
platform_adminYesYesYesFull
tenant_adminYesYesYesTenant
analystYesYesYesNo
integrationYesYesNoNo
readonlyNoNoYesNo

For machine-to-machine integrations, use a user account with the integration role. Create one in the dashboard under Team.

Quickstart

Sign in, send a transaction, read the decision. Pick the contract you are integrating against.

Score a transactionbash

curl -X POST "$REMORANT_API_URL/api/v2/transactions/score" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction": {
      "transaction_id": "txn_9x8K2",
      "amount": 750.0,
      "currency": "USD",
      "transaction_time": "2026-09-02T03:15:00Z",
      "channel": "ecommerce",
      "merchant": {
        "id": "merchant_123",
        "category": "electronics",
        "country": "US"
      },
      "customer": { "id": "customer_456", "country": "US" },
      "device": { "ip_address": "203.0.113.10" }
    }
  }'

Response (abridged)json

{
  "request_id": "5de12db5-6dc0-477b-80bb-e0d13f06116e",
  "transaction_id": "txn_9x8K2",
  "fraud_probability": 0.734,
  "risk_score": 734,
  "risk_band": "high",
  "decision": "block",
  "recommended_action": "BLOCK",
  "decision_id": "dec_5de12db5-6dc0-477b-80bb-e0d13f06116e",
  "audit_reference": "prediction:5de12db5-6dc0-477b-80bb-e0d13f06116e",
  "reason_codes": [
    {
      "code": "MODEL_SCORE_ABOVE_THRESHOLD",
      "message": "Model fraud probability exceeded the active decision threshold.",
      "severity": "high",
      "evidence": { "fraud_probability": 0.734, "threshold": 0.5 }
    }
  ],
  "policy_rules_triggered": ["ACTIVE_THRESHOLD_EXCEEDED"],
  "threshold": 0.5,
  "threshold_source": "global"
}

Feature schema (v1)

For POST /api/v1/predict, the features array must contain exactly 38 numbers in the order below. The authoritative list is returned by GET /info.

V1 to V28 are anonymized risk signal components that come from your card-network data feed or preprocessing pipeline. The field mapping depends on your provider. If you do not have these, use the v2 endpoint.

Endpoints

MethodPathAccessDescription
POST/api/v1/auth/loginPublicExchange email and password for a JWT token (8 hour expiry)
POST/api/v2/transactions/scoreToken requiredScore a transaction from ordinary transaction fields. Body: { transaction: { ... } }
POST/api/v1/predictToken requiredScore a transaction from a feature vector. Body: { features: number[38], transaction_id?: string }
POST/api/v1/batchToken requiredBatch scoring of many transactions in one call
POST/api/v1/labelToken requiredSubmit a confirmed outcome: { request_id, actual_label: 0|1 }
GET/api/v1/statsToken requiredPrediction statistics for your tenant
GET/infoPublicModel metadata: version, feature_columns, threshold, metrics
GET/healthPublicLiveness probe
GET/api/v1/healthPublicService health: API, database, cache and model registry status
GET/readyPublicReadiness probe. The model and database must be operational

Rate limits

Limits are enforced per credential with sliding-window counters. Going over a limit returns HTTP 429 with a Retry-After header.

PlanPer minutePer hourPer day
Trial202001,000
Starter1002,00010,000
Growth50010,00050,000
Enterprise2,00040,000500,000

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Sandbox

Send test traffic to the sandbox API at https://sandbox-api.remorant.com. Sandbox calls return the same response contract as production and are kept out of production billing, analytics and drift monitoring. The production API rejects requests that ask for sandbox mode.

Sandbox scoring requestbash

curl -X POST https://sandbox-api.remorant.com/api/v1/predict \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "features": [0, -0.31, 1.19, 1.03, 0.99, 0.84, -0.11, 0.42, 0.16, 0.07, 0.06, -0.34, 0.34, -0.2, 0.21, 0.09, 0.42, 0.15, 0.01, -0.13, -0.04, -0.08, -0.15, 0.02, -0.05, -0.21, -0.07, 0.11, -0.05, 47.5, 14, 0.4, 3.87, 0, 0, 0.2, 0, 3.87],
    "transaction_id": "test_txn_dev_001",
    "is_sandbox": true
  }'

# Sandbox predictions:
#  Same response format as production
#  NOT saved to the database
#  NOT counted toward your billing quota
#  NOT included in analytics or drift monitoring
# Production rejects is_sandbox. Use the production base URL without this flag.

Errors

Errors return a JSON body. Handle them by the error code, because several codes share one HTTP status.

Error responsejson

{ "error": "MISSING_FEATURES", "message": "features must contain exactly 38 numeric values" }
StatusError codeCauseWhat to do
400MISSING_FEATURESfeatures array is absent or does not contain exactly 38 elementsValidate array length before sending
400INVALID_FORMATfeatures contains non-numeric values or request body is invalid JSONEnsure every element is a finite float
400INVALID_INPUTRequest body fails schema validationCheck field names and types against the API reference
401AUTHENTICATION_FAILEDJWT token is missing, malformed, or has expired (8 hour lifetime)Re-authenticate via POST /api/v1/auth/login and retry
401INVALID_CREDENTIALSEmail or password is incorrectVerify credentials: do not retry in a tight loop
403ACCOUNT_DISABLEDAccount has been suspendedContact hello@whizzonby.com
403INSUFFICIENT_PERMISSIONSToken role cannot perform this actionUse a token with the integration or analyst role
429RATE_LIMIT_EXCEEDEDSliding-window rate limit hit for your planRead Retry-After header and wait before retrying
503MODEL_NOT_AVAILABLEPrediction model is temporarily unavailableCheck GET /health: contact support if the outage persists
500PREDICTION_ERRORUnexpected internal error during scoringRetry with exponential backoff; open a support ticket if it persists

Webhooks

Remorant signs each webhook delivery with an HMAC-SHA256 signature in the X-Remorant-Signature header. Verify the signature before you process the payload.

EventSent when
transaction.fraud_detectedFraud probability exceeds 0.80
transaction.high_riskRisk level is high or critical
transaction.blockedRecommended action is BLOCK
drift.detectedFeature distribution drift detected
rate_limit.exceededTenant rate limit was hit

Subscribe to events in the dashboard under Settings, Webhooks.

Signature verification, Python (Flask)python

import hmac, hashlib, os
from flask import Flask, request

def verify_signature(payload_bytes: bytes, header_sig: str, secret: str) -> bool:
    """Return True if the webhook signature is valid."""
    expected = hmac.new(
        secret.encode(),
        payload_bytes,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, header_sig)

app = Flask(__name__)

@app.route("/remorant-webhook", methods=["POST"])
def handle_webhook():
    ok = verify_signature(
        request.get_data(),                              # raw bytes: before JSON parsing
        request.headers.get("X-Remorant-Signature", ""),
        os.environ["REMORANT_WEBHOOK_SECRET"],
    )
    if not ok:
        return {"error": "Invalid signature"}, 401

    event = request.json
    if event["event_type"] == "transaction.fraud_detected":
        handle_fraud_alert(event["data"])
    elif event["event_type"] == "transaction.blocked":
        handle_blocked(event["data"])
    elif event["event_type"] == "drift.detected":
        notify_ops_team(event["data"])

    return {"received": True}, 200

Signature verification, Node.js (Express)javascript

import crypto from 'crypto';
import express from 'express';

function verifySignature(rawBody, headerSig, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  // timingSafeEqual prevents timing-based attacks
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headerSig));
}

const app = express();

// Use express.raw() to preserve raw bytes for signature verification
app.post('/remorant-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-remorant-signature'] ?? '';
  if (!verifySignature(req.body, sig, process.env.REMORANT_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  switch (event.event_type) {
    case 'transaction.fraud_detected': handleFraudAlert(event.data); break;
    case 'transaction.blocked':        handleBlocked(event.data);    break;
    case 'drift.detected':             notifyOps(event.data);        break;
  }
  res.json({ received: true });
});
Read the raw request bytes before parsing JSON. A parser may reorder keys, which invalidates the signature. Use express.raw() in Node.js or request.get_data() in Flask.

Support

Playground
Try scoring requests in the browser and inspect the full response. Open the playground
Integration help
Questions about mapping your transaction fields or your network data feed. hello@whizzonby.com
SDKs
Python and JavaScript SDKs are in development and are not yet published. Use the REST API directly for now.