Skip to main content

CDR Mediation Service — API Contracts

Version: 1.0 Status: Draft Owner: Commerce + Regulator Liaison Last Updated: 2026-04-21 Companion: APPLICATION_LOGIC · SYNC_CONTRACT · SECURITY_MODEL · EVENT_SCHEMAS

The cdr-mediation-service exposes a single interface plane:

  • HTTPS REST on :3018 — fronted by Kong, authenticated via platform JWT + RBAC, or via mTLS-only on internal network (for regulator-portal-service).

There is no gRPC hot path. CDR generation is event-driven (sms.dlr.inbound → CDR row), and the regulator-portal-service queries are read-heavy, low-volume (≤ 100 RPS), infrequent admin workflows. The absence of a gRPC plane is an intentional design choice to minimise the attack surface facing regulator-portal-service.


1. Base path and versioning

All endpoints are under /v1/cdr. Per-export-schema variants are carried in request bodies, not URLs, so that the regulator can evolve the TAP/RAP schema without breaking the REST API. Breaking REST changes bump the path to /v2/cdr/* with a 90-day deprecation window (see SYNC_CONTRACT §5).


2. Authentication & authorization

CallerAuthenticationRequired scope
admin-dashboard (platform operator)Platform JWT (RS256, JWKS from auth-service)cdr:read, cdr:write, or cdr:admin
regulator-portal-servicemTLS (CN=regulator-portal-service) + regulator JWTcdr:read (regulator-scoped) or cdr:verify
billing-service (internal recon)mTLS onlycdr:read
analytics-service (cold-tier export)mTLS onlycdr:read
Operator self-service (future)Platform JWT with tenant scopecdr:read RLS-gated

Roles are enforced by NestJS RoleGuard at handler boundary + Postgres RLS on tenant-scoped queries. Bulk mutation endpoints require a secondary X-Approver-Jwt header (four-eyes principle) when impact exceeds thresholds declared below.


3. Endpoint catalog

3.1 Query / read

MethodPathRolePurpose
GET/v1/cdr/recordscdr:readList CDRs with cursor pagination + filters
GET/v1/cdr/records/{cdrId}cdr:readSingle CDR with row hash + chain hash
GET/v1/cdr/rollupscdr:readList hourly rollups with filters
GET/v1/cdr/rollups/{bucketHour}/{operatorId}cdr:readSingle rollup with bucketRoot + chainHash
GET/v1/cdr/exportscdr:readList TAP/RAP exports with delivery status
GET/v1/cdr/exports/{exportId}cdr:readSingle export with signature metadata
GET/v1/cdr/exports/{exportId}/delivery-logcdr:readFull delivery attempt history
GET/v1/cdr/adjustmentscdr:readList adjustments with filters
GET/v1/cdr/adjustments/jobs/{jobId}cdr:readBulk re-rate job status
GET/v1/cdr/quarantinecdr:adminList quarantined records
GET/v1/cdr/transparency/{day}public + cdr:readTransparency log anchor + inclusion proof
GET/v1/cdr/auditcdr:admin | regulator-auditorChain verifier audit log (append-only)

Filters (query params) for /records:

tenantId, operatorId, bucketHourFrom, bucketHourTo, messageId, msisdnHashTo, adjustmentType, state, cursor, limit (≤ 500, default 100).

Response envelope:

{
"items": [ { "cdrId": "cdr_01HV...", ... } ],
"nextCursor": "eyJidWNrZXRIb3VyIjoi...",
"total": 123456
}

3.2 Chain & integrity

MethodPathRolePurpose
POST/v1/cdr/verifycdr:readVerify a single CDR's row hash
POST/v1/cdr/chain/verifycdr:verifyVerify a bucket-hour chain; optional Merkle inclusion proof
POST/v1/cdr/chain/verify/rangecdr:verifyWalk a 24 h range; returns aggregate verification summary

Request — single CDR verify:

POST /v1/cdr/verify
{ "cdrId": "cdr_01HV2P..." }

Response 200:
{
"cdrId": "cdr_01HV2P...",
"bucketHour": "2026-04-20T13:00:00Z",
"storedRowHash": "3c7b...",
"computedRowHash": "3c7b...",
"chainHashPrev": "b11a...",
"status": "VALID" | "MISMATCH"
}

Request — bucket chain verify:

POST /v1/cdr/chain/verify
{
"bucketHour": "2026-04-20T13:00:00Z",
"operatorId": "AWCC",
"proofForCdrId": "cdr_01HV2P..."
}

Response 200:
{
"bucketRoot": "9f3c...",
"chainHash": "73a8...",
"prevChainHash": "1200...",
"recordCount": 8421,
"sealedAt": "2026-04-20T13:05:12Z",
"signerKeyId": "cdr-export-signer-v1",
"verified": true,
"inclusionProof": {
"leafIndex": 3124,
"siblings": ["...", "...", ...]
}
}

3.3 Adjustments

MethodPathRolePurpose
POST/v1/cdr/{cdrId}/adjustmentscdr:writeIssue CORRECTION or VOID
POST/v1/cdr/adjustments/reratecdr:write (+ X-Approver-Jwt > 100k)Bulk RE_RATE
GET/v1/cdr/{cdrId}/adjustmentscdr:readList adjustments for a CDR (chain)

Create body — CORRECTION:

POST /v1/cdr/cdr_01HV2P.../adjustments
Idempotency-Key: op-1234
{
"type": "CORRECTION",
"correctedFields": {
"tapTariffClass": "A042",
"chargeAmount": "0.038400"
},
"reason": "Tariff misclassification — transactional SMS to MTN_AF",
"ticketId": "JIRA-CDR-4821"
}

Response 201:
{
"adjustmentId": "adj_01HV9T...",
"originalCdrId": "cdr_01HV2P...",
"bucketHour": "2026-04-21T09:00:00Z",
"rapBatchExpected": "2026-04-22T22:30:00Z"
}

Create body — VOID:

{
"type": "VOID",
"voidReason": "DUPLICATE_DLR",
"reason": "DLR redelivered after consumer crash; duplicate projection confirmed by trace XYZ",
"ticketId": "JIRA-CDR-4822"
}

Create body — bulk RE_RATE:

POST /v1/cdr/adjustments/rerate
X-Approver-Jwt: eyJhbGciOi...
{
"filter": {
"tenantId": "t_01HA...",
"operatorId": "AWCC",
"bucketHourFrom": "2026-04-01T00:00:00Z",
"bucketHourTo": "2026-04-14T23:59:59Z"
},
"newPricingTableId": "price_01HW...",
"ticketId": "JIRA-CDR-5000",
"reason": "Retroactive transactional tariff change per contract addendum"
}

Response 202:
{
"jobId": "adjjob_01HX...",
"estimatedImpact": 84200,
"requiresApproval": false
}

Impact > 100,000 CDRs: 403 FOUR_EYES_REQUIRED if X-Approver-Jwt missing or approver identity equals requester.

3.4 Export operations

MethodPathRolePurpose
POST/v1/cdr/exports/{exportId}/redropcdr:adminRe-deliver signed file without re-signing
POST/v1/cdr/exports/triggercdr:adminForce a TAP/RAP encoder run outside schedule
POST/v1/cdr/quarantine/{quarantineId}/resubmitcdr:adminRe-encode after fix
POST/v1/cdr/exports/{exportId}/mark-ackedregulator mTLS onlyATRA-callable ack endpoint

Trigger body:

POST /v1/cdr/exports/trigger
{
"exportType": "TAP_3_12" | "RAP_1_5",
"settlementDay": "2026-04-19",
"operatorId": "AWCC",
"schemaVariant": "atra-tap-v2",
"reason": "Regulator urgent request — missing file retransmit"
}

3.5 Schema adapter admin

MethodPathRolePurpose
GET/v1/cdr/regulator-schemascdr:adminList available schema variants
POST/v1/cdr/regulator-schemas/{variant}:activatecdr:admin + four-eyesActivate a new schema variant
GET/v1/cdr/regulator-schemas/{variant}/validatecdr:adminDry-run encode 100 sample CDRs

3.6 Health & introspection

MethodPathCallerPurpose
GET/health/liveK8sLiveness
GET/health/readyK8sReadiness (PG + NATS + Vault reachable)
GET/metricsPrometheusScrape
GET/v1/cdr/openapi.jsonAny authenticatedOpenAPI 3.1 generated doc

4. Error envelope

{
"error": {
"code": "CDR_BUCKET_NOT_SEALED",
"message": "Bucket 2026-04-21T09:00:00Z not yet sealed",
"details": {
"bucketHour": "2026-04-21T09:00:00Z",
"operatorId": "AWCC",
"expectedSealBy": "2026-04-21T09:05:00Z"
},
"traceId": "00-abc-..."
}
}
HTTPCodeWhen
400CDR_VALIDATION_FAILEDBody / query param invalid
401UNAUTHENTICATEDMissing/invalid JWT or mTLS
403INSUFFICIENT_SCOPERole lacks required scope
403FOUR_EYES_REQUIREDBulk re-rate needs approver
404CDR_NOT_FOUND / EXPORT_NOT_FOUND / ADJUSTMENT_NOT_FOUND
409BUCKET_NOT_SEALEDChain verify before seal
409ALREADY_VOIDEDVoiding a voided CDR
409DUPLICATE_FILESequence collision
409EXPORT_ALREADY_ACKEDRedrop after ACK
422CDR_CHAIN_MISMATCHRecomputation does not match stored hash (rare; usually 500 + SEV1)
422REGULATOR_SCHEMA_INVALIDVariant cannot encode provided row set
429RATE_LIMITEDKong rate limit
500INTERNALUnhandled
503DEPENDENCY_UNAVAILABLEPostgres / Vault / HSM / NATS down
503HSM_UNAVAILABLESigning pipeline degraded

Error codes follow the platform convention in docs/standards/ERROR_CODES.md.


5. Pagination

Cursor-based for all list endpoints. Opaque base64 cursor encodes the last (bucketHour, cdrSequence) tuple. limit ≤ 500 (soft cap 100). nextCursor absent when last page reached. No offset support (unsafe at scale).


6. Rate limits (Kong)

SurfaceLimit
Read endpoints (GET)600 req/min per operator
Write endpoints (POST single adjustment)120 req/min per operator
Bulk re-rate5 req/hour per operator
Chain verify60 req/min per regulator tenant
Quarantine resubmit60 req/min

7. Versioning & deprecation

  • /v1/cdr/* — additive changes non-breaking (new optional fields, new endpoints).
  • Breaking changes → /v2/cdr/*, co-hosted for 90 days, deprecation header Deprecation: true; sunset="<rfc3339>" on /v1.
  • The schemaVariant parameter in request bodies (for exports) is independent of the REST API version — ATRA schema revisions do not require a REST bump.

8. OpenAPI + proto artefacts

  • OpenAPI 3.1 served at GET /v1/cdr/openapi.json.
  • Schema-validation tests (spectral) run on every CI build.
  • No .proto artefact (service does not expose gRPC).
  • TAP 3.12 ASN.1 modules shipped under config/tap-schemas/atra-tap-v1.asn1 etc.; loaded at runtime by the encoder.

9. Cross-References

End of API_CONTRACTS.md