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.
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
Sign in
POST https://remorant.com/api/v1/auth/login with your email and password.
- 2
Read the token
Take the token string from the response JSON.
- 3
Send it with each request
Add the header Authorization: Bearer <token>.
Roles and permissions
| Role | Score | Label | Read statistics | Admin |
|---|---|---|---|---|
| platform_admin | Yes | Yes | Yes | Full |
| tenant_admin | Yes | Yes | Yes | Tenant |
| analyst | Yes | Yes | Yes | No |
| integration | Yes | Yes | No | No |
| readonly | No | No | Yes | No |
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.
Endpoints
| Method | Path | Access | Description |
|---|---|---|---|
| POST | /api/v1/auth/login | Public | Exchange email and password for a JWT token (8 hour expiry) |
| POST | /api/v2/transactions/score | Token required | Score a transaction from ordinary transaction fields. Body: { transaction: { ... } } |
| POST | /api/v1/predict | Token required | Score a transaction from a feature vector. Body: { features: number[38], transaction_id?: string } |
| POST | /api/v1/batch | Token required | Batch scoring of many transactions in one call |
| POST | /api/v1/label | Token required | Submit a confirmed outcome: { request_id, actual_label: 0|1 } |
| GET | /api/v1/stats | Token required | Prediction statistics for your tenant |
| GET | /info | Public | Model metadata: version, feature_columns, threshold, metrics |
| GET | /health | Public | Liveness probe |
| GET | /api/v1/health | Public | Service health: API, database, cache and model registry status |
| GET | /ready | Public | Readiness 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.
| Plan | Per minute | Per hour | Per day |
|---|---|---|---|
| Trial | 20 | 200 | 1,000 |
| Starter | 100 | 2,000 | 10,000 |
| Growth | 500 | 10,000 | 50,000 |
| Enterprise | 2,000 | 40,000 | 500,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" }| Status | Error code | Cause | What to do |
|---|---|---|---|
| 400 | MISSING_FEATURES | features array is absent or does not contain exactly 38 elements | Validate array length before sending |
| 400 | INVALID_FORMAT | features contains non-numeric values or request body is invalid JSON | Ensure every element is a finite float |
| 400 | INVALID_INPUT | Request body fails schema validation | Check field names and types against the API reference |
| 401 | AUTHENTICATION_FAILED | JWT token is missing, malformed, or has expired (8 hour lifetime) | Re-authenticate via POST /api/v1/auth/login and retry |
| 401 | INVALID_CREDENTIALS | Email or password is incorrect | Verify credentials: do not retry in a tight loop |
| 403 | ACCOUNT_DISABLED | Account has been suspended | Contact hello@whizzonby.com |
| 403 | INSUFFICIENT_PERMISSIONS | Token role cannot perform this action | Use a token with the integration or analyst role |
| 429 | RATE_LIMIT_EXCEEDED | Sliding-window rate limit hit for your plan | Read Retry-After header and wait before retrying |
| 503 | MODEL_NOT_AVAILABLE | Prediction model is temporarily unavailable | Check GET /health: contact support if the outage persists |
| 500 | PREDICTION_ERROR | Unexpected internal error during scoring | Retry 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.
| Event | Sent when |
|---|---|
| transaction.fraud_detected | Fraud probability exceeds 0.80 |
| transaction.high_risk | Risk level is high or critical |
| transaction.blocked | Recommended action is BLOCK |
| drift.detected | Feature distribution drift detected |
| rate_limit.exceeded | Tenant 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}, 200Signature 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 });
});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.