Fraud Intelligence Service — API Contracts
Version: 1.0 Status: Draft Owner: Trust and Safety Last Updated: 2026-04-21 Companion: SYNC_CONTRACT · APPLICATION_LOGIC · SECURITY_MODEL
The fraud-intel-service exposes three interface planes:
- gRPC on
:50054(mTLS required) — invoked bycompliance-engine,routing-engine,sender-id-registry-servicefor synchronous fraud scoring. Hot path. - HTTPS REST on
:3014— fronted by Kong; JWT + RBAC — foradmin-dashboard(case management, model lifecycle, feed admin), NOC operators, and Trust & Safety analysts. - HTTPS internal on
:3015— mTLS-only, no Kong — forregulator-portal-serviceand peer-MNO MISP feed import.
All three planes share the same domain aggregates but serve different actors.
1. gRPC service — FraudIntelService.v1
Full .proto lives in SYNC_CONTRACT §3. Reproduced here for reference with field-level stability notes.
syntax = "proto3";
package ghasi.sms.fraud.v1;
option go_package = "github.com/ghasi/sms-gateway/fraud/v1";
service FraudIntelService {
// Hot path. P95 ≤ 50 ms. mTLS required. Caller SPIFFE ID allowlisted.
rpc Score(ScoreRequest) returns (ScoreResponse);
// Server-streaming bulk score. Max 1000 entries per request. P50 throughput >= 5K scores/sec.
rpc BulkScore(BulkScoreRequest) returns (stream ScoreResponse);
// Read recent fraud signals for a subject. Used by NOC dashboards.
rpc GetSignals(GetSignalsRequest) returns (GetSignalsResponse);
}
enum ScoreScope {
SCORE_SCOPE_UNSPECIFIED = 0;
TENANT = 1;
SENDER_ID = 2;
MSISDN = 3;
PEER_ASN = 4;
}
enum FraudTier {
FRAUD_TIER_UNSPECIFIED = 0;
SAFE = 1;
WATCH = 2;
RISKY = 3;
HIGH_RISK = 4;
PROBATION = 5; // No data — neutral default
}
message ScoreRequest {
ScoreScope scope = 1;
string id = 2; // tenant UUID, sender_id (≤11 char), E.164 MSISDN, or ASN
string trace_id = 3;
}
message ScoreResponse {
string subject_id = 1;
ScoreScope scope = 2;
float score = 3; // 0.0..1.0
FraudTier tier = 4;
repeated ContributingFactor contributing_factors = 5;
string model_id = 6;
string model_version = 7;
google.protobuf.Timestamp computed_at = 8;
int32 stale_seconds = 9; // 0 if fresh; >0 if served from cache during refresh
string trace_id = 10;
}
message ContributingFactor {
string category = 1; // FraudCategory enum string
float weight = 2; // 0.0..1.0
string detection_id = 3; // FK if a detection drove this factor
}
message BulkScoreRequest {
repeated ScoreRequest entries = 1;
string trace_id = 2;
}
message GetSignalsRequest {
ScoreScope scope = 1;
string id = 2;
google.protobuf.Timestamp since = 3;
int32 limit = 4;
}
message GetSignalsResponse {
repeated FraudSignal signals = 1;
string next_cursor = 2;
}
message FraudSignal {
string signal_id = 1;
google.protobuf.Timestamp event_ts = 2;
string source_stream = 3;
string category = 4;
google.protobuf.Struct evidence = 5; // redacted
}
Error mapping
| gRPC status | Condition | Caller behaviour |
|---|---|---|
OK | Score returned | Use score in tenant tier or routing decision |
INVALID_ARGUMENT | Bad scope/id format | Caller logs, omits fraud factor |
PERMISSION_DENIED | Caller SPIFFE not allowlisted | Caller logs SOC alert |
NOT_FOUND | Used only by GetSignals for unknown subject | n/a for Score (PROBATION returned instead) |
RESOURCE_EXHAUSTED | BulkScore batch > 1000 | Caller chunks |
UNAVAILABLE, DEADLINE_EXCEEDED | Service down or slow | Treat as PROBATION (fail-closed-with-default) |
INTERNAL | Internal error | Same as UNAVAILABLE |
2. REST — admin and operational surface
Base path: /v1/fraud. All endpoints fronted by Kong (jwt plugin + rate-limiting-advanced). All responses JSON. State-changing endpoints idempotent via Idempotency-Key header where noted.
2.1 Cases (Trust & Safety analyst workflow)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/cases | tns-fraud-analyst, noc-operator, platform.auditor | List cases (filters: status, category, subjectScope, assignedTo, cursor) |
| GET | /v1/fraud/cases/{caseId} | same | Single case with full evidence (feature vector, SHAP, source events, MISP joins) |
| POST | /v1/fraud/cases/{caseId}/assign | tns-fraud-analyst-lead | Body: { assigneeUserId } |
| POST | /v1/fraud/cases/{caseId}/decide | tns-fraud-analyst | Body: { decision, reason, executeAction? }. Idempotent on caseId. |
| POST | /v1/fraud/cases/bulk-decide | tns-fraud-analyst-lead | Bulk action filtered by (category, subjectScope, openedBefore). Cap 1000. |
| GET | /v1/fraud/cases/metrics | tns-fraud-analyst, noc-operator | Queue depth, oldest pending, decision rate, auto-stale rate |
List response envelope:
{
"items": [
{
"caseId": "fc_01H...",
"category": "AIT",
"subjectScope": "TENANT",
"subjectId": "tnt_abc",
"score": 0.78,
"suggestedAction": "THROTTLE_TENANT",
"status": "PENDING_REVIEW",
"openedAt": "2026-04-20T10:00:00Z",
"openedBy": "system:auto",
"modelVersion": "ait-xgboost-2.1.4",
"evidenceSummary": "5min window submit_count=42100 unique_dst=39822 dlr_success=0.18"
}
],
"nextCursor": "eyJ...",
"total": 173
}
Decide body:
{
"decision": "CONFIRM_FRAUD",
"reason": "Confirmed via DLR analysis — 99.8% non-existent recipients in /28 block 0093790-0093795",
"executeAction": true
}
Decide errors:
| HTTP | Code | When |
|---|---|---|
| 400 | FRAUD_DECISION_REASON_TOO_SHORT | reason < 20 chars |
| 403 | FRAUD_SEPARATION_OF_DUTIES_VIOLATED | case.openedBy = decided_by |
| 409 | FRAUD_CASE_ALREADY_DECIDED | Case not in PENDING_REVIEW or IN_REVIEW |
| 410 | FRAUD_CASE_STALE | Case auto-closed (>30d) |
2.2 Detections browsing
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/detections | tns-fraud-analyst, noc-operator, platform.auditor | Search detections (filters: category, subjectScope, subjectId, since, confidenceTier) |
| GET | /v1/fraud/detections/{detectionId} | same | Single detection with full evidence + SHAP |
| GET | /v1/fraud/detections/{detectionId}/related-events | same | Source signal IDs that contributed |
2.3 Signals (raw evidence)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/signals | tns-fraud-analyst, noc-operator | Search recent signals (last 7d hot, older via async report) |
| GET | /v1/fraud/signals/by-subject | same | Filter by subject (tenant/sender-id/msisdn) |
2.4 Score lookup (REST mirror of gRPC)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/score?scope=TENANT&id={tenantId} | tns-fraud-analyst, noc-operator, platform.auditor | Same shape as gRPC Score for human inspection |
| GET | /v1/fraud/score/{scope}/{id}/history | same | Time series of scores (last 90 days) |
2.5 Model registry & lifecycle
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/admin/fraud/models | tns-ds, platform.compliance.admin | List models per (category, pipeline) |
| GET | /v1/admin/fraud/models/{modelId} | same | Detail incl. evaluation metrics, training-set hash, model-card URL |
| GET | /v1/admin/fraud/models/{modelId}/versions | same | All versions for a model |
| POST | /v1/admin/fraud/models | tns-ds | Register new ModelVersion: { modelId, version, artifactUri, artifactSha256, trainingSetHash, featureSetHash, evaluationMetrics, modelCardUri }. Status REGISTERED. |
| POST | /v1/admin/fraud/models/{versionId}/shadow | tns-ds | Promote to SHADOW. Idempotent. |
| POST | /v1/admin/fraud/models/{versionId}/promote | tns-ds (+ secondary approver platform.compliance.admin) | Atomic swap to ACTIVE. 412 if shadow < 24h or evaluation worse. 422 if SHA mismatch. |
| POST | /v1/admin/fraud/models/{versionId}/rollback | tns-ds | Re-activate previous ACTIVE. < 60s. |
| GET | /v1/admin/fraud/models/{versionId}/evaluation | tns-ds, platform.auditor | Full eval report (per-tenant fairness incl.) |
| POST | /v1/admin/fraud/training-runs | tns-ds | Trigger ad-hoc training run (Airflow DAG kick) |
Promote 412 response body:
{
"error": {
"code": "FRAUD_SHADOW_EVAL_INSUFFICIENT",
"message": "Shadow evaluation has run for 18h; minimum 24h required",
"details": {
"shadowStartedAt": "2026-04-20T08:00:00Z",
"elapsedHours": 18,
"requiredHours": 24
},
"traceId": "00-abc-…"
}
}
2.6 Feed admin (MISP/STIX)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/admin/fraud/feeds | tns-fraud-analyst-lead, platform.compliance.admin | List feeds (import/export) |
| GET | /v1/admin/fraud/feeds/{feedId} | same | Detail incl. last sync, indicator counts, decay profile |
| POST | /v1/admin/fraud/feeds | platform.compliance.admin | Register a feed |
| PUT | /v1/admin/fraud/feeds/{feedId} | same | Update (key rotation, schedule change) |
| POST | /v1/admin/fraud/feeds/{feedId}/sync-now | tns-fraud-analyst-lead | Trigger ad-hoc export or import run |
| GET | /v1/admin/fraud/feeds/{feedId}/indicators | tns-fraud-analyst, tns-fraud-analyst-lead | Browse indicators (paginated) |
| POST | /v1/admin/fraud/feeds/exports/run | platform.compliance.admin | Manual export trigger; returns { outputRef, sha256, signature, presignedUrls } |
2.7 Retroactive scan
| Method | Path | Role | Purpose |
|---|---|---|---|
| POST | /v1/admin/fraud/scans | tns-fraud-analyst-lead, platform.compliance.admin | Trigger retroactive scan over historical window: { scope, subjectId?, windowStart, windowEnd, categories[] }. Returns job receipt; result via /v1/admin/fraud/scans/{scanId} |
| GET | /v1/admin/fraud/scans/{scanId} | same | Status, progress, summary |
| GET | /v1/admin/fraud/scans/{scanId}/detections | same | Paginated detections produced by the scan |
2.8 Allowlists
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/admin/fraud/allowlists | tns-fraud-analyst-lead, platform.compliance.admin | List allowlists by scope |
| POST | /v1/admin/fraud/allowlists/{scope}/entries | same | Add (Bank/Gov sender-IDs, emergency-CBC cohorts, etc.) — secondary approver required |
| DELETE | /v1/admin/fraud/allowlists/{scope}/entries/{value} | same | Remove |
2.9 Dashboard query (NOC)
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/dashboard/summary | noc-operator, tns-fraud-analyst | Top-level: detections last 24h, open cases, model freshness, feed sync lag |
| GET | /v1/fraud/dashboard/topology | noc-operator | Per-MNO / per-peer-ASN heatmap |
| GET | /v1/fraud/dashboard/tenants/ranked | tns-fraud-analyst, noc-operator | Tenants sorted by fraud score |
2.10 Audit log query
| Method | Path | Role | Purpose |
|---|---|---|---|
| GET | /v1/fraud/audit-log | platform.auditor, tns-fraud-analyst-lead | Filter by entityType, entityId, actorUserId, date range. Cursor pagination. Read-only. |
2.11 Internal / operational
| Method | Path | Caller | Purpose |
|---|---|---|---|
| GET | /health/live, /health/ready | Kubernetes | Liveness/readiness; ready-fails if model not yet loaded |
| GET | /metrics | Prometheus | Scrape |
| GET | /v1/fraud/openapi.json | Any authenticated | OpenAPI 3.1 doc |
3. REST — internal mTLS plane (port 3015)
Reserved for service-to-service calls that bypass Kong (mTLS authentication, no JWT). Not exposed externally.
| Method | Path | Caller | Purpose |
|---|---|---|---|
| POST | /v1/internal/fraud/feed/import | regulator-portal-service (CN: regulator-portal), peer-MNO services | MISP/STIX feed import (signed) |
| POST | /v1/internal/fraud/signals/backfill | cdr-mediation-service, dlr-processor | Late-arriving event backfill (idempotent on signalId) |
Import body shape (MISP 2.4):
{
"Event": {
"uuid": "5f3e9a18-6c7e-4b8a-9c11-...",
"info": "ATRA daily fraud feed 2026-04-20",
"Attribute": [
{ "type": "phone-number", "value": "+93701123456", "comment": "category=msisdn" },
{ "type": "AS", "value": "AS9836", "comment": "category=peer-asn" },
{ "type": "sha256", "value": "abc123...", "comment": "category=template" }
]
},
"Signature": "<base64 PKCS#1 v1.5 signature over canonical JSON>",
"SignatureAlgorithm": "RSA-SHA256",
"SignatureKeyId": "atra-prod-2026-q2"
}
4. Error shape (REST)
Uniform envelope (per docs/standards/ERROR_CODES.md):
{
"error": {
"code": "FRAUD_VALIDATION_FAILED",
"message": "subjectScope must be one of TENANT, SENDER_ID, MSISDN, PEER_ASN",
"details": { "field": "subjectScope" },
"traceId": "00-abc-…"
}
}
| HTTP | Code | When |
|---|---|---|
| 400 | FRAUD_VALIDATION_FAILED | Payload invalid |
| 401 | UNAUTHENTICATED | Missing/invalid JWT (Kong upstream) |
| 403 | INSUFFICIENT_SCOPE | Caller role lacks scope |
| 403 | FRAUD_SEPARATION_OF_DUTIES_VIOLATED | Same actor opens & decides |
| 404 | NOT_FOUND | Case/model/feed/scan not found |
| 409 | CONFLICT | Version mismatch; case already decided |
| 410 | FRAUD_CASE_STALE | Case auto-closed |
| 412 | FRAUD_SHADOW_EVAL_INSUFFICIENT | Promote pre-conditions unmet |
| 422 | FRAUD_MODEL_ARTIFACT_INTEGRITY_FAIL | Artifact SHA-256 mismatch |
| 422 | FRAUD_FEED_SIGNATURE_INVALID | Import signature failed verification |
| 429 | RATE_LIMITED | Kong rate-limit |
| 500 | INTERNAL | Unhandled error |
| 503 | DEPENDENCY_UNAVAILABLE | Postgres / Redis / NATS / Triton unavailable |
Full catalog cross-references docs/standards/ERROR_CODES.md namespace FRAUD_*.
5. Versioning
- gRPC:
ghasi.sms.fraud.v1. Breaking change →v2in parallel with ≥ 90-day deprecation window. - REST:
/v1/fraud/*. Additive changes are non-breaking. schemaVersionon events per EVENT_SCHEMAS §7.
6. Rate limits & quotas (Kong)
| Surface | Limit |
|---|---|
| Admin REST (TnS analyst) | 1200 req/min per user |
Admin bulk endpoints (bulk-decide, scans) | 5 req/min per user |
| NOC dashboard reads | 600 req/min per user |
| Score lookup REST | 600 req/min per user |
| Internal feed import (mTLS) | No Kong limit — per-cert quota 60 req/min |
gRPC Score | No Kong limit (internal, mTLS); per-pod concurrency 2000 in-flight |
gRPC BulkScore | Per-pod 100 concurrent streams |
7. Idempotency
State-changing endpoints accept Idempotency-Key header (UUID). The server stores (key, response) for 24h. Replays return the original response with Idempotency-Replay: true.
Endpoints requiring idempotency:
POST /v1/fraud/cases/{caseId}/decidePOST /v1/admin/fraud/modelsPOST /v1/admin/fraud/models/{versionId}/promotePOST /v1/admin/fraud/models/{versionId}/rollbackPOST /v1/internal/fraud/feed/import(key = MISPEvent.uuid)
8. OpenAPI / Proto artefacts
- OpenAPI 3.1 served at
GET /v1/fraud/openapi.json; published to internal API registry on every deploy. .protoversioned in this repo; generated TypeScript + Go + Python clients published to internal package registry (@ghasi/fraud-client,pkg/ghasi/fraud/v1,ghasi-fraud-client).- Pact contract tests verify
compliance-engine ↔ fraud-intel-serviceScore gRPC contract on every CI run.