Skip to main content

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:

  1. gRPC on :50054 (mTLS required) — invoked by compliance-engine, routing-engine, sender-id-registry-service for synchronous fraud scoring. Hot path.
  2. HTTPS REST on :3014 — fronted by Kong; JWT + RBAC — for admin-dashboard (case management, model lifecycle, feed admin), NOC operators, and Trust & Safety analysts.
  3. HTTPS internal on :3015 — mTLS-only, no Kong — for regulator-portal-service and 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 statusConditionCaller behaviour
OKScore returnedUse score in tenant tier or routing decision
INVALID_ARGUMENTBad scope/id formatCaller logs, omits fraud factor
PERMISSION_DENIEDCaller SPIFFE not allowlistedCaller logs SOC alert
NOT_FOUNDUsed only by GetSignals for unknown subjectn/a for Score (PROBATION returned instead)
RESOURCE_EXHAUSTEDBulkScore batch > 1000Caller chunks
UNAVAILABLE, DEADLINE_EXCEEDEDService down or slowTreat as PROBATION (fail-closed-with-default)
INTERNALInternal errorSame 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)

MethodPathRolePurpose
GET/v1/fraud/casestns-fraud-analyst, noc-operator, platform.auditorList cases (filters: status, category, subjectScope, assignedTo, cursor)
GET/v1/fraud/cases/{caseId}sameSingle case with full evidence (feature vector, SHAP, source events, MISP joins)
POST/v1/fraud/cases/{caseId}/assigntns-fraud-analyst-leadBody: { assigneeUserId }
POST/v1/fraud/cases/{caseId}/decidetns-fraud-analystBody: { decision, reason, executeAction? }. Idempotent on caseId.
POST/v1/fraud/cases/bulk-decidetns-fraud-analyst-leadBulk action filtered by (category, subjectScope, openedBefore). Cap 1000.
GET/v1/fraud/cases/metricstns-fraud-analyst, noc-operatorQueue 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:

HTTPCodeWhen
400FRAUD_DECISION_REASON_TOO_SHORTreason < 20 chars
403FRAUD_SEPARATION_OF_DUTIES_VIOLATEDcase.openedBy = decided_by
409FRAUD_CASE_ALREADY_DECIDEDCase not in PENDING_REVIEW or IN_REVIEW
410FRAUD_CASE_STALECase auto-closed (>30d)

2.2 Detections browsing

MethodPathRolePurpose
GET/v1/fraud/detectionstns-fraud-analyst, noc-operator, platform.auditorSearch detections (filters: category, subjectScope, subjectId, since, confidenceTier)
GET/v1/fraud/detections/{detectionId}sameSingle detection with full evidence + SHAP
GET/v1/fraud/detections/{detectionId}/related-eventssameSource signal IDs that contributed

2.3 Signals (raw evidence)

MethodPathRolePurpose
GET/v1/fraud/signalstns-fraud-analyst, noc-operatorSearch recent signals (last 7d hot, older via async report)
GET/v1/fraud/signals/by-subjectsameFilter by subject (tenant/sender-id/msisdn)

2.4 Score lookup (REST mirror of gRPC)

MethodPathRolePurpose
GET/v1/fraud/score?scope=TENANT&id={tenantId}tns-fraud-analyst, noc-operator, platform.auditorSame shape as gRPC Score for human inspection
GET/v1/fraud/score/{scope}/{id}/historysameTime series of scores (last 90 days)

2.5 Model registry & lifecycle

MethodPathRolePurpose
GET/v1/admin/fraud/modelstns-ds, platform.compliance.adminList models per (category, pipeline)
GET/v1/admin/fraud/models/{modelId}sameDetail incl. evaluation metrics, training-set hash, model-card URL
GET/v1/admin/fraud/models/{modelId}/versionssameAll versions for a model
POST/v1/admin/fraud/modelstns-dsRegister new ModelVersion: { modelId, version, artifactUri, artifactSha256, trainingSetHash, featureSetHash, evaluationMetrics, modelCardUri }. Status REGISTERED.
POST/v1/admin/fraud/models/{versionId}/shadowtns-dsPromote to SHADOW. Idempotent.
POST/v1/admin/fraud/models/{versionId}/promotetns-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}/rollbacktns-dsRe-activate previous ACTIVE. < 60s.
GET/v1/admin/fraud/models/{versionId}/evaluationtns-ds, platform.auditorFull eval report (per-tenant fairness incl.)
POST/v1/admin/fraud/training-runstns-dsTrigger 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)

