API

Everything Spark exposes over HTTP.

A generated reference lives at /api/docs when the server is running. This page adds what a generated reference cannot: why each endpoint exists and what its answers mean.

Two ways in

Prefix For Authentication
/api/... The dashboard A session cookie, or nothing at all
/api/v1/... Your servers Authorization: Bearer sk_test_...

Both call the same engine, so what a developer sees in the sandbox is what their server gets.

Scoring one transaction

curl -X POST https://spark.spacesdrive.cc/api/v1/risk/score \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_000123",
    "amount": 4.5,
    "customer_id": "cust_8813",
    "merchant_id": "merch_204",
    "location": "IN-KA",
    "payment_type": "upi"
  }'

Request

Field Required Meaning
amount yes The transaction value. Must not be negative
customer_id yes Who paid
merchant_id yes Who was paid
transaction_id no Your reference. Generated if omitted
location no Recorded as unknown if omitted
payment_type no Recorded as unknown if omitted
mode no balanced, high_precision or high_recall. Default balanced
explain no Include the explanation. Default true

There is no field for a device fingerprint, an IP address or a card number. The data the model was trained on has none of them, so accepting them would mean collecting information the model cannot use.

Response

{
  "transaction_id": "txn_000123",
  "risk_score": 0.2143,
  "risk_band": "HIGH",
  "decision": "BLOCK",
  "mode": "balanced",
  "model_id": "hybrid-v1",
  "model_version": "spark-hybrid-v1",
  "path": "MODEL",
  "review_threshold": 0.0771,
  "block_threshold": 0.1402,
  "channel_scores": {
    "tabular": 0.0392,
    "graph": 0.8789,
    "behavioral": 0.4252,
    "velocity": 0.0603
  },
  "channel_attribution": { "tabular": 0.02, "graph": 0.96, "behavioral": 0.0, "velocity": 0.02 },
  "reasons": [
    {
      "text": "account has 2 prior transactions",
      "direction": "increases",
      "contribution": 0.571,
      "feature": "Source_txn_count"
    }
  ],
  "entity_history": {
    "Source": { "role": "customer account", "prior_transactions": 2, "is_new": false }
  },
  "graph_evidence": {
    "Target": [
      {
        "transaction_id": "txn_071557",
        "time": 71557,
        "amount": 7.0,
        "source": "S38165",
        "target": "T1822",
        "relation": "Same merchant",
        "outcome": "fraud"
      }
    ]
  },
  "related_ring": null,
  "stages": [{ "name": "Building features", "ms": 2.02 }],
  "latency_ms": 20.6,
  "notes": []
}

Fields worth explaining:

risk_score is calibrated, so it is meant to track the real chance of fraud rather than just being a ranking.

path is MODEL or COLD_START. COLD_START means the customer, the merchant and the channel all had almost no history, so the score was raised to a minimum instead of trusting a confident model output on no evidence.

channel_attribution is weight times score, normalised. It says what moved the final number, which is not always what moved the tree model.

reasons come from SHAP over the tree model. They are worded as contribution, never as cause, because a declined customer may challenge the decision.

graph_evidence lists real earlier transactions. Every one is older than the transaction being scored, because the graph only builds backward edges. An empty object means nothing earlier shared any detail, which is normal for a new customer.

related_ring is not ring membership. The ring detector runs over completed time windows, so a transaction that has just arrived was in none of them. What this says is that the merchant or channel appears in a group already flagged.

stages are the steps the server actually ran, with real timings.

notes explain what a missing optional field did.

Endpoints

Public

