Metering, billing & settlement platform
The normative engineering bundle that governs the numbers in the cockpit — this is what wins where the prose and a schema disagree. Reference material for whoever builds the backend; the full specification document carries the narrative, including the India GST and Ind AS 115 treatment.

OpenAPI 3.1, 18 operations: ingest, rate, invoice, credit, dispute, reconcile.
openapi: 3.1.0
info:
title: Reliance Intelligence — Metering, Billing, Settlement & Revenue Platform API
version: 1.0.0
description: |
Internal + partner API for the capacity (GPU-hour) and token (inference) businesses.
## Non-negotiable conventions
- **Money is integer minor units** (paise for INR, cents for USD) in every field ending `_minor`.
Rates and quantities are **decimal strings**, never JSON numbers — IEEE-754 cannot represent
them exactly and rating must be byte-reproducible.
- **Every mutating call requires `Idempotency-Key`.** Replaying a key within 7 days returns the
ORIGINAL response with `Idempotent-Replay: true`. Same key + different body = `409`.
- **Time is RFC3339 UTC** with microsecond precision. Billing periods are evaluated in
`Asia/Kolkata` for Indian selling entities; the boundary conversion happens once, in the
period resolver, never ad hoc in a service.
- **Nothing is deleted.** Corrections are compensating records. `DELETE` exists only for
draft objects that were never issued.
- **Pagination** is cursor-based (`page[size]`, `page[after]`); offset paging is not offered
because the event tables are append-only and large.
contact: { name: Platform Billing Engineering, email: billing-eng@ril-intelligence.in }
servers:
- url: https://billing.internal.ril-intelligence.in/v1
description: Production (in-jamnagar-1 primary, in-mumbai-2 standby)
- url: https://billing.stage.internal.ril-intelligence.in/v1
description: Staging
security:
- mtls: []
- oauth2: [billing.read]
tags:
- name: Metering
description: Usage-event ingestion and query. The only write path for billable quantities.
- name: Pricing
description: Versioned price books. Four-eyes gated.
- name: Rating
description: Deterministic conversion of usage to money.
- name: Billing
description: Invoice assembly, issue, credit notes, India e-invoicing (IRN).
- name: Settlement
description: Payments, application, TDS, dunning, disputes.
- name: Contracts
description: Take-or-pay reservations and committed-spend drawdown.
- name: Revenue
description: Ind AS 115 schedules, deferred revenue, RPO/backlog.
- name: Assurance
description: Three-way match, breaks, leakage findings.
paths:
/usage/events:batch:
post:
tags: [Metering]
operationId: ingestUsageEvents
summary: Ingest a batch of usage events
description: |
The single ingestion path. Accepts up to 5,000 events per call.
**Partial success is the contract.** A batch never fails wholesale for a bad element:
each event is accepted, deduplicated, or rejected individually and the response reports
per-event outcomes. This exists because a producer that must drop an entire batch to get
past one malformed event will drop *revenue*.
**Dedup**: on `event_id`, 7-day window in the stream layer plus a permanent unique index
in the rating ledger. A duplicate returns `deduplicated`, not an error.
**Clock guard**: events with `occurred_at` more than 500 ms in the future, or older than
the configured late-event window (default 72 h), are accepted but flagged
`quarantined` — they land in the quarantine topic for operator disposition rather than
being silently rated or silently lost.
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [events]
properties:
events:
type: array
minItems: 1
maxItems: 5000
items: { $ref: '#/components/schemas/UsageEvent' }
responses:
'202':
description: Batch accepted; see per-event results.
content:
application/json:
schema:
type: object
properties:
accepted: { type: integer }
deduplicated: { type: integer }
quarantined: { type: integer }
rejected: { type: integer }
results:
type: array
items:
type: object
properties:
event_id: { type: string, format: uuid }
status: { type: string, enum: [accepted, deduplicated, quarantined, rejected] }
reason: { type: [string, 'null'] }
'400': { $ref: '#/components/responses/BadRequest' }
'429': { $ref: '#/components/responses/RateLimited' }
/usage/events/{event_id}:
get:
tags: [Metering]
operationId: getUsageEvent
summary: Fetch a raw usage event
description: Disputes are settled by replaying raw events, not by arguing about aggregates. This endpoint is the evidence path and is retained for 25 months hot / 8 years cold.
parameters:
- name: event_id
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/UsageEvent' }
'404': { $ref: '#/components/responses/NotFound' }
/usage/aggregate:
get:
tags: [Metering]
operationId: aggregateUsage
summary: Aggregate billable metrics for a period
description: |
Read-only rollup used by the customer portal, the deal desk, and the completeness control.
Returns both the aggregate and `event_count`, so the caller can assert completeness against
the raw store rather than trusting a number.
parameters:
- { name: account_id, in: query, required: true, schema: { type: string } }
- { name: from, in: query, required: true, schema: { type: string, format: date-time } }
- { name: to, in: query, required: true, schema: { type: string, format: date-time } }
- { name: group_by, in: query, schema: { type: string, example: 'sku,project_id,service_tier' } }
- name: leg
in: query
description: Which reconciliation leg to aggregate. Defaults to intent (the billable leg).
schema: { type: string, enum: [intent, reality], default: intent }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
groups:
type: array
items:
type: object
properties:
key: { type: object, additionalProperties: { type: string } }
meter: { type: string }
quantity: { type: string, description: Decimal string. }
unit: { type: string }
event_count: { type: integer }
/pricebooks:
post:
tags: [Pricing]
operationId: createPriceBookVersion
summary: Create a draft price-book version
parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PriceBook' }
responses:
'201': { description: Draft created., content: { application/json: { schema: { $ref: '#/components/schemas/PriceBook' } } } }
/pricebooks/{price_book_id}/versions/{version}:publish:
post:
tags: [Pricing]
operationId: publishPriceBook
summary: Publish a price-book version (four-eyes)
description: |
**Rejects with `422` if `approval.approved_by` equals `approval.authored_by`.** This is
enforced in the API, not in policy documents, because the single most common source of
material rating leakage is an unreviewed rate-table change.
Publishing is effective-dated and never retroactive past a closed accounting period; an
attempt to publish `effective_from` inside a closed period returns `409`.
parameters:
- { name: price_book_id, in: path, required: true, schema: { type: string } }
- { name: version, in: path, required: true, schema: { type: integer } }
- { $ref: '#/components/parameters/IdempotencyKey' }
responses:
'200': { description: Published. }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/Unprocessable' }
/rating/runs:
post:
tags: [Rating]
operationId: createRatingRun
summary: Rate (or re-rate) a period
description: |
Orchestrated as a Temporal workflow. `mode=dry_run` produces rated lines in a shadow
namespace and a diff against the last committed run — this is the pre-close control and
the mechanism by which a rating change is proven safe before it touches revenue.
`mode=rerate` supersedes prior lines (setting `rating.rerate_of`) and emits reversal +
replacement lines. It never mutates.
parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tenant_id, period_start, period_end, mode]
properties:
tenant_id: { type: string }
account_ids: { type: array, items: { type: string }, description: Omit for all accounts. }
period_start: { type: string, format: date-time }
period_end: { type: string, format: date-time }
mode: { type: string, enum: [commit, dry_run, rerate] }
reason: { type: [string, 'null'], description: Required for rerate. }
responses:
'202':
description: Run started.
content:
application/json:
schema:
type: object
properties:
run_id: { type: string }
workflow_id: { type: string }
status: { type: string, enum: [queued, running, completed, failed] }
/rating/runs/{run_id}:
get:
tags: [Rating]
operationId: getRatingRun
summary: Rating run status, totals, and diff
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
run_id: { type: string }
status: { type: string }
engine_version: { type: string }
lines_created: { type: integer }
total_net_minor: { type: integer }
diff_vs_previous:
type: object
description: Present for dry_run. A material diff blocks the close until explained.
properties:
delta_net_minor: { type: integer }
accounts_changed: { type: integer }
largest_movers: { type: array, items: { type: object } }
/rated-lines:
get:
tags: [Rating]
operationId: listRatedLines
summary: Query rated lines
parameters:
- { name: account_id, in: query, schema: { type: string } }
- { name: invoice_id, in: query, schema: { type: [string, 'null'], description: 'Pass the literal string null to find UNBILLED usage.' } }
- { name: period_start, in: query, schema: { type: string, format: date-time } }
- { name: period_end, in: query, schema: { type: string, format: date-time } }
- { $ref: '#/components/parameters/PageSize' }
- { $ref: '#/components/parameters/PageAfter' }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/RatedLineItem' } }
next_cursor: { type: [string, 'null'] }
/invoices:assemble:
post:
tags: [Billing]
operationId: assembleInvoice
summary: Assemble a draft invoice from rated lines
description: |
Groups rated lines, resolves tax, applies statutory round-off once at document level, and
allocates **no invoice number** — numbering happens at issue, so a failed assembly never
burns a number in the gapless GST series.
parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tenant_id, account_id, period_start, period_end]
properties:
tenant_id: { type: string }
account_id: { type: string }
period_start: { type: string, format: date-time }
period_end: { type: string, format: date-time }
include_unbilled_prior_periods: { type: boolean, default: true, description: Sweeps rated lines left unbilled by earlier cycles. Leaving this false is how unbilled usage ages. }
responses:
'201':
description: Draft invoice.
content: { application/json: { schema: { $ref: '#/components/schemas/Invoice' } } }
/invoices/{invoice_id}:issue:
post:
tags: [Billing]
operationId: issueInvoice
summary: Issue an invoice (allocates number, triggers IRN)
description: |
Transactional sequence, all-or-nothing on the number allocation:
1. Allocate the next number from the `(gstin, fy, series)` sequence under row lock.
2. Persist the document as immutable; compute `pdf_sha256`.
3. Enqueue IRN registration with the IRP (asynchronous, retried with backoff).
The invoice reaches `issued` after step 2. IRP registration is **not** in the critical
path — an IRP outage must never block billing — but `gst.irp_status` stays `pending`
and the 30-day reporting clock is monitored (alert at T+7, page at T+21).
parameters:
- { name: invoice_id, in: path, required: true, schema: { type: string, format: uuid } }
- { $ref: '#/components/parameters/IdempotencyKey' }
responses:
'200': { description: Issued., content: { application/json: { schema: { $ref: '#/components/schemas/Invoice' } } } }
'409': { $ref: '#/components/responses/Conflict' }
/invoices/{invoice_id}/credit-notes:
post:
tags: [Billing]
operationId: createCreditNote
summary: Issue a credit note against an invoice
description: |
The ONLY way to reduce an issued invoice. Requires `credit_reason`; reasons are
governed — `billing_error` is the only value that counts against the billing-accuracy KPI,
and `commercial_goodwill` above a threshold requires an approval reference.
A credit note is itself a GST document and gets its own IRN.
parameters:
- { name: invoice_id, in: path, required: true, schema: { type: string, format: uuid } }
- { $ref: '#/components/parameters/IdempotencyKey' }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [credit_reason, lines]
properties:
credit_reason: { type: string, enum: [sla_credit, billing_error, commercial_goodwill, dispute_settlement, cancellation, rerate_adjustment] }
approval_ref: { type: [string, 'null'] }
lines: { type: array, items: { type: object } }
responses:
'201': { description: Credit note created., content: { application/json: { schema: { $ref: '#/components/schemas/Invoice' } } } }
/invoices/{invoice_id}/irn:
post:
tags: [Billing]
operationId: registerIRN
summary: Register (or re-attempt) the invoice with the IRP
description: Idempotent against the IRP's own duplicate-IRN response — a `2150 Duplicate IRN` from the portal is treated as SUCCESS and the returned IRN is stored, not as an error.
parameters:
- { name: invoice_id, in: path, required: true, schema: { type: string, format: uuid } }
- { $ref: '#/components/parameters/IdempotencyKey' }
responses:
'200': { description: Registered. }
'424': { description: IRP unavailable or rejected; retry scheduled. }
/contracts/{contract_id}/commitment:
get:
tags: [Contracts]
operationId: getCommitmentBalance
summary: Take-or-pay / committed-spend position
description: |
Returns the drawdown state that the rating engine used, not a recomputation. Fields
distinguish **consumed**, **committed**, **overage**, and **forfeited** — the last is the
take-or-pay economics made visible, and customers who cannot see it dispute it.
parameters: [{ name: contract_id, in: path, required: true, schema: { type: string } }]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
contract_id: { type: string }
period_start: { type: string, format: date-time }
period_end: { type: string, format: date-time }
committed_minor: { type: integer }
consumed_minor: { type: integer }
overage_minor: { type: integer }
forfeited_minor: { type: integer }
rollover_minor: { type: integer }
rollover_expires_at: { type: [string, 'null'], format: date-time }
/payments:
post:
tags: [Settlement]
operationId: recordPayment
summary: Record a receipt
description: |
Handles the Indian enterprise reality that customers remit **net of TDS**
(typically 2% u/s 194J for technical services). A receipt short by exactly the expected
TDS is applied as `paid_with_withholding` and does NOT open a dispute; the residual sits in a
TDS-receivable control account until the Form 16A / 26AS credit is matched.
parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [account_id, amount_minor, currency, value_date, method]
properties:
account_id: { type: string }
amount_minor: { type: integer }
currency: { type: string }
value_date: { type: string, format: date }
method: { type: string, enum: [neft, rtgs, upi, swift, card, credit_note_offset] }
bank_reference: { type: [string, 'null'] }
remittance_advice: { type: [array, 'null'], items: { type: object }, description: Invoice-level allocation supplied by the payer, when present. }
withholding_minor: { type: integer, default: 0 }
responses:
'201': { description: Recorded and applied. }
/reconciliation/runs:
post:
tags: [Assurance]
operationId: runReconciliation
summary: Execute the three-way match
description: |
Compares **intent** (control-plane allocation ledger) vs **reality** (node/serving telemetry)
vs **billing** (rated + invoiced). Runs daily at 02:00 IST plus on demand. Breaks above the
materiality threshold open findings with an owner and a closure SLA.
parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [tenant_id, date]
properties:
tenant_id: { type: string }
date: { type: string, format: date }
materiality_minor: { type: integer, default: 100000 }
responses:
'202': { description: Run started. }
/reconciliation/breaks:
get:
tags: [Assurance]
operationId: listBreaks
summary: Open reconciliation breaks
parameters:
- { name: status, in: query, schema: { type: string, enum: [open, investigating, resolved, accepted] } }
- { name: category, in: query, schema: { type: string, enum: [unmetered_usage, meter_to_bill_gap, rating_error, entitlement_drift, sla_over_crediting, fraud, contract_leakage, tokenizer_drift] } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
break_id: { type: string }
category: { type: string }
account_id: { type: [string, 'null'] }
estimated_value_minor: { type: integer }
intent_qty: { type: string }
reality_qty: { type: string }
billed_qty: { type: string }
first_seen: { type: string, format: date-time }
status: { type: string }
owner: { type: [string, 'null'] }
/revenue/schedules:
get:
tags: [Revenue]
operationId: listRevenueSchedules
summary: Ind AS 115 recognition schedule
description: Period-by-period recognized, deferred, and unbilled-receivable amounts by performance obligation. This is the ledger's source for the revenue journal; finance does not re-derive it in a spreadsheet.
parameters:
- { name: tenant_id, in: query, required: true, schema: { type: string } }
- { name: period, in: query, required: true, schema: { type: string, example: '2026-08' } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
period: { type: string }
recognized_minor: { type: integer }
deferred_closing_minor: { type: integer }
unbilled_receivable_minor: { type: integer }
variable_consideration_minor: { type: integer }
by_obligation: { type: array, items: { type: object } }
/revenue/rpo:
get:
tags: [Revenue]
operationId: getRPO
summary: Remaining performance obligations (backlog)
description: |
Contracted-but-unrecognized revenue with expected conversion by period. This is the number
lenders underwrite and the number that must tie to the disclosed backlog — it is computed
from contract lines and delivery/acceptance state, never typed in.
parameters:
- { name: tenant_id, in: query, required: true, schema: { type: string } }
- { name: as_of, in: query, required: true, schema: { type: string, format: date } }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
as_of: { type: string, format: date }
total_rpo_minor: { type: integer }
expected_conversion:
type: array
items:
type: object
properties:
period: { type: string }
amount_minor: { type: integer }
contingent_on_acceptance_minor: { type: integer, description: The portion that converts only if capacity passes acceptance on schedule. Disclosing this separately is the honest version of backlog. }
components:
securitySchemes:
mtls: { type: mutualTLS }
oauth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://auth.internal.ril-intelligence.in/oauth2/token
scopes:
billing.read: Read billing data
billing.write: Ingest usage and create documents
billing.admin: Publish price books, issue credit notes
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: "Client-generated UUID. Replay within 7 days returns the original response with `Idempotent-Replay: true`."
schema: { type: string, format: uuid }
PageSize:
name: page[size]
in: query
schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
PageAfter:
name: page[after]
in: query
schema: { type: string }
responses:
BadRequest:
description: Malformed request.
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
NotFound:
description: Not found.
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
Conflict:
description: State conflict (e.g. already issued, closed period, idempotency-key body mismatch).
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
Unprocessable:
description: Semantically invalid (e.g. self-approved price book).
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
RateLimited:
description: Too many requests.
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
schemas:
Problem:
type: object
description: RFC 9457 problem details.
properties:
type: { type: string }
title: { type: string }
status: { type: integer }
detail: { type: string }
instance: { type: string }
errors:
type: array
items:
type: object
properties:
pointer: { type: string }
code: { type: string }
message: { type: string }
UsageEvent:
$ref: '../schemas/usage_event.envelope.schema.json'
PriceBook:
$ref: '../schemas/price_book.schema.json'
RatedLineItem:
$ref: '../schemas/rated_line_item.schema.json'
Invoice:
$ref: '../schemas/invoice.schema.json'
Conformance checklist
Conformance checklist
A component is conformant when every applicable control below is implemented, automated, and alarmed. Each control names its owner, its cadence, and the failure it prevents. Controls marked BLOCKING stop the billing close when they fail — they are not advisory dashboards.
Controls C-01…C-15 have executable SQL in sql/reconciliation.sql.
Cases TV-* are in test/golden_vectors.json.
A. Metering (producers)
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| M-01 | Every billable action emits exactly one UsageEvent with a deterministic event_id (UUIDv7). Retries reuse the id. | continuous | Producer teams | Duplicate billing; silent loss on retry |
| M-02 | Producers buffer to local durable storage when the ingest endpoint is unreachable, and drain in order on recovery. Buffer depth ≥ 6 h. | continuous | Node/Gateway | Revenue lost to a network partition |
| M-03 | Clocks are PTP- or NTP-disciplined; drift > 500 ms raises a producer alarm and events are quarantined, not rated. | continuous | SRE | Usage attributed to the wrong billing period |
| M-04 | Capacity intent and reality legs use identical window boundaries so they join on (allocation_id, window_start). | design-time | Control plane / Node agent | A reconciliation that can never balance |
| M-05 | Token events are emitted at stream close, including on client disconnect, with tokens actually generated. | continuous | API Gateway | Unbilled generation on abandoned streams |
| M-06 | cached_input_tokens is disjoint from input_tokens. Asserted on live traffic, not only in unit tests. → TV-TOK-002 | continuous | API Gateway | Double-billing the cached portion |
| M-07 | tokenizer_version is emitted on every token event and is under four-eyes change control. → C-14, TV-ING-002 | per release | Serving | Silent re-pricing of every request |
| M-08 | Orphan sweep: allocations present in node telemetry with no control-plane record (and vice versa) are reported within 15 min. | 15 min | Control plane | Capacity that runs and never bills |
| M-09 | Producer emits source.version; a meter defect can be scoped to exact builds and blast radius computed. | continuous | All producers | Unbounded restatement when a defect is found |
B. Ingestion
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| I-01 | Idempotency enforced at ingest (7-day dedupe window) and by a permanent unique constraint in the ledger. Both, not either. → TV-ING-001 | continuous | Platform | Double billing |
| I-02 | Partial-success batch semantics: one malformed event never rejects a batch. | continuous | Platform | Producers dropping whole batches of revenue |
| I-03 | Quarantine is a queue with an owner and an SLA, not a dead-letter topic nobody reads. Aged quarantine > 72 h pages. | daily | Revenue Assurance | Silently discarded revenue |
| I-04 | Late-event window (default 72 h) is configured, documented, and exercised routinely via re-rate. → TV-REC-001 | continuous | Platform | Emergency handling of a routine event |
| I-05 | Negative quantities are rejected unless correction_of is set with a correction_reason. | continuous | Platform | Unexplained revenue reductions |
| I-06 | Ingest lag SLI: p99 emitted_at → queryable < 60 s. Breach alarms. | continuous | Platform | Portal/usage numbers customers cannot trust |
C. Pricing & rating
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| C-07 | BLOCKING. Rating is deterministic: re-running with the same input_hash yields a byte-identical rated line. Asserted in CI on the full golden-vector suite. | per build | Billing Eng | Invoices that cannot be defended in a dispute or audit |
| C-11 | BLOCKING. Price-book publication requires approved_by ≠ authored_by plus a change ticket. Enforced by a DB constraint, not policy. | per change | Pricing / Deal Desk | The most common material rating leakage |
| C-14 | BLOCKING. Tokenizer or model-version change runs the golden-request suite; billed amounts asserted to the paise before rollout. → TV-ING-002 | per release | Serving / Billing Eng | Silent re-pricing across the whole book |
| C-04 | Weekly sampled re-rate audit of issued invoices against pinned price-book versions; stratified to always include the largest. | weekly | Revenue Assurance | Undetected rating drift |
| R-01 | Money is integer minor units; rates and quantities are exact decimals. No IEEE-754 in the money path — enforced by a linter/import ban in CI. → TV-CAP-002 | per build | Billing Eng | Rounding drift nobody can reconcile |
| R-02 | Rounding is half-even, applied once per rated line; document round-off applied once at the invoice total. | design-time | Billing Eng | Systematic bias; totals that do not foot |
| R-03 | Tiering mode (graduated vs volume) is explicit in the price book and printed on the invoice. → TV-RATE-001 | design-time | Deal Desk | The classic tiering dispute |
| R-04 | Index-linked rates freeze the index print and FX rate onto the rated line; a floor is mandatory. → TV-RATE-002 | per contract | Deal Desk / Treasury | History changing when an index is restated; unpriced margin risk |
| R-05 | Pre-close dry_run diff against the prior committed run; a material delta blocks the close until explained. | per cycle | Billing Eng | Shipping a rating change straight into revenue |
D. Contracts & commitment
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| K-01 | Take-or-pay bills the commitment regardless of consumption; drawdown, overage, and forfeiture are separately visible to the customer. → TV-CON-001/002 | per cycle | Billing Eng | Overage absorbed silently into the commitment line |
| K-02 | Rollover policy is explicit per contract; rollover balances create a tracked contract liability with an expiry. | per contract | Finance | Early revenue recognition |
| K-03 | BLOCKING. Billing for newly delivered capacity does not start before accepted_at. → TV-CAP-004 | per delivery | Delivery / Billing | Disputes on greenfield clusters; misstated RPO |
| K-04 | provisioned token traffic is metered and shown zero-rated on the invoice, not omitted. | per cycle | Billing Eng | Customers unable to verify what their reservation bought |
E. Invoicing & India GST
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| G-01 | BLOCKING. Invoice numbers are gapless per (GSTIN, FY, series), ≤ 16 chars, allocated under row lock at issue — never from a PostgreSQL SEQUENCE. | per issue | Billing Eng | GST non-compliance; unexplainable gaps at audit |
| G-02 | IRN registration is asynchronous and retried; an IRP outage never blocks issuing. IRP error 2150 Duplicate IRN is treated as success. | continuous | Billing Eng | Billing halted by a third-party outage; duplicate-IRN loops |
| C-08 | 30-day IRN reporting clock monitored: alert T+7, page T+21, incident at breach. | hourly | Revenue Assurance | Customer loses input tax credit — a commercial incident |
| G-03 | Place of supply resolves to exactly one of CGST+SGST or IGST. Both populated is rejected by a DB constraint. → TV-TAX-002 | per invoice | Tax Engine | Wrong tax, wrong returns, customer credit denied |
| C-09 | BLOCKING. Export-without-payment invoicing is blocked when the LUT has lapsed; 45-day expiry warning. → TV-TAX-001 | daily | Finance / Tax | Silently invoicing zero-rated without valid authority |
| G-04 | SEZ and export supplies carry correct IRP supply-type codes (SEZWP/SEZWOP/EXPWP/EXPWOP). | per invoice | Tax Engine | Rejected e-invoices; misclassified exports |
| G-05 | Issued documents are immutable; amendments only by credit/debit note, each with its own IRN. Enforced by trigger. | continuous | Billing Eng | Retroactive edits to tax documents |
| G-06 | Signed IRP JSON and QR are archived verbatim for 8 years (Companies Act s.128 books; GST records 72 months from annual-return due date). | continuous | Platform | Inability to evidence a document at assessment |
F. Settlement
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| S-01 | A receipt short by exactly the expected TDS is applied as withholding_tds, not opened as a dispute. | continuous | AR | False disputes on every enterprise payment |
| C-10 | Unmatched TDS receivable aged by quarter against Form 26AS / 16A. | quarterly | Finance | Withholding that quietly becomes a cash loss |
| S-02 | Export receipts matched to FIRC/e-BRC within the 12-month FEMA realisation window; breaches raised as compliance exceptions. | monthly | Treasury | FEMA non-compliance |
| S-03 | Dunning profiles differ by segment: self-serve card retry ladder vs enterprise collections workflow. | continuous | AR | Churned self-serve revenue; annoyed enterprises |
| S-04 | Disputes carry evidence_event_ids; settlement replays raw events. | per dispute | AR / RA | Negotiating aggregates instead of facts |
G. Revenue recognition (Ind AS 115)
| ID | Control | Cadence | Owner | Prevents | |----|---------|---------|-------|----------| | V-01 | Capacity stand-ready obligations recognise ratably as capacity is made available; usage recognises on consumption. Classification is set at rating time. | per cycle | Finance | Recognition decided in a spreadsheet after the fact | | V-02 | Expected SLA credits and expected error credit-memos are estimated as variable consideration and constrained at recognition, not deducted only when issued. | monthly | Finance | Overstated revenue in the period | | C-12 | BLOCKING. Revenue tie-out: recognised = invoiced − deferred movement + unbilled receivable. A non-zero delta stops the GL post. | monthly | Finance / Billing Eng | A close that does not foot | | C-13 | RPO snapshot is daily and immutable; the portion contingent on acceptance is disclosed separately. | daily | Finance | Backlog that cannot be reproduced for lenders | | V-03 | Multi-element contracts (capacity + tokens + support) allocate on standalone selling price. | per contract | Finance | Mis-timed revenue on bundled deals |
H. Assurance & data protection
| ID | Control | Cadence | Owner | Prevents |
|----|---------|---------|-------|----------|
| C-01 | Three-way match (intent / reality / billing) with materiality thresholds, owners, and closure SLAs. | daily | Revenue Assurance | Unmetered and unbilled usage |
| C-02 | BLOCKING. Meter-to-bill completeness on quantities and event counts. | per cycle | Revenue Assurance | Merged/split events netting to zero by luck |
| C-03 | Unbilled usage aged; no bucket beyond one cycle. | daily | Billing Ops | Revenue sitting still |
| C-05 | Margin-by-customer outliers reviewed; negative GM triggers a contract review. | monthly | Deal Desk / RA | Mis-rated or mis-contracted accounts |
| C-06 | Credit-memo governance: governed reasons, approval refs, SLA credits matched to incidents. → TV-SLA-001 | monthly | Revenue Assurance | The softest leakage channel |
| C-15 | Leakage roll-up by mode with identified vs recovered and overdue findings. | monthly | CFO / RA | A programme with no measurable outcome |
| D-01 | Usage events carry residency_zone; personal data in prompts is not persisted in the billing path — only counts and identifiers cross the boundary. | design-time | Privacy / Platform | DPDP exposure from billing telemetry |
| D-02 | As a likely Significant Data Fiduciary, Reliance must hold an India-based DPO, an independent data auditor, and DPIAs for high-risk processing under the DPDP Rules (notified 14 Nov 2025; phased to full enforcement in 2027). Billing systems are in scope for the audit. | annual | Privacy | Regulatory exposure up to ₹250 crore |
| D-03 | Access to raw prompt/response content from the billing plane is denied by default; billing needs counts, not content. | continuous | Security | The worst-case breach surface |
Definition of done for the platform
The platform is production-ready when, for a full billing cycle:
- Every BLOCKING control runs automatically and has failed at least once in a game-day exercise with the correct outcome (the close stopped).
- The golden-vector suite passes in CI on every commit, including the tokenizer and price-book change gates.
- The three-way match closes within 10 bps for capacity and 0 tokens for the token business, for five consecutive days.
- A randomly chosen issued invoice can be reproduced from raw events, byte for byte, by a person who did not build the system, in under one hour.
- The revenue tie-out (C-12) is zero for two consecutive months.
That fourth item is the real test. Everything else is machinery to make it true.
The India tax and accounting treatments here are engineering-facing summaries of published rules and need confirmation from a tax and statutory audit team before anything goes live.