MethodPathRolePurpose
GET/v1/admin/fraud/feedstns-fraud-analyst-lead, platform.compliance.adminList feeds (import/export)
GET/v1/admin/fraud/feeds/{feedId}sameDetail incl. last sync, indicator counts, decay profile
POST/v1/admin/fraud/feedsplatform.compliance.adminRegister a feed
PUT/v1/admin/fraud/feeds/{feedId}sameUpdate (key rotation, schedule change)
POST/v1/admin/fraud/feeds/{feedId}/sync-nowtns-fraud-analyst-leadTrigger ad-hoc export or import run
GET/v1/admin/fraud/feeds/{feedId}/indicatorstns-fraud-analyst, tns-fraud-analyst-leadBrowse indicators (paginated)
POST/v1/admin/fraud/feeds/exports/runplatform.compliance.adminManual export trigger; returns { outputRef, sha256, signature, presignedUrls }

2.7 Retroactive scan

MethodPathRolePurpose
POST/v1/admin/fraud/scanstns-fraud-analyst-lead, platform.compliance.adminTrigger 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}sameStatus, progress, summary
GET/v1/admin/fraud/scans/{scanId}/detectionssamePaginated detections produced by the scan

2.8 Allowlists

MethodPathRolePurpose
GET/v1/admin/fraud/allowliststns-fraud-analyst-lead, platform.compliance.adminList allowlists by scope
POST/v1/admin/fraud/allowlists/{scope}/entriessameAdd (Bank/Gov sender-IDs, emergency-CBC cohorts, etc.) — secondary approver required
DELETE/v1/admin/fraud/allowlists/{scope}/entries/{value}sameRemove

2.9 Dashboard query (NOC)

MethodPathRolePurpose
GET/v1/fraud/dashboard/summarynoc-operator, tns-fraud-analystTop-level: detections last 24h, open cases, model freshness, feed sync lag
GET/v1/fraud/dashboard/topologynoc-operatorPer-MNO / per-peer-ASN heatmap
GET/v1/fraud/dashboard/tenants/rankedtns-fraud-analyst, noc-operatorTenants sorted by fraud score

2.10 Audit log query

MethodPathRolePurpose
GET/v1/fraud/audit-logplatform.auditor, tns-fraud-analyst-leadFilter by entityType, entityId, actorUserId, date range. Cursor pagination. Read-only.

2.11 Internal / operational

MethodPathCallerPurpose
GET/health/live, /health/readyKubernetesLiveness/readiness; ready-fails if model not yet loaded
GET/metricsPrometheusScrape
GET/v1/fraud/openapi.jsonAny authenticatedOpenAPI 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.

MethodPathCallerPurpose
POST/v1/internal/fraud/feed/importregulator-portal-service (CN: regulator-portal), peer-MNO servicesMISP/STIX feed import (signed)
POST/v1/internal/fraud/signals/backfillcdr-mediation-service, dlr-processorLate-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-…"
}
}
HTTPCodeWhen
400FRAUD_VALIDATION_FAILEDPayload invalid
401UNAUTHENTICATEDMissing/invalid JWT (Kong upstream)
403INSUFFICIENT_SCOPECaller role lacks scope
403FRAUD_SEPARATION_OF_DUTIES_VIOLATEDSame actor opens & decides
404NOT_FOUNDCase/model/feed/scan not found
409CONFLICTVersion mismatch; case already decided
410FRAUD_CASE_STALECase auto-closed
412FRAUD_SHADOW_EVAL_INSUFFICIENTPromote pre-conditions unmet
422FRAUD_MODEL_ARTIFACT_INTEGRITY_FAILArtifact SHA-256 mismatch
422FRAUD_FEED_SIGNATURE_INVALIDImport signature failed verification
429RATE_LIMITEDKong rate-limit
500INTERNALUnhandled error
503DEPENDENCY_UNAVAILABLEPostgres / Redis / NATS / Triton unavailable

Full catalog cross-references docs/standards/ERROR_CODES.md namespace FRAUD_*.


5. Versioning

  • gRPC: ghasi.sms.fraud.v1. Breaking change → v2 in parallel with ≥ 90-day deprecation window.
  • REST: /v1/fraud/*. Additive changes are non-breaking.
  • schemaVersion on events per EVENT_SCHEMAS §7.

6. Rate limits & quotas (Kong)

SurfaceLimit
Admin REST (TnS analyst)1200 req/min per user
Admin bulk endpoints (bulk-decide, scans)5 req/min per user
NOC dashboard reads600 req/min per user
Score lookup REST600 req/min per user
Internal feed import (mTLS)No Kong limit — per-cert quota 60 req/min
gRPC ScoreNo Kong limit (internal, mTLS); per-pod concurrency 2000 in-flight
gRPC BulkScorePer-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}/decide
  • POST /v1/admin/fraud/models
  • POST /v1/admin/fraud/models/{versionId}/promote
  • POST /v1/admin/fraud/models/{versionId}/rollback
  • POST /v1/internal/fraud/feed/import (key = MISP Event.uuid)

8. OpenAPI / Proto artefacts

  • OpenAPI 3.1 served at GET /v1/fraud/openapi.json; published to internal API registry on every deploy.
  • .proto versioned 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-service Score gRPC contract on every CI run.