Method Path What it does
GET /api/health Whether the API, the model and the database are working
GET /api/config What the browser needs to start. No private credentials
POST /api/risk/score Score one transaction. Rate limited, no key
GET /api/risk/thresholds The three settings, and whether each still works
GET /api/models Models available to the caller
GET /api/models/{id} One model, with its weights and thresholds
GET /api/metrics/overview Measured results, each labelled with its split
GET /api/metrics/charts The same numbers shaped for charts
GET /api/metrics/limitations What those numbers do not cover
GET /api/metrics/rings Abuse-ring detection results
GET /api/datasets/format Which columns Spark needs
GET /api/datasets/example Details of the built-in example dataset
POST /api/datasets/upload Upload a CSV, get the validation result
GET /api/datasets/{id} One dataset you uploaded
GET /api/datasets/{id}/preview The first rows, as the server parsed them
POST /api/datasets/{id}/validate Re-check, optionally with your own mapping
POST /api/datasets/score Queue scoring. Returns a job
DELETE /api/datasets/{id} Delete an upload now
GET /api/jobs/{id} How far a job has got
GET /api/jobs/{id}/result The finished result, rows paged
GET /api/jobs/{id}/download The scored rows as CSV
GET /api/training/limits The training limits this server enforces

Signed in

Method Path What it does
POST /api/auth/session Exchange a Supabase token for a session cookie
POST /api/auth/logout End the session on the server
GET /api/auth/me Who the caller is. Guests get authenticated: false
GET /api/organizations Organizations you belong to
POST /api/organizations Create one. You become its owner
GET /api/organizations/{id} One organization, with members and stage
GET /api/organizations/{id}/api-keys Your keys, masked
POST /api/organizations/{id}/api-keys Create a key. Secret returned once
POST /api/api-keys/{id}/rotate New secret, old one stops immediately
POST /api/api-keys/{id}/revoke Turn a key off permanently
GET /api/organizations/{id}/usage What your keys have been doing
GET /api/organizations/{id}/datasets Datasets owned by that organization
GET /api/jobs?organization_id= Jobs for one organization
POST /api/models/{id}/activate Use a custom model
POST /api/models/{id}/deactivate Stop using it
POST /api/training/jobs Runs the real checks, then returns 501

With an API key

Method Path What it does
POST /api/v1/risk/score Score one transaction, recorded against your usage

Jobs

Scoring a large file takes longer than a request should, so it becomes a job.

JOB=$(curl -s -X POST http://localhost:8000/api/datasets/score \
  -H "Content-Type: application/json" \
  -d '{"dataset_id":"ds_...","mode":"balanced"}' | jq -r .id)

curl -s "http://localhost:8000/api/jobs/$JOB"
{
  "id": "job_...",
  "status": "running",
  "stage": "Running the graph model",
  "progress": 0.62,
  "elapsed_seconds": 3.4
}

status moves through queued, running, then succeeded or failed. stage is whatever the work reported. When a step cannot say how far through it is, the stage changes and the number stays where it was. A bar that climbs on a timer while the work has stalled is worse than no bar.

A failed job carries the reason in error.

Errors

Every error body has a message written for a person and a reason your code can switch on.

{
  "detail": {
    "message": "The amount column contains text that is not a number.",
    "reason": "validation_failed",
    "fix": "Remove currency symbols and thousands separators."
  }
}
Status Means
400 Understood but cannot be done. The body says why
401 No key, an invalid key, or a revoked one
403 Signed in but not allowed
404 Not found, or not yours. Spark does not say which
409 The job has not finished
410 The file expired and was deleted
422 A field was the wrong shape. The body lists which
429 Too many requests. The body says how long to wait
500 Something broke. Details go to the server log, not to you
501 Not built yet
503 No model loaded, or the evaluation has not been run

Stack traces are never returned.

Limits

Set through environment variables, and reported by GET /api/config so the numbers shown in the dashboard are the ones being enforced.

Limit Default
Upload size 25 MB
Rows in a test dataset 100,000
Rows in a training dataset 200,000
Jobs running at once 2
Jobs per organization per day 20
Uploads kept for 24 hours
Scoring requests per minute 120
Uploads per minute 10

Rate limiting is a fixed window held in memory. That is right for a single API process, and it is honest about its limits: it does not survive a restart and it does not coordinate between processes. Running more than one process means moving it to Redis.

Security