{"openapi":"3.1.0","components":{"schemas":{"APIErrorResponse":{"description":"APIErrorResponse schema","properties":{"code":{"description":"Standard HTTP reason phrase for the status code.","example":"Bad Request","type":"string"},"error":{"description":"Human-readable explanation of why the request failed.","example":"invalid request","type":"string"},"status":{"description":"HTTP response status code defined by RFC 9110 semantics.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"integer"}},"type":"object"},"AddAllowlistEntryRequest":{"description":"AddAllowlistEntryRequest schema","properties":{"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"}},"type":"object"},"AddCarrierTrunkIPRequest":{"description":"AddCarrierTrunkIPRequest schema","properties":{"ip_address":{"description":"IPv4 or IPv6 address used for API allowlisting, SIP gateway routing, or trunk authentication.","example":"203.0.113.50","type":"string"},"port":{"default":5060,"description":"SIP UDP/TCP port used by a gateway or endpoint.","example":5060,"maximum":32767,"minimum":-32768,"type":"integer"},"priority":{"default":1,"description":"Gateway priority. Lower numbers are preferred first in failover mode.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"weight":{"default":1,"description":"Relative gateway weight used for weighted load balancing.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"AdjustRequest":{"description":"AdjustRequest schema","properties":{"amount":{"description":"Decimal money amount for the transaction or balance operation. Currency is provided separately.","example":"100.00","type":"string"},"cdr_id":{"description":"Call Detail Record ID linked to this transaction or adjustment.","example":1001,"format":"int64","nullable":true,"type":"integer"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","type":"string"}},"type":"object"},"AssignRateDeckRequest":{"description":"AssignRateDeckRequest schema","properties":{"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"}},"type":"object"},"AuthIPRecord":{"description":"AuthIPRecord schema"},"Balance":{"description":"Balance schema","example":"1250.0000","properties":{"balance":{"description":"Current account or carrier trunk balance as a decimal money string in the associated currency. Customer balances are keyed to customer accounts.","example":"1250.0000"},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"entity_id":{"format":"int64","type":"integer"},"entity_type":{"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"BalanceTransaction":{"description":"BalanceTransaction schema","properties":{"amount":{"description":"Decimal money amount for the transaction or balance operation. Currency is provided separately.","example":"100.00"},"call_id":{"description":"SIP Call-ID used to correlate signalling, CDRs, balance transactions, and troubleshooting logs.","example":"a84b4c76e66710@example.net","nullable":true,"type":"string"},"cdr_id":{"description":"Call Detail Record ID linked to this transaction or adjustment.","example":1001,"format":"int64","nullable":true,"type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"entity_id":{"format":"int64","type":"integer"},"entity_type":{"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"transaction_type":{"description":"Balance transaction type describing why money moved.","enum":["credit","debit","adjustment","reserve","release"],"example":"debit","type":"string"}},"type":"object"},"BlockRule":{"description":"BlockRule schema","properties":{"block_type":{"description":"Type of call attribute matched by the block rule.","enum":["ani","dnis","number_code"],"example":"dnis","type":"string"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"},"match_type":{"description":"How a block rule pattern is matched against ANI or DNIS.","enum":["exact","prefix"],"example":"prefix","type":"string"},"pattern":{"description":"ANI, DNIS, number code, or prefix pattern matched by the rule.","example":"1900","type":"string"},"scope":{"description":"Operational scope where the rule applies.","enum":["global","customer_trunk","carrier_trunk","route"],"example":"global","type":"string"},"scope_id":{"description":"ID of the scoped entity. Null when scope is global.","example":10,"format":"int64","nullable":true,"type":"integer"},"sip_response_code":{"description":"Final SIP response code recorded for the call or returned by a block rule.","example":200,"maximum":32767,"minimum":-32768,"type":"integer"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"BulkCreateRateDeckRateRequest":{"description":"BulkCreateRateDeckRateRequest schema","properties":{"on_conflict":{"default":"skip","description":"Bulk import behaviour when an incoming row conflicts with an existing record.","enum":["skip","overwrite","error"],"example":"skip","type":"string"},"rates":{"description":"Rate rows included in a bulk rate deck import request.","example":[],"items":{"description":"Rate rows included in a bulk rate deck import request.","example":[],"properties":{"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"destination_name":{"description":"Human-readable destination name for the matched rate prefix.","example":"Australia Mobile","type":"string"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","nullable":true,"type":"string"},"prefix":{"description":"Destination prefix used for longest-prefix rate matching.","example":"614","type":"string"},"rate_per_minute":{"description":"Rate value as a decimal money string in the rate deck currency. For per_minute rows this is the per-minute price; for per_call rows this is the flat per-call charge.","example":"0.0250","type":"string"},"rate_type":{"default":"per_minute","description":"Rate charging model for a prefix. per_minute uses duration and billing increments; per_call charges the value once per connected call.","enum":["per_minute","per_call"],"example":"per_minute","nullable":true,"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"BulkImportResponse":{"description":"BulkImportResponse schema","properties":{"imported":{"description":"Number of rows successfully imported by the bulk operation.","example":150,"type":"integer"},"skipped":{"description":"Number of rows skipped by a bulk operation.","example":2,"type":"integer"},"updated":{"description":"Number of rows updated by a bulk operation.","example":10,"type":"integer"}},"type":"object"},"CDR":{"description":"CDR schema","properties":{"ani":{"description":"Originating phone number, also known as caller ID, used for routing, blocking, and CDR reporting.","example":"61400111222","type":"string"},"answer_time":{"description":"UTC timestamp when the call was answered. Null or omitted means the call was not answered.","example":"2026-01-15T08:30:12Z","format":"date-time","nullable":true,"type":"string"},"billable_duration_customer":{"description":"Customer-billed call duration in seconds after applying the customer trunk billing increments.","example":60,"type":"integer"},"billable_duration_vendor":{"description":"Vendor-billed call duration in seconds after applying the carrier trunk billing increments.","example":60,"type":"integer"},"carrier_gateway_id":{"description":"Database ID of the selected carrier gateway IP row used for this call attempt.","example":5,"format":"int64","type":"integer"},"carrier_rate_deck_id":{"description":"Rate deck ID used to calculate vendor cost for the selected carrier route.","example":12,"format":"int64","nullable":true,"type":"integer"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"customer_cost":{"description":"Amount charged to the customer for the call as a decimal money string.","example":"0.0250"},"customer_currency":{"description":"Currency used for customer-side call charges.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_decimal_precision":{"description":"Decimal places used when rounding customer-side call cost.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"customer_rate_deck_id":{"description":"Rate deck ID used to calculate customer sell price for the call.","example":7,"format":"int64","nullable":true,"type":"integer"},"customer_rounding_direction":{"description":"Rounding mode applied to customer-side call cost.","enum":["up","down","standard"],"example":"standard","type":"string"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"},"duration_seconds":{"description":"Total answered call duration in seconds before billing increment rounding.","example":73,"type":"integer"},"end_time":{"description":"UTC timestamp when the call ended.","example":"2026-01-15T08:31:13Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"sip_call_id":{"description":"SIP Call-ID for correlating signalling, CDRs, and balance transactions.","example":"a84b4c76e66710@example.net","type":"string"},"sip_response_code":{"description":"Final SIP response code recorded for the call or returned by a block rule.","example":200,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"start_time":{"description":"UTC timestamp when the call attempt started.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"vendor_cost":{"description":"Amount charged by the carrier for the call as a decimal money string.","example":"0.0200"},"vendor_currency":{"description":"Currency used for vendor-side call costs.","enum":["AUD","USD"],"example":"AUD","type":"string"},"vendor_decimal_precision":{"description":"Decimal places used when rounding vendor-side call cost.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"vendor_rounding_direction":{"description":"Rounding mode applied to vendor-side call cost.","enum":["up","down","standard"],"example":"standard","type":"string"}},"type":"object"},"CPSPortMetricsResponse":{"description":"CPSPortMetricsResponse schema","properties":{"cps_current":{"description":"Current calls-per-second usage observed for the trunk at the reported timestamp.","example":3,"format":"int64","type":"integer"},"per_minute_current":{"description":"Current rolling 60-second call attempt count for the trunk.","example":25,"format":"int64","type":"integer"},"ports_current":{"description":"Current concurrent active call count for the trunk.","example":12,"format":"int64","type":"integer"},"timestamp":{"description":"UTC timestamp when the metric or event was sampled.","example":"2026-01-15T08:30:00Z","type":"string"},"trunk_id":{"description":"ID of the referenced balance entity. For trunk_type=customer this is the customer_account_id; for trunk_type=carrier this is the carrier_trunk_id.","example":10,"format":"int64","type":"integer"},"trunk_type":{"description":"Type of balance entity referenced by the record. customer means customer account-level billing; carrier means carrier trunk-level billing.","enum":["customer","carrier"],"example":"customer","type":"string"}},"type":"object"},"CSVRateDeckUploadRequest":{"description":"CSVRateDeckUploadRequest schema"},"CSVRateImportResponse":{"description":"CSVRateImportResponse schema","properties":{"count":{"description":"Number of CSV rate rows imported into the rate deck.","example":42,"type":"integer"},"errors":{"description":"Row-level validation errors when the CSV could not be imported.","example":"line 4: invalid rate_type","items":{"description":"Row-level validation errors when the CSV could not be imported.","example":"line 4: invalid rate_type","nullable":true,"type":"string"},"nullable":true,"type":"array"},"message":{"description":"Human-readable summary of the CSV import result.","example":"operation successful","type":"string"}},"type":"object"},"CacheRebuildResponse":{"description":"CacheRebuildResponse schema","properties":{"count":{"description":"Number of records affected, imported, rebuilt, or returned by the operation.","example":42,"type":"integer"},"message":{"description":"Human-readable operation result message.","example":"operation successful","type":"string"}},"type":"object"},"CallAdmitCheckRequest":{"description":"CallAdmitCheckRequest schema","properties":{"entity_id":{"example":1,"format":"int64","type":"integer"},"entity_type":{"example":"customer","type":"string"},"estimated_cost":{"description":"Estimated maximum call cost reserved or checked before allowing the call.","example":"0.2500","type":"string"}},"type":"object"},"CarrierSIPFailoverRule":{"description":"CarrierSIPFailoverRule schema","properties":{"action":{"description":"Action the API or OpenSIPS should take for this rule or decision.","enum":["retry_next","return_to_customer","block_new","terminate_all"],"example":"retry_next","type":"string"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"sip_code":{"description":"SIP response code matched by a carrier failover rule.","example":503,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"CarrierTrunk":{"description":"CarrierTrunk schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when account-level cutoff or a trunk security overlay triggers for balance or credit enforcement.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when account-level balance or credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Financial admission mode for calls. On customer accounts this is the billing source of truth; on customer trunks it is retained only as a compatibility/security overlay field.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Postpaid credit ceiling as a decimal money string. Customer account credit_limit is authoritative; customer trunk credit_limit is an optional stricter security overlay.","example":"5000.00","nullable":true},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"decimal_precision":{"default":4,"description":"Number of decimal places used for rating and money values. Supported values are 4, 5, and 6.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"gst_applicable":{"description":"Whether Australian GST applies for invoice generation. GST is not embedded in CDR cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when GST applies.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"multi_ip_mode":{"default":"load_balance","description":"How multiple gateway IPs on a carrier trunk are selected.","enum":["load_balance","failover"],"example":"load_balance","type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix prepended to DNIS for trunk identification and stripped before carrier relay.","example":"9999","nullable":true,"type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger warning notifications. Customer account threshold is authoritative; customer trunk threshold is an optional overlay.","example":"1000.00","nullable":true}},"type":"object"},"CarrierTrunkIP":{"description":"CarrierTrunkIP schema","properties":{"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"ip_address":{"description":"IPv4 or IPv6 address used for API allowlisting, SIP gateway routing, or trunk authentication.","example":"203.0.113.50","format":"byte","type":"string"},"port":{"default":5060,"description":"SIP UDP/TCP port used by a gateway or endpoint.","example":5060,"maximum":32767,"minimum":-32768,"type":"integer"},"priority":{"default":1,"description":"Gateway priority. Lower numbers are preferred first in failover mode.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"weight":{"default":1,"description":"Relative gateway weight used for weighted load balancing.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"CarrierTrunkRateDeck":{"description":"CarrierTrunkRateDeck schema","properties":{"assigned_at":{"description":"UTC timestamp when the rate deck assignment became effective for the trunk.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"}},"type":"object"},"CarrierTrunkWithIPs":{"description":"CarrierTrunkWithIPs schema","properties":{"ips":{"description":"Gateway IP records associated with the carrier trunk.","example":[],"items":{"description":"Gateway IP records associated with the carrier trunk.","example":[],"properties":{"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"ip_address":{"description":"IPv4 or IPv6 address used for API allowlisting, SIP gateway routing, or trunk authentication.","example":"203.0.113.50","format":"byte","type":"string"},"port":{"default":5060,"description":"SIP UDP/TCP port used by a gateway or endpoint.","example":5060,"maximum":32767,"minimum":-32768,"type":"integer"},"priority":{"default":1,"description":"Gateway priority. Lower numbers are preferred first in failover mode.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"weight":{"default":1,"description":"Relative gateway weight used for weighted load balancing.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"type":"array"},"trunk":{"description":"Carrier or customer trunk object returned with related child records.","example":{},"properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when account-level cutoff or a trunk security overlay triggers for balance or credit enforcement.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when account-level balance or credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Financial admission mode for calls. On customer accounts this is the billing source of truth; on customer trunks it is retained only as a compatibility/security overlay field.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Postpaid credit ceiling as a decimal money string. Customer account credit_limit is authoritative; customer trunk credit_limit is an optional stricter security overlay.","example":"5000.00","nullable":true},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"decimal_precision":{"default":4,"description":"Number of decimal places used for rating and money values. Supported values are 4, 5, and 6.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"gst_applicable":{"description":"Whether Australian GST applies for invoice generation. GST is not embedded in CDR cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when GST applies.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"multi_ip_mode":{"default":"load_balance","description":"How multiple gateway IPs on a carrier trunk are selected.","enum":["load_balance","failover"],"example":"load_balance","type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix prepended to DNIS for trunk identification and stripped before carrier relay.","example":"9999","nullable":true,"type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger warning notifications. Customer account threshold is authoritative; customer trunk threshold is an optional overlay.","example":"1000.00","nullable":true}},"type":"object"}},"type":"object"},"CheckAndReserveResult":{"description":"CheckAndReserveResult schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"headroom":{"description":"Remaining available account balance or credit after the current financial admission check.","example":"4999.7500"}},"type":"object"},"CreateBlockRuleRequest":{"description":"CreateBlockRuleRequest schema","properties":{"block_type":{"description":"Type of call attribute matched by the block rule.","enum":["ani","dnis","number_code"],"example":"dnis","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"},"match_type":{"description":"How a block rule pattern is matched against ANI or DNIS.","enum":["exact","prefix"],"example":"prefix","type":"string"},"pattern":{"description":"ANI, DNIS, number code, or prefix pattern matched by the rule.","example":"1900","type":"string"},"scope":{"description":"Operational scope where the rule applies.","enum":["global","customer_trunk","carrier_trunk","route"],"example":"global","type":"string"},"scope_id":{"description":"ID of the scoped entity. Null when scope is global.","example":10,"format":"int64","nullable":true,"type":"integer"},"sip_response_code":{"description":"Final SIP response code recorded for the call or returned by a block rule.","example":200,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"CreateCarrierSIPFailoverRuleRequest":{"description":"CreateCarrierSIPFailoverRuleRequest schema","properties":{"action":{"description":"Action the API or OpenSIPS should take for this rule or decision.","enum":["retry_next","return_to_customer","block_new","terminate_all"],"example":"retry_next","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"sip_code":{"description":"SIP response code matched by a carrier failover rule.","example":503,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"CreateCarrierTrunkRequest":{"description":"CreateCarrierTrunkRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when account-level cutoff or a trunk security overlay triggers for balance or credit enforcement.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when account-level balance or credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Financial admission mode for calls. On customer accounts this is the billing source of truth; on customer trunks it is retained only as a compatibility/security overlay field.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"type":"integer"},"credit_limit":{"description":"Postpaid credit ceiling as a decimal money string. Customer account credit_limit is authoritative; customer trunk credit_limit is an optional stricter security overlay.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"decimal_precision":{"default":4,"description":"Number of decimal places used for rating and money values. Supported values are 4, 5, and 6.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"gst_applicable":{"description":"Whether Australian GST applies for invoice generation. GST is not embedded in CDR cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when GST applies.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"multi_ip_mode":{"default":"load_balance","description":"How multiple gateway IPs on a carrier trunk are selected.","enum":["load_balance","failover"],"example":"load_balance","type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix prepended to DNIS for trunk identification and stripped before carrier relay.","example":"9999","nullable":true,"type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger warning notifications. Customer account threshold is authoritative; customer trunk threshold is an optional overlay.","example":"1000.00","nullable":true,"type":"string"}},"type":"object"},"CreateCustomerAccountRequest":{"description":"CreateCustomerAccountRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Account-level action used when auto cutoff triggers. block_new rejects new calls only; terminate_all also tears down active calls. Defaults to block_new.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off account traffic when balance or credit rules are breached.","example":false,"type":"boolean"},"billing_mode":{"default":"postpaid","description":"Account-level financial admission mode. prepaid requires available account balance before calls; postpaid allows usage up to credit_limit. Defaults to postpaid.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"can_create_sip_registrations":{"description":"Whether the customer portal user may add SIP registration credentials.","example":false,"type":"boolean"},"can_create_trunk_ips":{"description":"Whether the customer portal user may add IP auth records to their trunks.","example":false,"type":"boolean"},"can_create_trunks":{"description":"Whether the customer portal user may create new trunks under this account.","example":false,"type":"boolean"},"can_delete_sip_registrations":{"description":"Whether the customer portal user may remove SIP registration credentials.","example":false,"type":"boolean"},"can_delete_trunk_ips":{"description":"Whether the customer portal user may remove IP auth records for their trunks.","example":false,"type":"boolean"},"can_delete_trunks":{"description":"Whether the customer portal user may delete (soft-delete) trunks.","example":false,"type":"boolean"},"can_modify_sip_registrations":{"description":"Whether the customer portal user may update SIP registration credentials.","example":false,"type":"boolean"},"can_modify_trunk_ips":{"description":"Whether the customer portal user may update IP auth records for their trunks.","example":false,"type":"boolean"},"can_modify_trunks":{"description":"Whether the customer portal user may update trunk settings.","example":false,"type":"boolean"},"can_view_sip_registrations":{"description":"Whether the customer portal user may view SIP registration credentials for their trunks.","example":true,"type":"boolean"},"can_view_trunk_ips":{"description":"Whether the customer portal user may view IP auth records for their trunks.","example":true,"type":"boolean"},"can_view_trunks":{"description":"Whether the customer portal user may view trunks belonging to this account.","example":true,"type":"boolean"},"credit_limit":{"description":"Account-level postpaid credit ceiling as a decimal money string. Null means no explicit account credit ceiling.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"Default billing currency for this account. Supported values are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"gst_applicable":{"description":"Whether Australian GST applies to this account for invoice generation. GST is calculated at invoice time, not in CDR costs.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is shown on invoices when gst_applicable is true. inclusive means rates include GST; exclusive means GST is added on top.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"invoice_period_days":{"description":"Billing cycle length in days. Defaults to 30 when omitted or zero.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"name":{"description":"Human-readable wholesale customer account name used in the portal, invoices, reports, and logs.","example":"Acme Telecom","type":"string"},"password":{"description":"Portal login password for the customer account. Minimum 8 characters. Stored as a bcrypt hash; never returned in API responses.","example":"SecurePass123","minLength":8,"type":"string"},"payment_due_days":{"description":"Number of days after invoice issue before payment is due. Defaults to 7 when omitted or zero.","example":7,"maximum":32767,"minimum":1,"type":"integer"},"status":{"description":"Lifecycle state for the account. active operates normally, suspended is administratively blocked, and closed is retained as a soft-delete.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"username":{"description":"Portal login username for the customer account. The @wholesale suffix is appended automatically if not provided, ensuring domain separation from other frontend logins.","example":"cust001","type":"string"},"warning_threshold":{"description":"Account-level balance or credit threshold that should trigger warning notifications. Null disables threshold warnings.","example":"1000.00","nullable":true,"type":"string"}},"required":["currency","name","password","username"],"type":"object"},"CreateCustomerTrunkRequest":{"description":"CreateCustomerTrunkRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Auto cutoff behaviour inherited from the parent customer account. Any value provided in the request is ignored.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Auto cutoff flag inherited from the parent customer account. Any value provided in the request is ignored.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds. Defaults to 1 second when omitted or zero.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds after the initial block. Defaults to 1 second when omitted or zero.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Financial admission mode inherited from the parent customer account. Any value provided in the request is ignored.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new SIP call attempts per second accepted from this trunk. Defaults to 10 when omitted or zero.","example":10,"maximum":32767,"minimum":1,"type":"integer"},"credit_limit":{"description":"Postpaid credit ceiling as a decimal money string. Null means no explicit trunk-level credit ceiling.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"Billing currency inherited from the parent customer account. Any value provided in the request is ignored.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_account_id":{"description":"Parent customer account ID that will own this SIP ingress trunk.","example":1,"format":"int64","minimum":1,"type":"integer"},"decimal_precision":{"default":4,"description":"Decimal places used when rating customer costs. Supported values are 4, 5, and 6. Defaults to 4.","enum":[4,5,6],"example":4,"maximum":6,"minimum":4,"type":"integer"},"gst_applicable":{"description":"Whether GST applies to this trunk for invoice generation. GST is not included in CDR customer_cost values.","example":true,"type":"boolean"},"gst_treatment":{"description":"Trunk-level GST presentation for invoices when gst_applicable is true: inclusive or exclusive.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"name":{"description":"Human-readable trunk name used in portal screens, routing operations, logs, and billing reports.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window. Defaults to 300 when omitted or zero.","example":300,"maximum":32767,"minimum":1,"type":"integer"},"port_limit":{"description":"Maximum concurrent active calls allowed on this trunk. Defaults to 30 when omitted or zero.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated money values are rounded: up, down, or standard. Defaults to standard.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls on this trunk should anchor media through RTPEngine. False prefers direct media where possible.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted. False means PAI, PPI, and RPID should be normalised or treated cautiously by policy.","example":false,"type":"boolean"},"status":{"description":"Initial trunk lifecycle state. active accepts traffic, suspended blocks new calls temporarily, and closed is retained as a soft-delete. Defaults to active.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix expected before the DNIS and stripped before onward carrier relay.","example":"9999","nullable":true,"type":"string"},"warning_threshold":{"description":"Warning threshold inherited from the parent customer account. Any value provided in the request is ignored.","example":"1000.00","nullable":true,"type":"string"}},"required":["currency","customer_account_id","name"],"type":"object"},"CreateExchangeRateRequest":{"description":"CreateExchangeRateRequest schema","properties":{"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","type":"string"},"from_currency":{"description":"Source currency for an exchange rate conversion.","enum":["AUD","USD"],"example":"USD","type":"string"},"rate":{"description":"Exchange rate multiplier used to convert from source currency to target currency.","example":"1.520000","type":"string"},"to_currency":{"description":"Target currency for an exchange rate conversion.","enum":["AUD","USD"],"example":"AUD","type":"string"}},"type":"object"},"CreateLCRConfigRequest":{"description":"CreateLCRConfigRequest schema","properties":{"acceptable_loss_pct":{"description":"Decimal percentage of negative margin that may be tolerated by LCR before a route is blocked.","example":"5.00","type":"string"},"block_if_cost_exceeds_sell":{"description":"Whether LCR should block routes where carrier cost is greater than or equal to customer sell price.","example":true,"type":"boolean"},"cache_ttl_seconds":{"default":300,"description":"Number of seconds LCR or cache entries remain valid before being refreshed.","example":300,"format":"int32","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","nullable":true,"type":"integer"}},"type":"object"},"CreateOpensipsNodeRequest":{"description":"CreateOpensipsNodeRequest schema","properties":{"hostname":{"description":"Hostname of the OpenSIPS node or service instance.","example":"opensips-syd-01","type":"string"},"mi_socket_address":{"description":"OpenSIPS Management Interface socket address used by the API to push runtime changes.","example":"http://127.0.0.1:8888/mi","type":"string"},"region":{"description":"Geographic or operational region for an OpenSIPS node.","example":"sydney","nullable":true,"type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"}},"type":"object"},"CreateRateDeckRateRequest":{"description":"CreateRateDeckRateRequest schema","properties":{"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"destination_name":{"description":"Human-readable destination name for the matched rate prefix.","example":"Australia Mobile","type":"string"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","nullable":true,"type":"string"},"prefix":{"description":"Destination prefix used for longest-prefix rate matching.","example":"614","type":"string"},"rate_per_minute":{"description":"Rate value as a decimal money string in the rate deck currency. For per_minute rows this is the per-minute price; for per_call rows this is the flat per-call charge.","example":"0.0250","type":"string"},"rate_type":{"default":"per_minute","description":"Rate charging model for a prefix. per_minute uses duration and billing increments; per_call charges the value once per connected call.","enum":["per_minute","per_call"],"example":"per_minute","nullable":true,"type":"string"}},"type":"object"},"CreateRateDeckRequest":{"description":"CreateRateDeckRequest schema","properties":{"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"deck_type":{"description":"Rate deck type indicating whether rates are customer sell rates or carrier cost rates.","enum":["customer","carrier"],"example":"customer","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"}},"type":"object"},"CreateRateDeckUpdateRequest":{"description":"CreateRateDeckUpdateRequest schema","properties":{"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","type":"string"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"},"update_type":{"description":"Rate deck update type indicating whether the change is additive or a full replacement.","enum":["incremental","full_replace"],"example":"full_replace","type":"string"}},"type":"object"},"CreateRequest":{"description":"CreateRequest schema","properties":{"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","type":"string"},"ip_address":{"description":"IPv4 or IPv6 address used for API allowlisting, SIP gateway routing, or trunk authentication.","example":"203.0.113.50","type":"string"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"}},"type":"object"},"CreateTrunkIPAuthRequest":{"description":"CreateTrunkIPAuthRequest schema","properties":{"cidr":{"description":"IPv4 or IPv6 CIDR range authorised for trunk IP authentication.","example":"203.0.113.0/24","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"trunk_id":{"description":"ID of the referenced balance entity. For trunk_type=customer this is the customer_account_id; for trunk_type=carrier this is the carrier_trunk_id.","example":10,"format":"int64","type":"integer"},"trunk_type":{"description":"Type of balance entity referenced by the record. customer means customer account-level billing; carrier means carrier trunk-level billing.","enum":["customer","carrier"],"example":"customer","type":"string"}},"type":"object"},"CreateTrunkSIPRegistrationRequest":{"description":"CreateTrunkSIPRegistrationRequest schema","properties":{"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"domain":{"description":"SIP authentication realm or domain for endpoint registration.","example":"sip.crazytel.net.au","type":"string"},"hash_algorithm":{"description":"Digest hash algorithm used for stored SIP registration credentials.","enum":["md5","sha256"],"example":"md5","type":"string"},"password_hash":{"description":"Stored SIP digest password hash. This is credential material and should not be exposed unnecessarily.","example":"5f4dcc3b5aa765d61d8327deb882cf99","type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"username":{"description":"SIP username or API-auth username associated with the request.","example":"cust001","type":"string"}},"type":"object"},"CreditRequest":{"description":"CreditRequest schema","properties":{"amount":{"description":"Decimal money amount for the transaction or balance operation. Currency is provided separately.","example":"100.00","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","type":"string"}},"type":"object"},"CustomerAccount":{"description":"CustomerAccount schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Account-level action used when auto cutoff triggers. block_new rejects new calls only; terminate_all also tears down active calls.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off account traffic when balance or credit rules are breached.","example":false,"type":"boolean"},"billing_mode":{"default":"postpaid","description":"Account-level financial admission mode. prepaid requires available account balance before calls; postpaid allows usage up to the account credit_limit.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"can_create_sip_registrations":{"description":"Whether the customer portal user may add SIP registration credentials.","example":false,"type":"boolean"},"can_create_trunk_ips":{"description":"Whether the customer portal user may add IP auth records to their trunks.","example":false,"type":"boolean"},"can_create_trunks":{"description":"Whether the customer portal user may create new trunks under this account.","example":false,"type":"boolean"},"can_delete_sip_registrations":{"description":"Whether the customer portal user may remove SIP registration credentials.","example":false,"type":"boolean"},"can_delete_trunk_ips":{"description":"Whether the customer portal user may remove IP auth records for their trunks.","example":false,"type":"boolean"},"can_delete_trunks":{"description":"Whether the customer portal user may delete (soft-delete) trunks.","example":false,"type":"boolean"},"can_modify_sip_registrations":{"description":"Whether the customer portal user may update SIP registration credentials.","example":false,"type":"boolean"},"can_modify_trunk_ips":{"description":"Whether the customer portal user may update IP auth records for their trunks.","example":false,"type":"boolean"},"can_modify_trunks":{"description":"Whether the customer portal user may update trunk settings.","example":false,"type":"boolean"},"can_view_sip_registrations":{"description":"Whether the customer portal user may view SIP registration credentials for their trunks.","example":true,"type":"boolean"},"can_view_trunk_ips":{"description":"Whether the customer portal user may view IP auth records for their trunks.","example":true,"type":"boolean"},"can_view_trunks":{"description":"Whether the customer portal user may view trunks belonging to this account.","example":true,"type":"boolean"},"created_at":{"description":"UTC timestamp when the account was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Account-level postpaid credit ceiling as a decimal money string. Null means no explicit credit ceiling.","example":"5000.00","nullable":true},"currency":{"description":"Default billing currency for this customer account. Supported values are AUD and USD. Trunks under the account should use the same currency.","enum":["AUD","USD"],"example":"AUD","type":"string"},"gst_applicable":{"description":"Whether Australian GST applies to this customer for invoice generation. GST is not included in CDR cost fields and is calculated at invoice time.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when gst_applicable is true. inclusive means prices include GST; exclusive means GST is added on top at invoice time.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the customer account. Use this value when creating customer trunks or querying account-specific billing data.","example":1,"format":"int64","type":"integer"},"invoice_period_days":{"description":"Length of the customer billing cycle in days. For example, 30 means invoices are generated roughly monthly.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"name":{"description":"Human-readable wholesale customer name shown in the portal, invoices, reports, and operational logs.","example":"Acme Telecom","type":"string"},"payment_due_days":{"description":"Number of days after invoice issue before payment is due and the invoice becomes overdue.","example":7,"maximum":32767,"minimum":1,"type":"integer"},"status":{"description":"Lifecycle state of the customer account. active can operate normally, suspended is administratively blocked, and closed is a soft-delete retained for audit and CDR history.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"updated_at":{"description":"UTC timestamp when the account was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"username":{"description":"Portal login username for the customer account. Always stored with @wholesale suffix for domain separation from other frontend logins.","example":"cust001","nullable":true,"type":"string"},"warning_threshold":{"description":"Account-level balance or credit threshold that should trigger customer warning notifications. Null disables threshold warnings.","example":"1000.00","nullable":true}},"type":"object"},"CustomerAccountWithTrunks":{"description":"CustomerAccountWithTrunks schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Account-level action used when auto cutoff triggers. block_new rejects new calls only; terminate_all also tears down active calls.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off account traffic when balance or credit rules are breached.","example":false,"type":"boolean"},"billing_mode":{"default":"postpaid","description":"Account-level financial admission mode. prepaid requires available account balance before calls; postpaid allows usage up to the account credit_limit.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"can_create_sip_registrations":{"description":"Whether the customer portal user may add SIP registration credentials.","example":false,"type":"boolean"},"can_create_trunk_ips":{"description":"Whether the customer portal user may add IP auth records to their trunks.","example":false,"type":"boolean"},"can_create_trunks":{"description":"Whether the customer portal user may create new trunks under this account.","example":false,"type":"boolean"},"can_delete_sip_registrations":{"description":"Whether the customer portal user may remove SIP registration credentials.","example":false,"type":"boolean"},"can_delete_trunk_ips":{"description":"Whether the customer portal user may remove IP auth records for their trunks.","example":false,"type":"boolean"},"can_delete_trunks":{"description":"Whether the customer portal user may delete (soft-delete) trunks.","example":false,"type":"boolean"},"can_modify_sip_registrations":{"description":"Whether the customer portal user may update SIP registration credentials.","example":false,"type":"boolean"},"can_modify_trunk_ips":{"description":"Whether the customer portal user may update IP auth records for their trunks.","example":false,"type":"boolean"},"can_modify_trunks":{"description":"Whether the customer portal user may update trunk settings.","example":false,"type":"boolean"},"can_view_sip_registrations":{"description":"Whether the customer portal user may view SIP registration credentials for their trunks.","example":true,"type":"boolean"},"can_view_trunk_ips":{"description":"Whether the customer portal user may view IP auth records for their trunks.","example":true,"type":"boolean"},"can_view_trunks":{"description":"Whether the customer portal user may view trunks belonging to this account.","example":true,"type":"boolean"},"created_at":{"description":"UTC timestamp when the account was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Account-level postpaid credit ceiling as a decimal money string. Null means no explicit credit ceiling.","example":"5000.00","nullable":true},"currency":{"description":"Default billing currency for this customer account. Supported values are AUD and USD. Trunks under the account should use the same currency.","enum":["AUD","USD"],"example":"AUD","type":"string"},"gst_applicable":{"description":"Whether Australian GST applies to this customer for invoice generation. GST is not included in CDR cost fields and is calculated at invoice time.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when gst_applicable is true. inclusive means prices include GST; exclusive means GST is added on top at invoice time.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the customer account. Use this value when creating customer trunks or querying account-specific billing data.","example":1,"format":"int64","type":"integer"},"invoice_period_days":{"description":"Length of the customer billing cycle in days. For example, 30 means invoices are generated roughly monthly.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"name":{"description":"Human-readable wholesale customer name shown in the portal, invoices, reports, and operational logs.","example":"Acme Telecom","type":"string"},"payment_due_days":{"description":"Number of days after invoice issue before payment is due and the invoice becomes overdue.","example":7,"maximum":32767,"minimum":1,"type":"integer"},"settings":{"description":"Editable account-level billing and tax settings grouped for frontend forms.","example":{"billing_mode":"postpaid","credit_limit":"5000.00","currency":"AUD","gst_applicable":true,"gst_treatment":"exclusive","invoice_period_days":30,"payment_due_days":7},"properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Account-level automatic cutoff action duplicated for frontend settings forms.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether account-level automatic cutoff is enabled.","example":false,"type":"boolean"},"billing_mode":{"default":"postpaid","description":"Account-level financial admission mode duplicated for frontend settings forms.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"credit_limit":{"description":"Account-level postpaid credit ceiling duplicated for frontend settings forms.","example":"5000.00","nullable":true},"currency":{"description":"Account-level billing currency duplicated for frontend settings forms. Supported values are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"gst_applicable":{"description":"Whether GST applies to this account for invoice generation.","example":true,"type":"boolean"},"gst_treatment":{"description":"Invoice GST presentation for the account: inclusive or exclusive.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"invoice_period_days":{"description":"Billing cycle length in days, duplicated for frontend settings forms.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"payment_due_days":{"description":"Days after invoice issue before payment is due, duplicated for frontend settings forms.","example":7,"maximum":32767,"minimum":1,"type":"integer"},"warning_threshold":{"description":"Account-level warning threshold duplicated for frontend settings forms.","example":"1000.00","nullable":true}},"type":"object"},"status":{"description":"Lifecycle state of the customer account. active can operate normally, suspended is administratively blocked, and closed is a soft-delete retained for audit and CDR history.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"trunks":{"description":"Customer SIP ingress trunks that belong to this account. Each trunk controls SIP admission, CPS/port limits, billing mode, and routing options.","items":{"description":"Customer SIP ingress trunks that belong to this account. Each trunk controls SIP admission, CPS/port limits, billing mode, and routing options.","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when auto cutoff triggers. block_new rejects new calls only; terminate_all also tears down existing active calls.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when prepaid balance or postpaid credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds. For example, 60 means the first minute is charged even for shorter answered calls.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds after the initial block. For example, 30 means duration rounds to the next 30-second boundary after the initial block.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_mode":{"default":"postpaid","description":"How calls are financially admitted. prepaid requires available balance before calls; postpaid allows usage up to the configured credit_limit.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new SIP call attempts per second accepted from this trunk. OpenSIPS enforces this at INVITE admission time.","example":10,"maximum":32767,"minimum":1,"type":"integer"},"created_at":{"description":"UTC timestamp when the trunk was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Postpaid credit ceiling for this trunk as a decimal money string. Null means no explicit trunk-level credit ceiling.","example":"5000.00","nullable":true},"currency":{"description":"Billing currency for this trunk. Supported values are AUD and USD. Should normally match the parent customer account currency.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_account_id":{"description":"Parent customer account ID that owns this SIP ingress trunk.","example":1,"format":"int64","minimum":1,"type":"integer"},"decimal_precision":{"default":4,"description":"Number of decimal places used when rating this trunk's customer costs. Supported operational range is 4 to 6 decimal places.","enum":[4,5,6],"example":4,"maximum":6,"minimum":4,"type":"integer"},"gst_applicable":{"description":"Whether GST applies to this trunk for invoice generation. GST is not included in CDR customer_cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"Trunk-level GST presentation for invoices when gst_applicable is true. inclusive means rates include GST; exclusive means GST is added at invoice time.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for this customer trunk.","example":1,"format":"int64","type":"integer"},"name":{"description":"Human-readable trunk name shown in the portal, routing screens, logs, and billing reports.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window for this trunk.","example":300,"maximum":32767,"minimum":1,"type":"integer"},"port_limit":{"description":"Maximum concurrent active calls allowed on this trunk. A port is one active call slot.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded: up always rounds away from zero, down truncates, and standard uses normal half-up rounding.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls on this trunk should anchor media through RTPEngine. False means direct media is preferred when possible.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted. False means PAI, PPI, and RPID should be treated cautiously or normalised by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the trunk. active accepts traffic, suspended blocks new calls temporarily, and closed is a soft-delete retained for CDR history.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix expected before the DNIS. The API/OpenSIPS can use it to identify routing context and strip it before onward carrier relay.","example":"9999","nullable":true,"type":"string"},"updated_at":{"description":"UTC timestamp when the trunk was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger customer warning notifications. Null disables threshold warnings.","example":"1000.00","nullable":true}},"type":"object"},"type":"array"},"updated_at":{"description":"UTC timestamp when the account was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"username":{"description":"Portal login username for the customer account. Always stored with @wholesale suffix for domain separation from other frontend logins.","example":"cust001","nullable":true,"type":"string"},"warning_threshold":{"description":"Account-level balance or credit threshold that should trigger customer warning notifications. Null disables threshold warnings.","example":"1000.00","nullable":true}},"type":"object"},"CustomerTrunk":{"description":"CustomerTrunk schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when auto cutoff triggers. block_new rejects new calls only; terminate_all also tears down existing active calls.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when prepaid balance or postpaid credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds. For example, 60 means the first minute is charged even for shorter answered calls.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds after the initial block. For example, 30 means duration rounds to the next 30-second boundary after the initial block.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_mode":{"default":"postpaid","description":"How calls are financially admitted. prepaid requires available balance before calls; postpaid allows usage up to the configured credit_limit.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new SIP call attempts per second accepted from this trunk. OpenSIPS enforces this at INVITE admission time.","example":10,"maximum":32767,"minimum":1,"type":"integer"},"created_at":{"description":"UTC timestamp when the trunk was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"credit_limit":{"description":"Postpaid credit ceiling for this trunk as a decimal money string. Null means no explicit trunk-level credit ceiling.","example":"5000.00","nullable":true},"currency":{"description":"Billing currency for this trunk. Supported values are AUD and USD. Should normally match the parent customer account currency.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_account_id":{"description":"Parent customer account ID that owns this SIP ingress trunk.","example":1,"format":"int64","minimum":1,"type":"integer"},"decimal_precision":{"default":4,"description":"Number of decimal places used when rating this trunk's customer costs. Supported operational range is 4 to 6 decimal places.","enum":[4,5,6],"example":4,"maximum":6,"minimum":4,"type":"integer"},"gst_applicable":{"description":"Whether GST applies to this trunk for invoice generation. GST is not included in CDR customer_cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"Trunk-level GST presentation for invoices when gst_applicable is true. inclusive means rates include GST; exclusive means GST is added at invoice time.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for this customer trunk.","example":1,"format":"int64","type":"integer"},"name":{"description":"Human-readable trunk name shown in the portal, routing screens, logs, and billing reports.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window for this trunk.","example":300,"maximum":32767,"minimum":1,"type":"integer"},"port_limit":{"description":"Maximum concurrent active calls allowed on this trunk. A port is one active call slot.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded: up always rounds away from zero, down truncates, and standard uses normal half-up rounding.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls on this trunk should anchor media through RTPEngine. False means direct media is preferred when possible.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted. False means PAI, PPI, and RPID should be treated cautiously or normalised by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the trunk. active accepts traffic, suspended blocks new calls temporarily, and closed is a soft-delete retained for CDR history.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix expected before the DNIS. The API/OpenSIPS can use it to identify routing context and strip it before onward carrier relay.","example":"9999","nullable":true,"type":"string"},"updated_at":{"description":"UTC timestamp when the trunk was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger customer warning notifications. Null disables threshold warnings.","example":"1000.00","nullable":true}},"type":"object"},"CustomerTrunkRateDeck":{"description":"CustomerTrunkRateDeck schema","properties":{"assigned_at":{"description":"UTC timestamp when the rate deck assignment became effective for the trunk.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"}},"type":"object"},"DebitRequest":{"description":"DebitRequest schema","properties":{"amount":{"description":"Decimal money amount for the transaction or balance operation. Currency is provided separately.","example":"100.00","type":"string"},"cdr_id":{"description":"Call Detail Record ID linked to this transaction or adjustment.","example":1001,"format":"int64","nullable":true,"type":"integer"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","type":"string"}},"type":"object"},"ErrorResponse":{"description":"ErrorResponse schema","properties":{"code":{"description":"Standard HTTP reason phrase for the status code.","example":"Bad Request","type":"string"},"error":{"description":"Human-readable explanation of why the request failed.","example":"invalid request","type":"string"},"status":{"description":"HTTP response status code defined by RFC 9110 semantics.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"integer"}},"type":"object"},"ExchangeRate":{"description":"ExchangeRate schema","properties":{"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"created_by":{"description":"Operator, system user, or process that created the record.","example":"admin@crazytel.com.au","format":"int64","nullable":true,"type":"integer"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","format":"date-time","type":"string"},"from_currency":{"description":"Source currency for an exchange rate conversion.","enum":["AUD","USD"],"example":"USD","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"rate":{"description":"Exchange rate multiplier used to convert from source currency to target currency.","example":"1.520000"},"to_currency":{"description":"Target currency for an exchange rate conversion.","enum":["AUD","USD"],"example":"AUD","type":"string"}},"type":"object"},"FailoverRequest":{"description":"FailoverRequest schema","properties":{"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"failure_code":{"description":"SIP response code from a failed carrier attempt used to determine failover behaviour.","example":503,"type":"integer"}},"type":"object"},"FailoverResponse":{"description":"FailoverResponse schema","properties":{"action":{"description":"Action the API or OpenSIPS should take for this rule or decision.","enum":["retry_next","return_to_customer","block_new","terminate_all"],"example":"retry_next","type":"string"}},"type":"object"},"HealthResponse":{"description":"HealthResponse schema","properties":{"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"valkey":{"description":"Valkey cache health status reported by the API health check.","enum":["ok","disabled","unreachable"],"example":"ok","type":"string"}},"type":"object"},"IngestCDRRequest":{"description":"IngestCDRRequest schema","properties":{"ani":{"description":"Originating phone number, also known as caller ID, used for routing, blocking, and CDR reporting.","example":"61400111222","type":"string"},"answer_time":{"description":"UTC timestamp when the call was answered. Null or omitted means the call was not answered.","example":"2026-01-15T08:30:12Z","nullable":true,"type":"string"},"carrier_gateway_id":{"description":"Database ID of the selected carrier gateway IP row used for this call attempt.","example":5,"format":"int64","type":"integer"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"},"duration_seconds":{"description":"Total answered call duration in seconds before billing increment rounding.","example":73,"type":"integer"},"end_time":{"description":"UTC timestamp when the call ended.","example":"2026-01-15T08:31:13Z","type":"string"},"sip_call_id":{"description":"SIP Call-ID for correlating signalling, CDRs, and balance transactions.","example":"a84b4c76e66710@example.net","type":"string"},"sip_response_code":{"description":"Final SIP response code recorded for the call or returned by a block rule.","example":200,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"start_time":{"description":"UTC timestamp when the call attempt started.","example":"2026-01-15T08:30:00Z","type":"string"}},"type":"object"},"IngestResult":{"description":"IngestResult schema","properties":{"adjustment":{"description":"Balance adjustment amount applied during rating, reconciliation, or manual correction.","example":"-0.1250","nullable":true},"cdr_id":{"description":"Call Detail Record ID linked to this transaction or adjustment.","example":1001,"format":"int64","type":"integer"},"customer_cost":{"description":"Amount charged to the customer for the call as a decimal money string.","example":"0.0250"},"customer_currency":{"description":"Currency used for customer-side call charges.","enum":["AUD","USD"],"example":"AUD","type":"string"},"reconciled":{"description":"Whether CDR rating and balance reconciliation completed successfully.","example":true,"type":"boolean"},"vendor_cost":{"description":"Amount charged by the carrier for the call as a decimal money string.","example":"0.0200"},"vendor_currency":{"description":"Currency used for vendor-side call costs.","enum":["AUD","USD"],"example":"AUD","type":"string"}},"type":"object"},"LCRConfig":{"description":"LCRConfig schema","properties":{"acceptable_loss_pct":{"description":"Decimal percentage of negative margin that may be tolerated by LCR before a route is blocked.","example":"5.00"},"block_if_cost_exceeds_sell":{"description":"Whether LCR should block routes where carrier cost is greater than or equal to customer sell price.","example":true,"type":"boolean"},"cache_ttl_seconds":{"default":300,"description":"Number of seconds LCR or cache entries remain valid before being refreshed.","example":300,"format":"int32","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","nullable":true,"type":"integer"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"LCRLookupRequest":{"description":"LCRLookupRequest schema","properties":{"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"}},"type":"object"},"LCRResult":{"description":"LCRResult schema","properties":{"carriers":{"description":"Carrier route candidates considered by the LCR engine, including cost, margin, and allow/block reason.","example":[],"items":{"description":"Carrier route candidates considered by the LCR engine, including cost, margin, and allow/block reason.","example":[],"properties":{"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"carrier_name":{"type":"string"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"converted_cost":{},"margin_ok":{"type":"boolean"},"original_cost":{},"original_currency":{"type":"string"},"original_rate_type":{"default":"per_minute","description":"Carrier rate charging model before currency conversion: per_minute or per_call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"}},"type":"object"},"type":"array"},"config":{"description":"LCR configuration applied to the lookup, including margin protection and cache behaviour.","example":{},"properties":{"acceptable_loss_pct":{"description":"Decimal percentage of negative margin that may be tolerated by LCR before a route is blocked.","example":"5.00"},"block_if_cost_exceeds_sell":{"description":"Whether LCR should block routes where carrier cost is greater than or equal to customer sell price.","example":true,"type":"boolean"},"cache_ttl_seconds":{"default":300,"description":"Number of seconds LCR or cache entries remain valid before being refreshed.","example":300,"format":"int32","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","nullable":true,"type":"integer"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"customer_billing_increment_initial":{"maximum":32767,"minimum":-32768,"type":"integer"},"customer_billing_increment_step":{"maximum":32767,"minimum":-32768,"type":"integer"},"customer_currency":{"description":"Currency used for customer-side call charges.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_rate":{"description":"Customer sell rate per minute that matched the destination during LCR lookup.","example":"0.0250"},"customer_rate_type":{"default":"per_minute","description":"Customer rate charging model for the matched prefix: per_minute or per_call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"},"no_match_reason":{"description":"Reason no LCR carrier route matched or was allowed.","example":"no_active_rate_match","nullable":true,"type":"string"}},"type":"object"},"LoginRequest":{"description":"LoginRequest schema","properties":{"password":{"description":"Portal login password. Minimum 8 characters. Compared against the bcrypt hash stored on the customer account.","example":"SecurePass123","type":"string"},"username":{"description":"Portal login username. The @wholesale suffix is appended automatically if not provided, matching the stored customer account username.","example":"cust001","type":"string"}},"type":"object"},"LoginResponse":{"description":"LoginResponse schema","properties":{"currency":{"description":"Default billing currency for this account.","enum":["AUD","USD"],"example":"AUD","type":"string"},"id":{"description":"Unique database identifier for the authenticated customer account.","example":1,"format":"int64","type":"integer"},"message":{"description":"Human-readable result of the login attempt.","example":"operation successful","type":"string"},"name":{"description":"Human-readable wholesale customer account name.","example":"Acme Telecom","type":"string"},"status":{"description":"Lifecycle state of the customer account.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"token":{"description":"Bearer token to be sent in the Authorization header for subsequent customer portal API requests. Expires after 24 hours.","example":"a1b2c3d4e5f6789012345678abcdef0123456789","type":"string"},"username":{"description":"Canonical portal login username with @wholesale suffix.","example":"cust001","type":"string"}},"type":"object"},"MessageResponse":{"description":"MessageResponse schema","properties":{"message":{"description":"Human-readable operation result message.","example":"operation successful","type":"string"}},"type":"object"},"OpensipsAdmitRequest":{"description":"OpensipsAdmitRequest schema","properties":{"call_id":{"description":"SIP Call-ID used to correlate signalling, CDRs, balance transactions, and troubleshooting logs.","example":"a84b4c76e66710@example.net","nullable":true,"type":"string"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","nullable":true,"type":"string"},"estimated_cost":{"description":"Estimated maximum call cost reserved or checked before allowing the call.","example":"0.2500","type":"string"}},"type":"object"},"OpensipsAdmitResponse":{"description":"OpensipsAdmitResponse schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"headroom":{"description":"Remaining available account balance or credit after the current financial admission check.","example":"4999.7500","nullable":true,"type":"string"},"reject_code":{"description":"SIP response code OpenSIPS should return when a call or registration is rejected.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Human-readable reason a call, registration, or route was rejected.","example":"insufficient_balance","nullable":true,"type":"string"}},"type":"object"},"OpensipsAuthRequest":{"description":"OpensipsAuthRequest schema","properties":{"ani":{"description":"Originating phone number, also known as caller ID, used for routing, blocking, and CDR reporting.","example":"61400111222","type":"string"},"call_id":{"description":"SIP Call-ID used to correlate signalling, CDRs, balance transactions, and troubleshooting logs.","example":"a84b4c76e66710@example.net","type":"string"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"},"domain":{"description":"SIP authentication realm or domain for endpoint registration.","example":"sip.crazytel.net.au","nullable":true,"type":"string"},"from":{"description":"Raw SIP From header value supplied by OpenSIPS during auth or routing checks.","example":"\u003csip:61400111222@example.net\u003e","type":"string"},"pai":{"description":"P-Asserted-Identity SIP header value supplied by the endpoint or upstream proxy.","example":"\u003csip:61400111222@example.net\u003e","nullable":true,"type":"string"},"ppi":{"description":"P-Preferred-Identity SIP header value supplied by the endpoint.","example":"\u003csip:61400111222@example.net\u003e","nullable":true,"type":"string"},"privacy":{"description":"SIP Privacy header value used when evaluating identity handling.","example":"none","nullable":true,"type":"string"},"rpid":{"description":"Remote-Party-ID SIP identity header value supplied by the endpoint.","example":"\u003csip:61400111222@example.net\u003e","nullable":true,"type":"string"},"ruri":{"description":"SIP Request-URI received by OpenSIPS.","example":"sip:61400123456@sip.crazytel.net.au","type":"string"},"source_ip":{"description":"Source IP address observed by OpenSIPS or the API middleware.","example":"203.0.113.10","type":"string"},"to":{"description":"Raw SIP To header value supplied by OpenSIPS during auth or routing checks.","example":"\u003csip:61400123456@example.net\u003e","type":"string"},"username":{"description":"SIP username or API-auth username associated with the request.","example":"cust001","nullable":true,"type":"string"}},"type":"object"},"OpensipsAuthResponse":{"description":"OpensipsAuthResponse schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"ani":{"description":"Originating phone number, also known as caller ID, used for routing, blocking, and CDR reporting.","example":"61400111222","nullable":true,"type":"string"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","nullable":true,"type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","nullable":true,"type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"reject_code":{"description":"SIP response code OpenSIPS should return when a call or registration is rejected.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Human-readable reason a call, registration, or route was rejected.","example":"insufficient_balance","nullable":true,"type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"},"trunk_type":{"description":"Type of balance entity referenced by the record. customer means customer account-level billing; carrier means carrier trunk-level billing.","enum":["customer","carrier"],"example":"customer","nullable":true,"type":"string"}},"type":"object"},"OpensipsNode":{"description":"OpensipsNode schema","properties":{"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"hostname":{"description":"Hostname of the OpenSIPS node or service instance.","example":"opensips-syd-01","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"last_seen_at":{"description":"UTC timestamp when the OpenSIPS node was last observed by the control plane.","example":"2026-01-15T08:30:00Z","format":"date-time","nullable":true,"type":"string"},"mi_socket_address":{"description":"OpenSIPS Management Interface socket address used by the API to push runtime changes.","example":"http://127.0.0.1:8888/mi","type":"string"},"region":{"description":"Geographic or operational region for an OpenSIPS node.","example":"sydney","nullable":true,"type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"OpensipsRouteRequest":{"description":"OpensipsRouteRequest schema","properties":{"ani":{"description":"Originating phone number, also known as caller ID, used for routing, blocking, and CDR reporting.","example":"61400111222","type":"string"},"attempt":{"description":"Routing attempt number for this call. The first carrier selection attempt is 1.","example":1,"type":"integer"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled number, also known as destination number, used for rate lookup, block rules, and routing.","example":"61400123456","type":"string"},"previous_carrier_trunk_id":{"description":"Carrier trunk ID used on the previous failed routing attempt, if any.","example":20,"format":"int64","nullable":true,"type":"integer"},"previous_failure_code":{"description":"SIP response code from the previous failed routing attempt, if any.","example":503,"nullable":true,"type":"integer"},"previous_gateway_id":{"description":"Carrier gateway ID used on the previous failed routing attempt, if any.","example":5,"format":"int64","nullable":true,"type":"integer"},"tried_carrier_trunk_ids":{"description":"Carrier trunk IDs already attempted for this call and excluded from the next failover choice.","example":[20,21],"items":{"description":"Carrier trunk IDs already attempted for this call and excluded from the next failover choice.","example":[20,21],"format":"int64","nullable":true,"type":"integer"},"nullable":true,"type":"array"},"tried_gateway_ids":{"description":"Carrier gateway IDs already attempted for this call and excluded from the next failover choice.","example":[5,6],"items":{"description":"Carrier gateway IDs already attempted for this call and excluded from the next failover choice.","example":[5,6],"format":"int64","nullable":true,"type":"integer"},"nullable":true,"type":"array"}},"type":"object"},"OpensipsRouteResponse":{"description":"OpensipsRouteResponse schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"carrier_gateway_id":{"description":"Database ID of the selected carrier gateway IP row used for this call attempt.","example":5,"format":"int64","nullable":true,"type":"integer"},"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","nullable":true,"type":"integer"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","nullable":true,"type":"integer"},"destination_uri":{"description":"SIP URI that OpenSIPS should use as the outbound destination for the selected route.","example":"sip:61400123456@203.0.113.50:5060","nullable":true,"type":"string"},"outbound_dnis":{"description":"DNIS sent to the selected carrier after tech prefix stripping or normalisation.","example":"61400123456","nullable":true,"type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"reject_code":{"description":"SIP response code OpenSIPS should return when a call or registration is rejected.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Human-readable reason a call, registration, or route was rejected.","example":"insufficient_balance","nullable":true,"type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"}},"type":"object"},"OpensipsRoutingSyncResponse":{"description":"OpensipsRoutingSyncResponse schema","properties":{"carriers":{"description":"Carrier route candidates considered by the LCR engine, including cost, margin, and allow/block reason.","example":[],"type":"integer"},"gateways":{"description":"Gateway records included in an OpenSIPS routing sync response.","example":[],"type":"integer"},"groups":{"description":"Routing group records included in an OpenSIPS routing sync response.","example":[],"type":"integer"},"reload_results":{"description":"Results from OpenSIPS reload commands issued after a routing sync.","example":[],"items":{"description":"Results from OpenSIPS reload commands issued after a routing sync.","example":[],"properties":{"error":{"nullable":true,"type":"string"},"hostname":{"description":"Hostname of the OpenSIPS node or service instance.","example":"opensips-syd-01","type":"string"},"node_id":{"format":"int64","type":"integer"},"success":{"type":"boolean"}},"type":"object"},"type":"array"},"rules":{"description":"Routing or failover rules included in the response.","example":[],"type":"integer"},"warnings":{"description":"Non-fatal warnings produced by the operation.","example":[],"items":{"description":"Non-fatal warnings produced by the operation.","example":[],"nullable":true,"type":"string"},"nullable":true,"type":"array"}},"type":"object"},"PerMinuteRateLimitRequest":{"description":"PerMinuteRateLimitRequest schema","properties":{"limit":{"description":"Maximum allowed counter value or maximum number of records to return.","example":100,"format":"int64","type":"integer"},"trunk_id":{"description":"ID of the referenced balance entity. For trunk_type=customer this is the customer_account_id; for trunk_type=carrier this is the carrier_trunk_id.","example":10,"format":"int64","type":"integer"}},"type":"object"},"PerMinuteRateLimitResponse":{"description":"PerMinuteRateLimitResponse schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"current":{"description":"Current counter value at the time of the check.","example":4,"format":"int64","type":"integer"},"limit":{"description":"Maximum allowed counter value or maximum number of records to return.","example":100,"format":"int64","type":"integer"}},"type":"object"},"RateDeck":{"description":"RateDeck schema","properties":{"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"deck_type":{"description":"Rate deck type indicating whether rates are customer sell rates or carrier cost rates.","enum":["customer","carrier"],"example":"customer","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"}},"type":"object"},"RateDeckCarrierAllowlist":{"description":"RateDeckCarrierAllowlist schema","properties":{"carrier_trunk_id":{"description":"Carrier trunk ID for the upstream SIP endpoint used for egress routing.","example":20,"format":"int64","type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"}},"type":"object"},"RateDeckRate":{"description":"RateDeckRate schema","properties":{"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"destination_name":{"description":"Human-readable destination name for the matched rate prefix.","example":"Australia Mobile","type":"string"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"prefix":{"description":"Destination prefix used for longest-prefix rate matching.","example":"614","type":"string"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"},"rate_per_minute":{"description":"Rate value as a decimal money string in the rate deck currency. For per_minute rows this is the per-minute price; for per_call rows this is the flat per-call charge.","example":"0.0250"},"rate_type":{"default":"per_minute","description":"Rate charging model for a prefix. per_minute uses duration and billing increments; per_call charges the value once per connected call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"}},"type":"object"},"RateDeckUpdate":{"description":"RateDeckUpdate schema","properties":{"activated_at":{"description":"UTC timestamp when the record or scheduled update became active.","example":"2026-01-15T08:30:00Z","format":"date-time","nullable":true,"type":"string"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"created_by":{"description":"Operator, system user, or process that created the record.","example":"admin@crazytel.com.au","format":"int64","nullable":true,"type":"integer"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","format":"date-time","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"update_type":{"description":"Rate deck update type indicating whether the change is additive or a full replacement.","enum":["incremental","full_replace"],"example":"full_replace","type":"string"}},"type":"object"},"RateLookupRequest":{"description":"RateLookupRequest schema","properties":{"destination":{"description":"Dialled destination number or prefix used for rate lookup and routing.","example":"61400123456","type":"string"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"}},"type":"object"},"RateLookupResponse":{"description":"RateLookupResponse schema","properties":{"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"destination_name":{"description":"Human-readable destination name for the matched rate prefix.","example":"Australia Mobile","type":"string"},"prefix":{"description":"Destination prefix used for longest-prefix rate matching.","example":"614","type":"string"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","type":"integer"},"rate_per_minute":{"description":"Rate value as a decimal money string in the rate deck currency. For per_minute rows this is the per-minute price; for per_call rows this is the flat per-call charge.","example":"0.0250"},"rate_type":{"default":"per_minute","description":"Rate charging model for a prefix. per_minute uses duration and billing increments; per_call charges the value once per connected call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"}},"type":"object"},"RegisterAuthRequest":{"description":"RegisterAuthRequest schema","properties":{"auth_response":{"description":"SIP digest Authorization or Proxy-Authorization response hash supplied by the endpoint.","example":"6629fae49393a05397450978507c4ef1","type":"string"},"method":{"description":"SIP method being authenticated or processed.","enum":["REGISTER","INVITE","OPTIONS"],"example":"REGISTER","type":"string"},"realm":{"description":"SIP digest authentication realm.","example":"sip.crazytel.net.au","type":"string"},"source_ip":{"description":"Source IP address observed by OpenSIPS or the API middleware.","example":"203.0.113.10","type":"string"},"uri":{"description":"SIP URI used in digest authentication verification.","example":"sip:sip.crazytel.net.au","type":"string"},"username":{"description":"SIP username or API-auth username associated with the request.","example":"cust001","type":"string"}},"type":"object"},"RegisterAuthResponse":{"description":"RegisterAuthResponse schema","properties":{"allowed":{"description":"Whether the requested call, route, registration, or operation is allowed to proceed.","example":true,"type":"boolean"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","nullable":true,"type":"integer"},"reject_code":{"description":"SIP response code OpenSIPS should return when a call or registration is rejected.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Human-readable reason a call, registration, or route was rejected.","example":"insufficient_balance","nullable":true,"type":"string"}},"type":"object"},"RouteSimulationRequest":{"description":"RouteSimulationRequest schema","properties":{"ani":{"description":"Originating phone number to test against ANI block rules and routing logic.","example":"61400111222","type":"string"},"customer_account_id":{"description":"Optional customer account ID selected in the admin portal. When supplied, the simulator verifies the chosen customer trunk belongs to this account.","example":1,"format":"int64","nullable":true,"type":"integer"},"customer_trunk_id":{"description":"Customer ingress trunk ID to simulate. The trunk controls rate deck assignment, tech prefix stripping, block-rule scope, and CPS/port limits. Balance admission is controlled by the parent customer account.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Dialled destination number to test against DNIS block rules, customer rate lookup, LCR, and carrier rate lookup.","example":"61400123456","type":"string"},"duration_seconds":{"description":"Answered call duration in seconds used to estimate retail and wholesale costs. Defaults to 60 seconds when omitted.","example":73,"nullable":true,"type":"integer"}},"type":"object"},"RouteSimulationResult":{"description":"RouteSimulationResult schema","properties":{"admission_allowed":{"description":"Whether the customer account has sufficient prepaid balance or postpaid credit headroom for the estimated retail cost. This is read-only and does not reserve funds.","example":true,"type":"boolean"},"allowed":{"description":"Whether the simulated call would pass account, trunk, block-rule, admission, LCR, carrier, and gateway checks with at least one usable carrier route.","example":true,"type":"boolean"},"ani":{"description":"Original ANI supplied in the simulation request.","example":"61400111222","type":"string"},"carriers":{"description":"Carrier route candidates considered by LCR, including wholesale cost, margin status, block-rule status, and gateway availability.","example":[],"items":{"description":"Carrier route candidates considered by LCR, including wholesale cost, margin status, block-rule status, and gateway availability.","example":[],"properties":{"allowed":{"description":"Whether this carrier candidate is usable after margin, block-rule, status, and gateway checks.","example":true,"type":"boolean"},"block_rule_id":{"description":"Block rule ID that excluded this carrier candidate, if any.","example":15,"format":"int64","nullable":true,"type":"integer"},"blocked":{"description":"Whether an active carrier-trunk block rule matched this ANI or DNIS.","example":false,"type":"boolean"},"carrier_gateway_id":{"description":"Previewed active gateway ID for this carrier candidate.","example":5,"format":"int64","nullable":true,"type":"integer"},"carrier_name":{"description":"Human-readable carrier trunk name.","example":"Premium Mobile Carrier","type":"string"},"carrier_trunk_id":{"description":"Carrier trunk candidate ID returned by LCR.","example":20,"format":"int64","type":"integer"},"converted_wholesale_cost":{"description":"Estimated wholesale cost converted into the customer currency for margin comparison.","example":"0.0150","type":"string"},"converted_wholesale_rate":{"description":"Carrier wholesale rate per minute converted into the customer currency for LCR comparison.","example":"0.0150","type":"string"},"customer_currency":{"description":"Currency for customer retail rating.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_rate":{"description":"Customer retail rate per minute used for margin and retail cost calculation.","example":"0.0250","type":"string"},"customer_rate_type":{"default":"per_minute","description":"Customer rate type used for cost estimation: per_minute or per_call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"},"destination_uri":{"description":"SIP URI that would be used for the outbound carrier attempt.","example":"sip:61400123456@203.0.113.50:5060","nullable":true,"type":"string"},"excluded_reason":{"description":"Reason this carrier candidate was not usable.","example":"blocked by carrier trunk rule","nullable":true,"type":"string"},"margin_amount":{"description":"Retail cost minus converted wholesale cost in the customer currency for the supplied duration.","example":"0.0100","type":"string"},"margin_ok":{"description":"Whether the route satisfies the configured LCR margin protection rules.","example":true,"type":"boolean"},"outbound_dnis":{"description":"DNIS sent to the carrier after applying any carrier tech prefix.","example":"61400123456","nullable":true,"type":"string"},"reject_code":{"description":"SIP response code associated with the exclusion, if any.","example":403,"nullable":true,"type":"integer"},"retail_cost":{"description":"Estimated retail charge to the customer for the supplied duration.","example":"0.0250","type":"string"},"wholesale_cost":{"description":"Estimated wholesale carrier cost for the supplied duration after carrier billing increment and rounding rules.","example":"0.0150","type":"string"},"wholesale_currency":{"description":"Currency of the carrier wholesale rate and wholesale cost.","example":"AUD","type":"string"},"wholesale_rate":{"description":"Carrier wholesale rate per minute from the carrier's active rate deck.","example":"0.0150","type":"string"},"wholesale_rate_type":{"default":"per_minute","description":"Carrier rate type used for wholesale cost estimation: per_minute or per_call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"}},"type":"object"},"type":"array"},"checks":{"description":"Ordered admission and routing checks performed by the simulator.","example":[],"items":{"description":"Ordered admission and routing checks performed by the simulator.","example":[],"properties":{"name":{"description":"Name of the simulated routing or admission check.","example":"Acme Telecom","type":"string"},"passed":{"description":"Whether this individual check passed.","example":true,"type":"boolean"},"reject_code":{"description":"SIP response code associated with this failed check, if any.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Operational reason for a failed check.","example":"insufficient_balance","nullable":true,"type":"string"}},"type":"object"},"type":"array"},"customer_account_id":{"description":"Customer account ID used for the simulation when supplied or inferred from the customer trunk.","example":1,"format":"int64","nullable":true,"type":"integer"},"customer_currency":{"description":"Currency used for customer-side retail rating.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_rate":{"description":"Customer retail sell rate per minute matched by the active customer rate deck.","example":"0.0250","type":"string"},"customer_rate_type":{"default":"per_minute","description":"Rate type for the matched customer rate. per_minute is duration-rated using billing increments; per_call is a flat charge per connected call.","enum":["per_minute","per_call"],"example":"per_minute","type":"string"},"customer_trunk_id":{"description":"Customer ingress trunk ID used for the simulation.","example":10,"format":"int64","type":"integer"},"dnis":{"description":"Original DNIS supplied in the simulation request.","example":"61400123456","type":"string"},"duration_seconds":{"description":"Answered duration used for cost estimation.","example":73,"type":"integer"},"headroom":{"description":"Remaining prepaid balance or postpaid credit headroom observed during the read-only simulation.","example":"4999.7500","type":"string"},"no_match_reason":{"description":"LCR or routing reason no usable carrier route was found.","example":"no_active_rate_match","nullable":true,"type":"string"},"normalized_ani":{"description":"ANI after SIP URI parsing and non-digit cleanup, matching the normalisation used by the OpenSIPS REST routing path.","example":"61400111222","type":"string"},"normalized_dnis":{"description":"DNIS after SIP URI parsing, non-digit cleanup, and customer tech prefix stripping where configured.","example":"61400123456","type":"string"},"reject_code":{"description":"SIP response code that would be returned if the simulated call is rejected before carrier routing.","example":403,"nullable":true,"type":"integer"},"reject_reason":{"description":"Human-readable reason the simulated call would not proceed.","example":"insufficient_balance","nullable":true,"type":"string"},"retail_cost":{"description":"Estimated customer charge for the supplied duration after customer billing increment and rounding rules.","example":"0.0250","type":"string"},"selected_carrier_trunk_id":{"description":"First carrier trunk that would be selected for the call after filtering blocked, unprofitable, inactive, and gateway-less candidates.","example":20,"format":"int64","nullable":true,"type":"integer"},"selected_gateway_id":{"description":"Gateway ID previewed for the selected carrier route without mutating load-balance state.","example":5,"format":"int64","nullable":true,"type":"integer"}},"type":"object"},"ToggleActiveRequest":{"description":"ToggleActiveRequest schema","properties":{"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"}},"type":"object"},"ToggleResponse":{"description":"ToggleResponse schema","properties":{"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"}},"type":"object"},"TrunkIPAuth":{"description":"TrunkIPAuth schema","properties":{"cidr":{"description":"IPv4 or IPv6 CIDR range authorised for trunk IP authentication.","example":"203.0.113.0/24","type":"string"},"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"trunk_id":{"description":"ID of the referenced balance entity. For trunk_type=customer this is the customer_account_id; for trunk_type=carrier this is the carrier_trunk_id.","example":10,"format":"int64","type":"integer"},"trunk_type":{"description":"Type of balance entity referenced by the record. customer means customer account-level billing; carrier means carrier trunk-level billing.","enum":["customer","carrier"],"example":"customer","type":"string"}},"type":"object"},"TrunkSIPRegistration":{"description":"TrunkSIPRegistration schema","properties":{"created_at":{"description":"UTC timestamp when the record was created.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"domain":{"description":"SIP authentication realm or domain for endpoint registration.","example":"sip.crazytel.net.au","type":"string"},"hash_algorithm":{"description":"Digest hash algorithm used for stored SIP registration credentials.","enum":["md5","sha256"],"example":"md5","type":"string"},"id":{"description":"Unique database identifier for the record.","example":1,"format":"int64","type":"integer"},"password_hash":{"description":"Stored SIP digest password hash. This is credential material and should not be exposed unnecessarily.","example":"5f4dcc3b5aa765d61d8327deb882cf99","type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"updated_at":{"description":"UTC timestamp when the record was last modified.","example":"2026-01-15T08:30:00Z","format":"date-time","type":"string"},"username":{"description":"SIP username or API-auth username associated with the request.","example":"cust001","type":"string"}},"type":"object"},"UpdateBlockRuleRequest":{"description":"UpdateBlockRuleRequest schema","properties":{"block_type":{"description":"Type of call attribute matched by the block rule.","enum":["ani","dnis","number_code"],"example":"dnis","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"is_active":{"description":"Whether the record is active and should be enforced by the API, cache, or routing layer.","example":true,"type":"boolean"},"match_type":{"description":"How a block rule pattern is matched against ANI or DNIS.","enum":["exact","prefix"],"example":"prefix","type":"string"},"pattern":{"description":"ANI, DNIS, number code, or prefix pattern matched by the rule.","example":"1900","type":"string"},"scope":{"description":"Operational scope where the rule applies.","enum":["global","customer_trunk","carrier_trunk","route"],"example":"global","type":"string"},"scope_id":{"description":"ID of the scoped entity. Null when scope is global.","example":10,"format":"int64","nullable":true,"type":"integer"},"sip_response_code":{"description":"Final SIP response code recorded for the call or returned by a block rule.","example":200,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"UpdateCarrierSIPFailoverRuleRequest":{"description":"UpdateCarrierSIPFailoverRuleRequest schema","properties":{"action":{"description":"Action the API or OpenSIPS should take for this rule or decision.","enum":["retry_next","return_to_customer","block_new","terminate_all"],"example":"retry_next","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"sip_code":{"description":"SIP response code matched by a carrier failover rule.","example":503,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"UpdateCarrierTrunkIPRequest":{"description":"UpdateCarrierTrunkIPRequest schema","properties":{"ip_address":{"description":"IPv4 or IPv6 address used for API allowlisting, SIP gateway routing, or trunk authentication.","example":"203.0.113.50","type":"string"},"port":{"default":5060,"description":"SIP UDP/TCP port used by a gateway or endpoint.","example":5060,"maximum":32767,"minimum":-32768,"type":"integer"},"priority":{"default":1,"description":"Gateway priority. Lower numbers are preferred first in failover mode.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"weight":{"default":1,"description":"Relative gateway weight used for weighted load balancing.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"}},"type":"object"},"UpdateCarrierTrunkRequest":{"description":"UpdateCarrierTrunkRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Action used when account-level cutoff or a trunk security overlay triggers for balance or credit enforcement.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether the switch should automatically block or cut off traffic when account-level balance or credit rules are breached.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Financial admission mode for calls. On customer accounts this is the billing source of truth; on customer trunks it is retained only as a compatibility/security overlay field.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Maximum new call attempts per second allowed for the trunk.","example":10,"maximum":32767,"minimum":-32768,"type":"integer"},"credit_limit":{"description":"Postpaid credit ceiling as a decimal money string. Customer account credit_limit is authoritative; customer trunk credit_limit is an optional stricter security overlay.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"decimal_precision":{"default":4,"description":"Number of decimal places used for rating and money values. Supported values are 4, 5, and 6.","enum":[4,5,6],"example":4,"maximum":32767,"minimum":-32768,"type":"integer"},"gst_applicable":{"description":"Whether Australian GST applies for invoice generation. GST is not embedded in CDR cost fields.","example":true,"type":"boolean"},"gst_treatment":{"description":"How GST is presented on invoices when GST applies.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"multi_ip_mode":{"default":"load_balance","description":"How multiple gateway IPs on a carrier trunk are selected.","enum":["load_balance","failover"],"example":"load_balance","type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":-32768,"type":"integer"},"port_limit":{"description":"Maximum concurrent active call slots allowed for the trunk.","example":30,"maximum":32767,"minimum":-32768,"type":"integer"},"rounding_direction":{"default":"standard","description":"How rated monetary values are rounded.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls for this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted by policy.","example":false,"type":"boolean"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Optional routing prefix prepended to DNIS for trunk identification and stripped before carrier relay.","example":"9999","nullable":true,"type":"string"},"warning_threshold":{"description":"Balance or credit threshold that should trigger warning notifications. Customer account threshold is authoritative; customer trunk threshold is an optional overlay.","example":"1000.00","nullable":true,"type":"string"}},"type":"object"},"UpdateCustomerAccountRequest":{"description":"UpdateCustomerAccountRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Updated account-level automatic cutoff action: block_new or terminate_all.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Whether account-level automatic cutoff is enabled.","example":false,"type":"boolean"},"billing_mode":{"default":"postpaid","description":"Updated account-level financial admission mode: prepaid or postpaid.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"can_create_sip_registrations":{"description":"Updated permission: whether the customer portal user may add SIP registration credentials.","example":false,"type":"boolean"},"can_create_trunk_ips":{"description":"Updated permission: whether the customer portal user may add IP auth records to their trunks.","example":false,"type":"boolean"},"can_create_trunks":{"description":"Updated permission: whether the customer portal user may create new trunks under this account.","example":false,"type":"boolean"},"can_delete_sip_registrations":{"description":"Updated permission: whether the customer portal user may remove SIP registration credentials.","example":false,"type":"boolean"},"can_delete_trunk_ips":{"description":"Updated permission: whether the customer portal user may remove IP auth records for their trunks.","example":false,"type":"boolean"},"can_delete_trunks":{"description":"Updated permission: whether the customer portal user may delete (soft-delete) trunks.","example":false,"type":"boolean"},"can_modify_sip_registrations":{"description":"Updated permission: whether the customer portal user may update SIP registration credentials.","example":false,"type":"boolean"},"can_modify_trunk_ips":{"description":"Updated permission: whether the customer portal user may update IP auth records for their trunks.","example":false,"type":"boolean"},"can_modify_trunks":{"description":"Updated permission: whether the customer portal user may update trunk settings.","example":false,"type":"boolean"},"can_view_sip_registrations":{"description":"Updated permission: whether the customer portal user may view SIP registration credentials for their trunks.","example":true,"type":"boolean"},"can_view_trunk_ips":{"description":"Updated permission: whether the customer portal user may view IP auth records for their trunks.","example":true,"type":"boolean"},"can_view_trunks":{"description":"Updated permission: whether the customer portal user may view trunks belonging to this account.","example":true,"type":"boolean"},"credit_limit":{"description":"Updated account-level postpaid credit ceiling as a decimal money string. Null clears the account credit ceiling.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"Updated default billing currency for this account. Supported values are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"gst_applicable":{"description":"Whether Australian GST applies to this account for future invoice generation.","example":true,"type":"boolean"},"gst_treatment":{"description":"Updated invoice GST presentation: inclusive or exclusive.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"invoice_period_days":{"description":"Updated billing cycle length in days.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"name":{"description":"Updated human-readable wholesale customer account name used in the portal, invoices, reports, and logs.","example":"Acme Telecom","type":"string"},"password":{"description":"Updated portal login password. Minimum 8 characters. If provided, the existing password is replaced with a new bcrypt hash. Omit to keep the current password unchanged.","example":"SecurePass123","nullable":true,"type":"string"},"payment_due_days":{"description":"Updated number of days after invoice issue before payment is due.","example":7,"maximum":32767,"minimum":1,"type":"integer"},"status":{"description":"Updated account lifecycle state. Omit to preserve the current status.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"username":{"description":"Updated portal login username. The @wholesale suffix is appended automatically if not provided.","example":"cust001","type":"string"},"warning_threshold":{"description":"Updated account-level warning threshold as a decimal money string. Null clears threshold warnings.","example":"1000.00","nullable":true,"type":"string"}},"required":["billing_mode","currency","name","username"],"type":"object"},"UpdateCustomerTrunkRequest":{"description":"UpdateCustomerTrunkRequest schema","properties":{"auto_cutoff_behaviour":{"default":"block_new","description":"Updated auto cutoff behaviour inherited from the parent customer account. Any value provided in the request is ignored.","enum":["block_new","terminate_all"],"example":"block_new","type":"string"},"auto_cutoff_enabled":{"default":false,"description":"Updated auto cutoff flag inherited from the parent customer account. Any value provided in the request is ignored.","example":false,"type":"boolean"},"billing_increment_initial":{"default":1,"description":"Updated initial billing block in seconds.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_increment_step":{"default":1,"description":"Updated billing step in seconds after the initial block.","example":1,"maximum":32767,"minimum":1,"type":"integer"},"billing_mode":{"default":"postpaid","description":"Updated financial admission mode inherited from the parent customer account. Any value provided in the request is ignored.","enum":["prepaid","postpaid"],"example":"postpaid","type":"string"},"cps_limit":{"description":"Updated maximum new SIP call attempts per second accepted from this trunk.","example":10,"maximum":32767,"minimum":1,"type":"integer"},"credit_limit":{"description":"Updated postpaid credit ceiling as a decimal money string. Null clears the trunk-level credit ceiling.","example":"5000.00","nullable":true,"type":"string"},"currency":{"description":"Updated billing currency inherited from the parent customer account. Any value provided in the request is ignored.","enum":["AUD","USD"],"example":"AUD","type":"string"},"customer_account_id":{"description":"Parent customer account ID that owns this SIP ingress trunk.","example":1,"format":"int64","minimum":1,"type":"integer"},"decimal_precision":{"default":4,"description":"Updated decimal places used when rating customer costs. Supported values are 4, 5, and 6.","enum":[4,5,6],"example":4,"maximum":6,"minimum":4,"type":"integer"},"gst_applicable":{"description":"Whether GST applies to this trunk for future invoice generation.","example":true,"type":"boolean"},"gst_treatment":{"description":"Updated trunk-level GST presentation for invoices: inclusive or exclusive.","enum":["inclusive","exclusive"],"example":"exclusive","nullable":true,"type":"string"},"name":{"description":"Updated human-readable trunk name used in portal screens, routing operations, logs, and billing reports.","example":"Acme Telecom","type":"string"},"per_minute_rate_limit":{"description":"Updated maximum new call attempts allowed in a rolling 60-second window.","example":300,"maximum":32767,"minimum":1,"type":"integer"},"port_limit":{"description":"Updated maximum concurrent active calls allowed on this trunk.","example":30,"maximum":32767,"minimum":1,"type":"integer"},"rounding_direction":{"default":"standard","description":"Updated money rounding mode: up, down, or standard.","enum":["up","down","standard"],"example":"standard","type":"string"},"rtpengine_enabled":{"default":false,"description":"Whether calls on this trunk should anchor media through RTPEngine.","example":false,"type":"boolean"},"sip_trusted":{"default":false,"description":"Whether SIP identity headers from this trunk are trusted.","example":false,"type":"boolean"},"status":{"description":"Updated trunk lifecycle state. Omit to preserve the current status.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"tech_prefix":{"description":"Updated optional routing prefix expected before the DNIS and stripped before onward carrier relay.","example":"9999","nullable":true,"type":"string"},"warning_threshold":{"description":"Updated warning threshold inherited from the parent customer account. Any value provided in the request is ignored.","example":"1000.00","nullable":true,"type":"string"}},"required":["currency","customer_account_id","name"],"type":"object"},"UpdateLCRConfigRequest":{"description":"UpdateLCRConfigRequest schema","properties":{"acceptable_loss_pct":{"description":"Decimal percentage of negative margin that may be tolerated by LCR before a route is blocked.","example":"5.00","type":"string"},"block_if_cost_exceeds_sell":{"description":"Whether LCR should block routes where carrier cost is greater than or equal to customer sell price.","example":true,"type":"boolean"},"cache_ttl_seconds":{"default":300,"description":"Number of seconds LCR or cache entries remain valid before being refreshed.","example":300,"format":"int32","type":"integer"},"rate_deck_id":{"description":"Rate deck ID used for assignment, lookup, rate rows, or LCR configuration.","example":7,"format":"int64","nullable":true,"type":"integer"}},"type":"object"},"UpdateOpensipsNodeRequest":{"description":"UpdateOpensipsNodeRequest schema","properties":{"hostname":{"description":"Hostname of the OpenSIPS node or service instance.","example":"opensips-syd-01","type":"string"},"mi_socket_address":{"description":"OpenSIPS Management Interface socket address used by the API to push runtime changes.","example":"http://127.0.0.1:8888/mi","type":"string"},"region":{"description":"Geographic or operational region for an OpenSIPS node.","example":"sydney","nullable":true,"type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"}},"type":"object"},"UpdateRateDeckRateRequest":{"description":"UpdateRateDeckRateRequest schema","properties":{"billing_increment_initial":{"default":1,"description":"Initial billing block in seconds applied to answered calls before step rounding begins.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"billing_increment_step":{"default":1,"description":"Billing step in seconds applied after the initial billing block.","example":1,"maximum":32767,"minimum":-32768,"nullable":true,"type":"integer"},"destination_name":{"description":"Human-readable destination name for the matched rate prefix.","example":"Australia Mobile","type":"string"},"effective_at":{"description":"UTC timestamp when the rate, exchange rate, or scheduled update becomes effective.","example":"2026-02-01T00:00:00Z","nullable":true,"type":"string"},"prefix":{"description":"Destination prefix used for longest-prefix rate matching.","example":"614","type":"string"},"rate_per_minute":{"description":"Rate value as a decimal money string in the rate deck currency. For per_minute rows this is the per-minute price; for per_call rows this is the flat per-call charge.","example":"0.0250","type":"string"},"rate_type":{"default":"per_minute","description":"Rate charging model for a prefix. per_minute uses duration and billing increments; per_call charges the value once per connected call.","enum":["per_minute","per_call"],"example":"per_minute","nullable":true,"type":"string"}},"type":"object"},"UpdateRateDeckRequest":{"description":"UpdateRateDeckRequest schema","properties":{"currency":{"description":"ISO currency code used for this monetary amount. Supported currencies are AUD and USD.","enum":["AUD","USD"],"example":"AUD","type":"string"},"deck_type":{"description":"Rate deck type indicating whether rates are customer sell rates or carrier cost rates.","enum":["customer","carrier"],"example":"customer","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"name":{"description":"Human-readable name displayed in the portal, reports, routing screens, and logs.","example":"Acme Telecom","type":"string"}},"type":"object"},"UpdateTrunkIPAuthRequest":{"description":"UpdateTrunkIPAuthRequest schema","properties":{"cidr":{"description":"IPv4 or IPv6 CIDR range authorised for trunk IP authentication.","example":"203.0.113.0/24","type":"string"},"description":{"description":"Human-readable operational note explaining the purpose of the record.","example":"Primary Sydney carrier route","nullable":true,"type":"string"},"trunk_id":{"description":"ID of the referenced balance entity. For trunk_type=customer this is the customer_account_id; for trunk_type=carrier this is the carrier_trunk_id.","example":10,"format":"int64","type":"integer"},"trunk_type":{"description":"Type of balance entity referenced by the record. customer means customer account-level billing; carrier means carrier trunk-level billing.","enum":["customer","carrier"],"example":"customer","type":"string"}},"type":"object"},"UpdateTrunkSIPRegistrationRequest":{"description":"UpdateTrunkSIPRegistrationRequest schema","properties":{"customer_trunk_id":{"description":"Customer trunk ID for the ingress SIP endpoint or account route context.","example":10,"format":"int64","type":"integer"},"domain":{"description":"SIP authentication realm or domain for endpoint registration.","example":"sip.crazytel.net.au","type":"string"},"hash_algorithm":{"description":"Digest hash algorithm used for stored SIP registration credentials.","enum":["md5","sha256"],"example":"md5","type":"string"},"password_hash":{"description":"Stored SIP digest password hash. This is credential material and should not be exposed unnecessarily.","example":"5f4dcc3b5aa765d61d8327deb882cf99","type":"string"},"status":{"description":"Lifecycle state of the record.","enum":["active","suspended","closed","disabled","pending","superseded"],"example":"active","type":"string"},"username":{"description":"SIP username or API-auth username associated with the request.","example":"cust001","type":"string"}},"type":"object"}}},"info":{"contact":{"email":"wholesale@crazytel.com.au","name":"Crazytel Wholesale Support","url":"https://crazytel.com.au/support"},"description":"Carrier-grade Class 4 Softswitch — SIP call routing, Least Cost Routing (LCR), Call Detail Records (CDR), and real-time billing.","license":{"name":"Proprietary"},"title":"Crazytel Wholesale Switch API","version":"1.0.0"},"paths":{"/api/admin/route-simulator":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RouteSimulatorHandler).Simulate`\n\n---\n\nSimulates whether a customer call would be accepted and which carrier routes would be available without reserving balance, sending SIP traffic, or mutating OpenSIPS runtime state.\n\n**Purpose:**\nLets an operator choose a customer account, customer trunk, ANI, DNIS, and optional duration to preview the same admission and routing decisions used by the live OpenSIPS REST path. The response shows whether the call would go through, the selected carrier if any, all LCR carrier candidates, retail customer cost, wholesale carrier cost, margin status, block-rule failures, gateway availability, and the reject code/reason when the call would fail.\n\n**How it works:**\n- Reads the selected customer trunk from the **read replica** and verifies status and optional customer account ownership.\n- Normalises ANI/DNIS and strips the customer tech prefix like the live OpenSIPS auth path.\n- Checks active global and customer-trunk block rules.\n- Runs LCR using the customer's active rate deck, carrier allowlist, carrier active rate decks, exchange rates, and margin protection.\n- Performs a read-only balance/credit headroom check for the estimated retail cost; no reservation or balance transaction is written.\n- Checks carrier-trunk block rules and active gateway availability for each LCR candidate.\n\n**Request payload:**\n```json\n{\n  \"customer_account_id\": 1,\n  \"customer_trunk_id\": 10,\n  \"ani\": \"61400111222\",\n  \"dnis\": \"61400123456\",\n  \"duration_seconds\": 60\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — simulation completed; inspect `allowed`, `reject_code`, `reject_reason`, and `carriers`\n- `400 Bad Request` — malformed JSON\n- `422 Unprocessable Content` — validation failure\n- `500 Internal Server Error` — repository or simulator failure","operationId":"POST_/api/admin/route-simulator","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteSimulationRequest"}}},"description":"Request body for service.RouteSimulationRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteSimulationResult"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIErrorResponse"}}},"description":"Bad Request"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIErrorResponse"}}},"description":"Unprocessable Content"},"default":{"description":""}},"summary":"Admin Route Simulator","tags":["Admin APIs - Route Simulator"]}},"/api/auth/ips":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*AuthIPHandler).List`\n\n---\n\nReturns all active, non-expired IP addresses authorised to access the API.\n\n**Purpose:**\nThe Crazytel Wholesale Switch API is protected by an IP-based authentication middleware. Every request (except `/api/health` and `/health`) is checked against this whitelist. This endpoint lets administrators audit which source IPs currently have access.\n\n**How it works:**\n- Queries the `auth_ips` table on the **read replica**.\n- Returns only records where `is_active = true` and the optional expiry timestamp has not passed.\n- The IP-auth middleware caches this list in Valkey with a short TTL to avoid hitting the database on every request.\n- A seed record for `127.0.0.1` (localhost) is automatically created at startup for local development access.\n\n**Query parameters:**\n- `active_only` — optional boolean; when `true`, filters to active records only (default behaviour is to return all active records)\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"ip_address\": \"203.0.113.0/24\",\n    \"description\": \"Crazytel HQ\",\n    \"is_active\": true,\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  },\n  {\n    \"id\": 2,\n    \"ip_address\": \"127.0.0.1\",\n    \"description\": \"localhost seed\",\n    \"is_active\": true,\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — list returned successfully\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/auth/ips","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AuthIPRecord"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Authorised IPs","tags":["Admin APIs - Authentication"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*AuthIPHandler).Create`\n\n---\n\nAdds a new IP address to the API authentication whitelist.\n\n**Purpose:**\nGrants API access to a new source IP address. Once added, requests from this IP will pass the IP-auth middleware and reach the protected API endpoints. This is the gatekeeper for all API access.\n\n**How it works:**\n- Validates that the provided `ip_address` is a valid IPv4 or IPv6 address (individual IPs only — CIDR ranges are not supported at creation time).\n- Performs an **upsert** into `auth_ips` on the **write primary**: if an entry for the same IP already exists, it is updated; otherwise a new row is inserted.\n- The IP-auth middleware picks up the change on its next Valkey cache refresh (typically within seconds).\n- There is no expiry mechanism in the current implementation — records remain active until `is_active` is set to `false` or the row is manually deleted.\n\n**Request payload:**\n```json\n{\n  \"ip_address\": \"203.0.113.10\",\n  \"description\": \"Customer portal server\",\n  \"is_active\": true\n}\n```\n\n**Fields:**\n- `ip_address` (required) — a single IPv4 or IPv6 address (e.g., `203.0.113.10` or `2001:db8::1`)\n- `description` (optional) — human-readable label for audit purposes\n- `is_active` (optional) — defaults to `true`; set to `false` to add a pre-approved but currently disabled entry\n\n**Response (201 Created):**\n```json\n{\n  \"message\": \"auth ip created\",\n  \"ip_address\": \"203.0.113.10\"\n}\n```\n\n**HTTP status codes:**\n- `201 Created` — IP added or updated successfully\n- `400 Bad Request` — invalid IP address format or missing required field\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/auth/ips","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequest"}}},"description":"Request body for handler.CreateRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"Created"},"default":{"description":""}},"summary":"Add Authorised IP","tags":["Admin APIs - Authentication"]}},"/api/auth/login":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*AuthHandler).Login`\n\n---\n\nAuthenticates a wholesale customer portal user by username and password.\n\n**Purpose:**\nValidates the portal login credentials of a customer account. The frontend uses this endpoint when a user attempts to sign in to the Crazytel Wholesale customer portal. Because the frontend also supports logins for other systems, the username is always normalised with a `@wholesale` suffix before lookup.\n\n**How it works:**\n- Normalises the provided `username` by appending `@wholesale` if it is not already present.\n- Queries `customer_accounts` by `username` on the **read replica**.\n- Verifies the provided `password` against the stored `password_hash` using bcrypt.\n- Rejects inactive, suspended, or closed accounts with `403 Forbidden`.\n- On success, generates a secure bearer token, stores its SHA-256 hash in `customer_auth_tokens`, and returns the raw token in the response. The raw token is returned only once — it is never stored in plaintext.\n- The token expires after 24 hours.\n\n**Request payload:**\n```json\n{\n  \"username\": \"acme\",\n  \"password\": \"SecurePass123\"\n}\n```\n\n**Fields:**\n- `username` (required) — portal username. The `@wholesale` suffix is appended automatically if missing.\n- `password` (required) — portal password.\n\n**Response (200 OK):**\n```json\n{\n  \"id\": 1,\n  \"name\": \"Acme Telecom\",\n  \"username\": \"acme@wholesale\",\n  \"currency\": \"AUD\",\n  \"status\": \"active\",\n  \"token\": \"a1b2c3d4e5f6...\",\n  \"message\": \"login successful\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — credentials valid and account is active\n- `400 Bad Request` — missing username or password\n- `401 Unauthorized` — unknown username or password mismatch\n- `403 Forbidden` — account exists but is suspended or closed\n- `500 Internal Server Error` — database or hashing error","operationId":"POST_/api/auth/login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"description":"Request body for handler.LoginRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"Unauthorized"},"default":{"description":""}},"summary":"Customer Portal Login","tags":["Admin APIs - Authentication"]}},"/api/auth/logout":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*AuthHandler).Logout`\n\n---\n\nInvalidates the customer's bearer token, ending the portal session.\n\n**Purpose:**\nTerminates the authenticated session by deleting the bearer token from `customer_auth_tokens`. Subsequent requests using the same token will be rejected with `401 Unauthorized`.\n\n**How it works:**\n- Reads the `Authorization: Bearer \u003ctoken\u003e` header from the request.\n- Hashes the raw token with SHA-256 and looks it up in `customer_auth_tokens`.\n- Deletes the matching token row from the **write primary**.\n- Returns `200 OK` even if the token is not found (idempotent logout).\n\n**Authentication:**\nRequires a valid `Authorization: Bearer \u003ctoken\u003e` header. IP-auth (admin) requests are also allowed but have no effect because there is no bearer token to invalidate.\n\n**Response (200 OK):**\n```json\n{\n  \"message\": \"logout successful\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — token invalidated (or token not found)\n- `401 Unauthorized` — no `Authorization` header present","operationId":"POST_/api/auth/logout","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Customer Portal Logout","tags":["Admin APIs - Authentication"]}},"/api/balances/{entity_type}/{entity_id}/balance":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).GetBalance`\n\n---\n\nReturns the current prepaid balance or postpaid credit usage for an entity.\n\n**Purpose:**\nShows the real-time financial state of an entity. For prepaid entities, this is the remaining dollar balance. For postpaid entities, this is the accumulated usage against the credit limit.\n\n**How it works:**\n- Queries the `balances` table on the **read replica** for the entity's current balance.\n- Calculates `headroom` = `credit_limit - balance` (postpaid) or `balance` (prepaid).\n- All monetary values are decimal strings, never floats.\n\n**Path parameters:**\n- `entity_type` — `customer` or `carrier`\n- `entity_id` — entity database ID (customer account ID or carrier trunk ID)\n\n**Response example (postpaid):**\n```json\n{\n  \"entity_type\": \"customer\",\n  \"entity_id\": 10,\n  \"balance\": \"2500.50\",\n  \"currency\": \"AUD\",\n  \"billing_mode\": \"postpaid\",\n  \"credit_limit\": \"5000.00\",\n  \"headroom\": \"2499.50\"\n}\n```\n\n**Response example (prepaid):**\n```json\n{\n  \"entity_type\": \"customer\",\n  \"entity_id\": 11,\n  \"balance\": \"750.00\",\n  \"currency\": \"AUD\",\n  \"billing_mode\": \"prepaid\",\n  \"credit_limit\": null,\n  \"headroom\": \"750.00\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — balance returned\n- `400 Bad Request` — invalid entity_type or entity_id\n- `500 Internal Server Error` — database query or service error","operationId":"GET_/api/balances/:entity_type/:entity_id/balance","parameters":[{"in":"path","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"path","name":"entity_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Balance","tags":["Billing APIs - Balances"]}},"/api/balances/{entity_type}/{entity_id}/balance/adjust":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).Adjust`\n\n---\n\nAdjusts a balance by an arbitrary signed amount.\n\n**Purpose:**\nApplies a manual correction — refund, write-off, or billing fix. Unlike credit/debit, the amount can be positive OR negative.\n\n**How it works:**\n- Parses `amount` as a `decimal.Decimal` string (can be negative, e.g., `\"-25.00\"`).\n- Adds the amount to the current balance on the **write primary**.\n- Inserts an `adjustment` transaction into `balance_transactions`.\n\n**Path parameters:**\n- `entity_type` — `customer` or `carrier`\n- `entity_id` — entity database ID (customer account ID or carrier trunk ID)\n\n**Request payload:**\n```json\n{\n  \"amount\": \"-25.00\",\n  \"description\": \"Billing correction for duplicate CDR\",\n  \"cdr_id\": 12345\n}\n```\n\n**Response:**\n```json\n{\n  \"message\": \"adjustment applied\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — adjustment applied\n- `400 Bad Request` — invalid amount or service error","operationId":"POST_/api/balances/:entity_type/:entity_id/balance/adjust","parameters":[{"in":"path","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"path","name":"entity_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdjustRequest"}}},"description":"Request body for handler.AdjustRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Adjust Balance","tags":["Billing APIs - Balances"]}},"/api/balances/{entity_type}/{entity_id}/balance/credit":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).Credit`\n\n---\n\nAdds funds to a balance and records a credit transaction.\n\n**Purpose:**\nTops up a prepaid entity or applies a payment to a postpaid account. Creates an auditable transaction record.\n\n**How it works:**\n- Parses `amount` as a `decimal.Decimal` string.\n- Atomically increases the entity's balance in the `balances` table on the **write primary**.\n- Inserts a `credit` transaction into `balance_transactions` with the new `balance_after`.\n\n**Path parameters:**\n- `entity_type` — `customer` or `carrier`\n- `entity_id` — entity database ID (customer account ID or carrier trunk ID)\n\n**Request payload:**\n```json\n{\n  \"amount\": \"1000.00\",\n  \"description\": \"Top-up January 2026\"\n}\n```\n\n**Fields:**\n- `amount` (required) — positive decimal string (e.g., `\"1000.00\"`)\n- `description` (optional) — memo for audit trail\n\n**Response:**\n```json\n{\n  \"message\": \"credit applied\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — credit applied\n- `400 Bad Request` — invalid amount, invalid entity_type, or service validation error","operationId":"POST_/api/balances/:entity_type/:entity_id/balance/credit","parameters":[{"in":"path","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"path","name":"entity_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditRequest"}}},"description":"Request body for handler.CreditRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Credit Balance","tags":["Billing APIs - Balances"]}},"/api/balances/{entity_type}/{entity_id}/balance/debit":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).Debit`\n\n---\n\nSubtracts funds from a balance and records a debit transaction.\n\n**Purpose:**\nDeducts call costs from a prepaid balance or increases postpaid usage. Typically called by the CDR ingest process after a call completes.\n\n**How it works:**\n- Parses `amount` as a `decimal.Decimal` string.\n- Atomically decreases the entity's balance (or increases postpaid usage) on the **write primary**.\n- Inserts a `debit` transaction into `balance_transactions`.\n- Optionally links the transaction to a CDR via `cdr_id` for traceability.\n\n**Path parameters:**\n- `entity_type` — `customer` or `carrier`\n- `entity_id` — entity database ID (customer account ID or carrier trunk ID)\n\n**Request payload:**\n```json\n{\n  \"amount\": \"50.25\",\n  \"description\": \"Call to 61400123456\",\n  \"cdr_id\": 12345\n}\n```\n\n**Fields:**\n- `amount` (required) — positive decimal string representing the cost to deduct\n- `description` (optional) — memo\n- `cdr_id` (optional) — link to the CDR that triggered this debit\n\n**Response:**\n```json\n{\n  \"message\": \"debit applied\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — debit applied\n- `400 Bad Request` — invalid amount, insufficient balance, or service error","operationId":"POST_/api/balances/:entity_type/:entity_id/balance/debit","parameters":[{"in":"path","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"path","name":"entity_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DebitRequest"}}},"description":"Request body for handler.DebitRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Debit Balance","tags":["Billing APIs - Balances"]}},"/api/balances/{entity_type}/{entity_id}/transactions":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).ListTransactions`\n\n---\n\nReturns paginated balance transactions for an entity.\n\n**Purpose:**\nProvides an audit trail of all balance changes — credits, debits, adjustments, and reservations. Ordered by most recent first.\n\n**How it works:**\n- Queries `balance_transactions` on the **read replica**, ordered by `created_at DESC`.\n- Supports `limit` and `offset` for pagination.\n- Each transaction includes `transaction_type`, `amount`, `balance_after`, and optional `cdr_id` link.\n\n**Path parameters:**\n- `entity_type` — `customer` or `carrier`\n- `entity_id` — entity database ID (customer account ID or carrier trunk ID)\n\n**Query parameters:**\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"entity_type\": \"customer\",\n      \"entity_id\": 10,\n      \"transaction_type\": \"credit\",\n      \"amount\": \"1000.00\",\n      \"balance_after\": \"2500.50\",\n      \"description\": \"Top-up January 2026\",\n      \"cdr_id\": null,\n      \"call_id\": null,\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — transactions returned\n- `400 Bad Request` — invalid entity_type or entity_id\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/balances/:entity_type/:entity_id/transactions","parameters":[{"in":"path","name":"entity_type","required":true,"schema":{"type":"string"}},{"in":"path","name":"entity_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/BalanceTransaction"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Transactions","tags":["Billing APIs - Balances"]}},"/api/block-rules":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).List`\n\n---\n\nReturns all block rules with optional filtering.\n\n**Purpose:**\nBlock rules reject calls based on ANI (caller ID), DNIS (dialled number), or number codes. Active rules are synced to KeyDB for sub-millisecond lookup by OpenSIPS at INVITE time.\n\n**How it works:**\n- Queries `block_rules` from the **read replica**.\n- Supports filtering by `scope`, `scope_id`, `block_type`, and `is_active`.\n- Each rule specifies a `sip_response_code` (300–699) to return when matched.\n\n**Query parameters:**\n- `scope` — optional, `global`, `customer_trunk`, `carrier_trunk`, or `route`\n- `scope_id` — optional, the ID of the scoped entity\n- `block_type` — optional, `ani`, `dnis`, or `number_code`\n- `is_active` — optional, `true` or `false`\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"scope\": \"global\",\n      \"scope_id\": null,\n      \"block_type\": \"dnis\",\n      \"pattern\": \"1900\",\n      \"match_type\": \"prefix\",\n      \"sip_response_code\": 403,\n      \"description\": \"Block premium rate numbers\",\n      \"is_active\": true,\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — rules returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/block-rules","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/BlockRule"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Block Rules","tags":["Routing APIs - Block Rules"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).Create`\n\n---\n\nCreates a new ANI, DNIS, or number code block rule.\n\n**Purpose:**\nAdds a call-blocking rule. Active rules are immediately synced to KeyDB so OpenSIPS can enforce them on the next INVITE.\n\n**How it works:**\n- Inserts a `block_rules` record on the **write primary**.\n- If `is_active` is true, the rule is pushed to the KeyDB cache.\n- `match_type`: `exact` requires exact match; `prefix` matches if the number starts with the pattern.\n- `sip_response_code` (default: 403) is the SIP status returned when the rule triggers.\n\n**Request payload:**\n```json\n{\n  \"scope\": \"global\",\n  \"scope_id\": null,\n  \"block_type\": \"dnis\",\n  \"pattern\": \"1900\",\n  \"match_type\": \"prefix\",\n  \"sip_response_code\": 403,\n  \"description\": \"Block premium rate numbers\",\n  \"is_active\": true\n}\n```\n\n**Fields:**\n- `scope` (required) — `global`, `customer_trunk`, `carrier_trunk`, or `route`\n- `scope_id` — required when `scope` is not `global`\n- `block_type` (required) — `ani`, `dnis`, or `number_code`\n- `pattern` (required) — the number, prefix, or code to match\n- `match_type` (required) — `exact` or `prefix`\n- `sip_response_code` — SIP status code (300–699, default: 403)\n- `is_active` — defaults to `true`\n\n**Response (201 Created):** Returns the created rule record.\n\n**HTTP status codes:**\n- `201 Created` — rule created and cached\n- `400 Bad Request` — validation failure\n- `500 Internal Server Error` — database write or cache sync failed","operationId":"POST_/api/block-rules","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBlockRuleRequest"}}},"description":"Request body for handler.CreateBlockRuleRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockRule"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Block Rule","tags":["Routing APIs - Block Rules"]}},"/api/block-rules/rebuild-cache":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).RebuildCache`\n\n---\n\nClears all block keys from KeyDB and repopulates from active rules in PostgreSQL.\n\n**Purpose:**\nEmergency recovery tool for when the KeyDB cache is suspected to be out of sync with the database.\n\n**How it works:**\n- Deletes every key in KeyDB under the block-rules namespace.\n- Scans `block_rules` on the **read replica** for `is_active = true`.\n- Re-inserts each active rule into KeyDB.\n\n**When to use:**\n- After bulk imports or direct database edits that bypassed the API.\n- When the cache is suspected to be out of sync.\n- After KeyDB failover in an active-active cluster.\n\n**Response:**\n```json\n{\n  \"message\": \"block rule cache rebuilt\",\n  \"count\": 42\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — cache rebuilt; `count` shows number of rules synced\n- `500 Internal Server Error` — cache rebuild failed","operationId":"POST_/api/block-rules/rebuild-cache","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CacheRebuildResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Rebuild Block Rule Cache","tags":["Routing APIs - Block Rules"]}},"/api/block-rules/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).Delete`\n\n---\n\nPermanently deletes a block rule and removes it from the KeyDB cache.\n\n**Purpose:**\nRemoves a blocking rule entirely. The corresponding KeyDB key is deleted immediately.\n\n**How it works:**\n- Physically deletes the `block_rules` record from the **write primary**.\n- Removes the corresponding key from the KeyDB cache.\n\n**Path parameter:**\n- `id` — block rule ID\n\n**Warning:** This action is irreversible.\n\n**HTTP status codes:**\n- `204 No Content` — rule deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete or cache removal failed","operationId":"DELETE_/api/block-rules/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Block Rule","tags":["Routing APIs - Block Rules"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).Get`\n\n---\n\nRetrieves a single block rule by its database ID.\n\n**Path parameter:**\n- `id` — block rule ID\n\n**HTTP status codes:**\n- `200 OK` — rule returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no rule with that ID","operationId":"GET_/api/block-rules/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockRule"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Block Rule","tags":["Routing APIs - Block Rules"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).Update`\n\n---\n\nUpdates an existing block rule.\n\n**Purpose:**\nModifies a block rule's pattern, scope, or active state. If `is_active` changes, the KeyDB cache is automatically updated.\n\n**Path parameter:**\n- `id` — block rule ID\n\n**Request payload:** Same shape as Create.\n\n**HTTP status codes:**\n- `200 OK` — rule updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no rule with that ID\n- `500 Internal Server Error` — database write or cache sync failed","operationId":"PUT_/api/block-rules/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBlockRuleRequest"}}},"description":"Request body for handler.UpdateBlockRuleRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockRule"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Block Rule","tags":["Routing APIs - Block Rules"]}},"/api/block-rules/{id}/toggle":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BlockRuleHandler).ToggleActive`\n\n---\n\nActivates or deactivates a block rule and syncs the KeyDB cache.\n\n**Purpose:**\nQuickly enable or disable a block rule without deleting it. Useful for temporarily suspending a rule during troubleshooting.\n\n**How it works:**\n- Sets `is_active` to the value provided in the request body.\n- If activating, adds to KeyDB cache; if deactivating, removes from cache.\n\n**Path parameter:**\n- `id` — block rule ID\n\n**Request payload:**\n```json\n{\n  \"is_active\": false\n}\n```\n\n**Response:**\n```json\n{\n  \"id\": 1,\n  \"is_active\": false\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — state toggled\n- `400 Bad Request` — invalid ID or missing is_active\n- `500 Internal Server Error` — database or cache update failed","operationId":"POST_/api/block-rules/:id/toggle","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleActiveRequest"}}},"description":"Request body for handler.ToggleActiveRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToggleResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Toggle Block Rule Active State","tags":["Routing APIs - Block Rules"]}},"/api/call-admit/check":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*BalanceHandler).CallAdmitCheck`\n\n---\n\nChecks prepaid/postpaid headroom and atomically reserves the estimated call cost.\n\n**Purpose:**\nThe financial gate before a call is allowed to proceed. OpenSIPS calls this at INVITE time after authentication but before routing. If the customer lacks sufficient funds/credit, the call is rejected before any carrier resources are consumed.\n\n**How it works:**\n- Validates that the entity has sufficient balance headroom for the estimated call cost.\n- For `prepaid` entities: ensures `balance \u003e= estimated_cost`.\n- For `postpaid` entities: ensures `usage + estimated_cost \u003c= credit_limit`.\n- Atomically reserves the estimated cost by creating a `reserve` transaction in `balance_transactions`.\n- The reservation is later reconciled when the CDR is ingested (converted to a debit for the actual cost).\n\n**Request payload:**\n```json\n{\n  \"entity_type\": \"customer\",\n  \"entity_id\": 10,\n  \"estimated_cost\": \"0.50\"\n}\n```\n\n**Fields:**\n- `entity_type` (required) — `customer` or `carrier`\n- `entity_id` (required) — entity database ID (customer account ID or carrier trunk ID)\n- `estimated_cost` (required) — decimal string representing the worst-case call cost\n\n**Response (allowed):**\n```json\n{\n  \"allowed\": true,\n  \"headroom\": \"2499.50\",\n  \"message\": \"call admitted\"\n}\n```\n\n**Response (rejected):**\n```json\n{\n  \"allowed\": false,\n  \"headroom\": \"0.00\",\n  \"message\": \"insufficient balance\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — admission decision returned in body (check `allowed` field)\n- `400 Bad Request` — invalid entity_type, entity_id, or estimated_cost format\n- `403 Forbidden` — entity has insufficient headroom","operationId":"POST_/api/call-admit/check","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallAdmitCheckRequest"}}},"description":"Request body for handler.CallAdmitCheckRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckAndReserveResult"}}},"description":"OK"},"default":{"description":""}},"summary":"Call Admission Check","tags":["OpenSIPS Internal APIs - Call Admission"]}},"/api/carrier-trunks":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).List`\n\n---\n\nReturns all carrier trunks with optional filtering and pagination.\n\n**Purpose:**\nCarrier trunks are SIP egress endpoints that send calls to upstream carriers. Each carrier trunk defines capacity limits, gateway IPs, failover rules, and a cost rate deck. The LCR engine selects the cheapest eligible carrier trunk for each outbound call.\n\n**How it works:**\n- Queries `carrier_trunks` from the **read replica**.\n- Supports `limit` and `offset` for pagination.\n- Can filter by `status` (`active` or `closed`).\n- Each trunk can have multiple gateway IPs (for load balancing or failover) and SIP failover rules.\n\n**Query parameters:**\n- `limit` / `offset` — pagination\n- `status` — optional, filter by `active` or `closed`\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 20,\n      \"name\": \"Carrier A — Sydney\",\n      \"currency\": \"AUD\",\n      \"cps_limit\": 50,\n      \"port_limit\": 200,\n      \"per_minute_rate_limit\": 1000,\n      \"status\": \"active\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — trunks returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/carrier-trunks","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CarrierTrunk"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Carrier Trunks","tags":["Carrier APIs - Trunks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).Create`\n\n---\n\nCreates a new carrier trunk (SIP egress endpoint).\n\n**Purpose:**\nProvisions a new outbound route to an upstream carrier. After creation, gateway IPs, a cost rate deck, and failover rules must be added before the trunk can carry traffic.\n\n**How it works:**\n- Inserts a new `carrier_trunks` record on the **write primary**.\n- Applies default capacity limits if omitted: `cps_limit=10`, `port_limit=100`, `per_minute_rate_limit=600`.\n- Defaults `multi_ip_mode` to `load_balance` (round-robin across gateway IPs).\n- A corresponding `balances` row is automatically created.\n\n**Request payload:**\n```json\n{\n  \"name\": \"Carrier A — Sydney\",\n  \"currency\": \"AUD\",\n  \"cps_limit\": 50,\n  \"port_limit\": 200,\n  \"per_minute_rate_limit\": 1000,\n  \"sip_trusted\": false,\n  \"multi_ip_mode\": \"load_balance\",\n  \"status\": \"active\"\n}\n```\n\n**Fields:**\n- `name` (required) — human-readable carrier name\n- `currency` (required) — `AUD` or `USD`\n- `cps_limit` — max calls per second to this carrier (default: 10)\n- `port_limit` — max concurrent calls to this carrier (default: 100)\n- `per_minute_rate_limit` — max calls per rolling 60s window (default: 600)\n- `billing_mode` — `prepaid` or `postpaid` (default: `postpaid`)\n- `multi_ip_mode` — `load_balance` (round-robin) or `failover` (sequential by priority)\n- `sip_trusted` — if true, identity headers from this carrier are trusted\n- `status` — `active` (default), `suspended`, or `closed`\n\n**Response (201 Created):** Returns the created trunk record.\n\n**HTTP status codes:**\n- `201 Created` — trunk created\n- `400 Bad Request` — validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/carrier-trunks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCarrierTrunkRequest"}}},"description":"Request body for handler.CreateCarrierTrunkRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunk"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Carrier Trunk","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).Delete`\n\n---\n\nSoft-deletes a carrier trunk by setting its `status` to `closed`.\n\n**Purpose:**\nDecommissions a carrier trunk. The LCR engine will skip closed trunks when selecting routes for new calls.\n\n**How it works:**\n- Does NOT physically delete the trunk.\n- Sets `status = closed` on the **write primary**.\n- The LCR engine immediately excludes this trunk from route selection.\n- Existing active calls through the trunk are not terminated.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**HTTP status codes:**\n- `204 No Content` — trunk closed\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database update failed","operationId":"DELETE_/api/carrier-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Carrier Trunk","tags":["Carrier APIs - Trunks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).Get`\n\n---\n\nRetrieves a carrier trunk by its database ID, including its configured gateway IPs.\n\n**Purpose:**\nFetches the full carrier trunk configuration plus all associated gateway IP addresses in a single call.\n\n**How it works:**\n- Fetches the trunk from `carrier_trunks` on the **read replica**.\n- Also queries `carrier_trunk_ips` for all gateway IPs belonging to this trunk.\n- Returns both in a combined response: `{\"trunk\": {...}, \"ips\": [...]}`.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Response example:**\n```json\n{\n  \"trunk\": {\n    \"id\": 20,\n    \"name\": \"Carrier A — Sydney\",\n    \"currency\": \"AUD\",\n    \"cps_limit\": 50,\n    \"port_limit\": 200,\n    \"status\": \"active\"\n  },\n  \"ips\": [\n    {\n      \"id\": 1,\n      \"carrier_trunk_id\": 20,\n      \"ip_address\": \"203.0.113.50\",\n      \"port\": 5060,\n      \"priority\": 1,\n      \"status\": \"active\"\n    }\n  ]\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — trunk returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no trunk with that ID\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/carrier-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunkWithIPs"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Carrier Trunk","tags":["Carrier APIs - Trunks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).Update`\n\n---\n\nUpdates an existing carrier trunk.\n\n**Purpose:**\nModifies a carrier trunk's configuration — capacity limits, multi-IP mode, or status. Changes take effect immediately for subsequent LCR decisions.\n\n**How it works:**\n- Fetches the trunk by ID (returns 404 if not found).\n- Updates all fields from the request body on the **write primary**.\n- The `status` field is optional — if omitted, the current status is preserved.\n- Sets `updated_at` to now.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Request payload:** Same shape as Create. All fields except `status` are required.\n\n**Note:** Changes to capacity limits (`cps_limit`, `port_limit`, `per_minute_rate_limit`) take effect immediately on the next OpenSIPS call admission check.\n\n**HTTP status codes:**\n- `200 OK` — trunk updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no trunk with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/carrier-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCarrierTrunkRequest"}}},"description":"Request body for handler.UpdateCarrierTrunkRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunk"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Carrier Trunk","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}/failover-rules":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierSIPFailoverRuleHandler).List`\n\n---\n\nReturns all SIP failover rules configured for a carrier trunk.\n\n**Purpose:**\nFailover rules tell OpenSIPS how to handle specific SIP error responses from a carrier. For example, a `503 Service Unavailable` might trigger a retry on the next carrier, while a `404 Not Found` should be returned directly to the customer.\n\n**How it works:**\n- Queries `carrier_sip_failover_rules` on the **read replica** for rules belonging to the carrier trunk.\n- Each rule maps a SIP response code (100–699) to an action: `retry_next` or `return_to_customer`.\n- If no rule exists for a given SIP code, OpenSIPS defaults to `retry_next`.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"carrier_trunk_id\": 20,\n    \"sip_code\": 503,\n    \"action\": \"retry_next\",\n    \"description\": \"Retry on service unavailable\"\n  },\n  {\n    \"id\": 2,\n    \"carrier_trunk_id\": 20,\n    \"sip_code\": 404,\n    \"action\": \"return_to_customer\",\n    \"description\": \"Don't retry on not found\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — rules returned\n- `400 Bad Request` — invalid trunk ID\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/carrier-trunks/:id/failover-rules","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CarrierSIPFailoverRule"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Carrier SIP Failover Rules","tags":["Carrier APIs - Trunks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierSIPFailoverRuleHandler).Create`\n\n---\n\nAdds a new SIP code failover rule to a carrier trunk.\n\n**Purpose:**\nDefines how OpenSIPS should respond when a specific SIP error code is received from this carrier. This is critical for call reliability — without proper failover rules, transient carrier errors could be passed to the customer instead of retrying on an alternate route.\n\n**How it works:**\n- Maps a specific SIP response code (100–699) to an action: `retry_next` or `return_to_customer`.\n- Inserts into `carrier_sip_failover_rules` on the **write primary**.\n- OpenSIPS consults these rules after receiving a non-2xx final response from the carrier.\n- Common configurations:\n  - `503`, `504`, `408` → `retry_next` (transient failures)\n  - `404`, `486`, `603` → `return_to_customer` (definitive failures)\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Request payload:**\n```json\n{\n  \"sip_code\": 503,\n  \"action\": \"retry_next\",\n  \"description\": \"Retry on service unavailable\"\n}\n```\n\n**Fields:**\n- `sip_code` (required) — SIP response code, 100–699\n- `action` (required) — `retry_next` (try the next carrier in LCR order) or `return_to_customer` (pass the failure code to the calling party)\n- `description` (optional) — human-readable label\n\n**Response (201 Created):** Returns the created rule record.\n\n**HTTP status codes:**\n- `201 Created` — rule created\n- `400 Bad Request` — invalid SIP code, invalid action, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/carrier-trunks/:id/failover-rules","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCarrierSIPFailoverRuleRequest"}}},"description":"Request body for handler.CreateCarrierSIPFailoverRuleRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierSIPFailoverRule"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Carrier SIP Failover Rule","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}/failover-rules/{rule_id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierSIPFailoverRuleHandler).Delete`\n\n---\n\nRemoves a SIP failover rule from a carrier trunk.\n\n**Purpose:**\nDeletes a specific failover rule. After deletion, OpenSIPS will use the default behaviour (`retry_next`) for that SIP code.\n\n**How it works:**\n- Fetches the rule and verifies ownership by the carrier trunk.\n- Physically deletes the row from `carrier_sip_failover_rules` on the **write primary**.\n\n**Path parameters:**\n- `id` — carrier trunk ID\n- `rule_id` — failover rule ID\n\n**Note:** If no rule exists for a given SIP code, OpenSIPS defaults to `retry_next` behaviour.\n\n**HTTP status codes:**\n- `204 No Content` — rule deleted\n- `400 Bad Request` — rule doesn't belong to trunk\n- `404 Not Found` — rule not found\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/carrier-trunks/:id/failover-rules/:rule_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rule_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Carrier SIP Failover Rule","tags":["Carrier APIs - Trunks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierSIPFailoverRuleHandler).Get`\n\n---\n\nRetrieves a single SIP failover rule by ID, verifying it belongs to the specified carrier trunk.\n\n**Path parameters:**\n- `id` — carrier trunk ID\n- `rule_id` — failover rule ID\n\n**HTTP status codes:**\n- `200 OK` — rule returned\n- `400 Bad Request` — rule doesn't belong to this trunk\n- `404 Not Found` — rule not found","operationId":"GET_/api/carrier-trunks/:id/failover-rules/:rule_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rule_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierSIPFailoverRule"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Carrier SIP Failover Rule","tags":["Carrier APIs - Trunks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierSIPFailoverRuleHandler).Update`\n\n---\n\nUpdates an existing SIP failover rule, verifying ownership by the carrier trunk.\n\n**Path parameters:**\n- `id` — carrier trunk ID\n- `rule_id` — failover rule ID\n\n**Request payload:** Same shape as Create.\n\n**HTTP status codes:**\n- `200 OK` — rule updated; returns the updated record\n- `400 Bad Request` — rule doesn't belong to trunk, or validation failure\n- `404 Not Found` — rule not found\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/carrier-trunks/:id/failover-rules/:rule_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rule_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCarrierSIPFailoverRuleRequest"}}},"description":"Request body for handler.UpdateCarrierSIPFailoverRuleRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierSIPFailoverRule"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Carrier SIP Failover Rule","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}/ips":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).ListIPs`\n\n---\n\nReturns all IP addresses configured as egress gateways for a carrier trunk.\n\n**Purpose:**\nLists the SIP gateway IPs that OpenSIPS will forward calls to when this carrier trunk is selected by the LCR engine. Each IP has a priority and weight for load balancing or failover ordering.\n\n**How it works:**\n- Queries `carrier_trunk_ips` on the **read replica** for the given trunk ID.\n- IPs are returned with their `priority` (lower = preferred), `weight` (for weighted round-robin), and `status`.\n- The `multi_ip_mode` on the carrier trunk determines how these IPs are used: `load_balance` distributes calls across active IPs; `failover` tries them in priority order.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"carrier_trunk_id\": 20,\n    \"ip_address\": \"203.0.113.50\",\n    \"port\": 5060,\n    \"priority\": 1,\n    \"weight\": 1,\n    \"status\": \"active\",\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — IPs returned\n- `400 Bad Request` — invalid trunk ID\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/carrier-trunks/:id/ips","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CarrierTrunkIP"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Carrier Trunk IPs","tags":["Carrier APIs - Trunks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).AddIP`\n\n---\n\nAdds a new egress gateway IP address to a carrier trunk.\n\n**Purpose:**\nAdds a SIP gateway destination for outbound calls. A carrier trunk must have at least one active IP to carry traffic.\n\n**How it works:**\n- Validates the IP address format (must be a valid IPv4 or IPv6 address).\n- Inserts into `carrier_trunk_ips` on the **write primary**.\n- Defaults: `port=5060`, `weight=1`, `priority=1`, `status=active`.\n- OpenSIPS uses these IPs as next-hop destinations in the LCR routing decision.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Request payload:**\n```json\n{\n  \"ip_address\": \"203.0.113.50\",\n  \"port\": 5060,\n  \"weight\": 1,\n  \"priority\": 1,\n  \"status\": \"active\"\n}\n```\n\n**Fields:**\n- `ip_address` (required) — valid IPv4 or IPv6 address\n- `port` — SIP port (default: 5060)\n- `weight` — relative weight for weighted round-robin (default: 1)\n- `priority` — lower values are tried first in failover mode (default: 1)\n- `status` — `active` or `disabled` (default: `active`)\n\n**Response (201 Created):** Returns the created IP record.\n\n**HTTP status codes:**\n- `201 Created` — IP added\n- `400 Bad Request` — invalid IP, invalid trunk ID, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/carrier-trunks/:id/ips","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCarrierTrunkIPRequest"}}},"description":"Request body for handler.AddCarrierTrunkIPRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunkIP"}}},"description":"Created"},"default":{"description":""}},"summary":"Add Carrier Trunk IP","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}/ips/{ip_id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).RemoveIP`\n\n---\n\nRemoves a gateway IP address from a carrier trunk.\n\n**Purpose:**\nDecommissions a specific gateway destination. If this was the last active IP, the carrier trunk becomes unreachable.\n\n**How it works:**\n- Fetches the IP record and verifies ownership by the carrier trunk.\n- Physically deletes the row from `carrier_trunk_ips` on the **write primary**.\n- Returns `204 No Content`.\n\n**Path parameters:**\n- `id` — carrier trunk ID\n- `ip_id` — gateway IP record ID\n\n**Warning:** Removing the last IP from a carrier trunk will leave it unreachable for new calls. The LCR engine will skip it during route selection.\n\n**HTTP status codes:**\n- `204 No Content` — IP removed\n- `400 Bad Request` — IP doesn't belong to trunk\n- `404 Not Found` — IP not found\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/carrier-trunks/:id/ips/:ip_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"ip_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Remove Carrier Trunk IP","tags":["Carrier APIs - Trunks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CarrierTrunkHandler).UpdateIP`\n\n---\n\nUpdates an existing gateway IP address on a carrier trunk.\n\n**Purpose:**\nModifies a gateway IP's address, port, weight, priority, or status.\n\n**How it works:**\n- Fetches the IP record by `ip_id` and verifies it belongs to the specified carrier trunk.\n- Updates all fields on the **write primary**.\n- The `status` field is optional — if omitted, the current status is preserved.\n\n**Path parameters:**\n- `id` — carrier trunk ID\n- `ip_id` — gateway IP record ID\n\n**Request payload:** Same shape as Add IP. All fields except `status` are required.\n\n**HTTP status codes:**\n- `200 OK` — IP updated; returns the updated record\n- `400 Bad Request` — IP doesn't belong to trunk, or validation failure\n- `404 Not Found` — IP not found\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/carrier-trunks/:id/ips/:ip_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"ip_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCarrierTrunkIPRequest"}}},"description":"Request body for handler.UpdateCarrierTrunkIPRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunkIP"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Carrier Trunk IP","tags":["Carrier APIs - Trunks"]}},"/api/carrier-trunks/{id}/rate-deck":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).UnassignCarrierTrunkRateDeck`\n\n---\n\nRemoves the active rate deck assignment from a carrier trunk.\n\n**Purpose:**\nDetaches the cost rate deck. The carrier trunk will have no cost rates, causing the LCR engine to skip it during route selection.\n\n**How it works:**\n- Sets `superseded_at` on the current active assignment on the **write primary**.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**HTTP status codes:**\n- `204 No Content` — deck unassigned\n- `400 Bad Request` — invalid trunk ID\n- `500 Internal Server Error` — database update failed","operationId":"DELETE_/api/carrier-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Unassign Carrier Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).GetCarrierTrunkRateDeck`\n\n---\n\nReturns the active rate deck assigned to a carrier trunk.\n\n**Purpose:**\nShows which cost rate deck is currently active for a carrier trunk. This deck's rates determine what the carrier charges for calls.\n\n**How it works:**\n- Queries `rate_deck_assignments` on the **read replica** for the most recent active assignment.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**HTTP status codes:**\n- `200 OK` — assignment returned\n- `404 Not Found` — no active rate deck assigned","operationId":"GET_/api/carrier-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CarrierTrunkRateDeck"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Carrier Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).AssignCarrierTrunkRateDeck`\n\n---\n\nAssigns a cost rate deck to a carrier trunk.\n\n**Purpose:**\nSets the cost basis for a carrier. The LCR engine uses this deck's rates to calculate how much each route through this carrier will cost.\n\n**How it works:**\n- Inserts a new `rate_deck_assignments` record on the **write primary**.\n- Automatically supersedes any previous assignment.\n- The LCR engine uses this deck's rates to calculate `vendor_cost` on CDRs.\n\n**Path parameter:**\n- `id` — carrier trunk ID\n\n**Request payload:**\n```json\n{\n  \"rate_deck_id\": 2\n}\n```\n\n**Response:**\n```json\n{\n  \"message\": \"rate deck assigned\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — deck assigned\n- `400 Bad Request` — invalid IDs\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/carrier-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRateDeckRequest"}}},"description":"Request body for handler.AssignRateDeckRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Assign Carrier Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]}},"/api/cdrs/ingest":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CDRHandler).Ingest`\n\n---\n\nReceives a raw CDR from OpenSIPS and produces the final billed CDR.\n\n**Purpose:**\nThe endpoint where OpenSIPS (via the cdr-forwarder) sends completed call records. The Go API calculates both customer and vendor costs, writes the final CDR, and reconciles the balance reservation.\n\n**How it works:**\n- Accepts raw call data: timestamps, duration, trunk IDs, ANI, DNIS.\n- Looks up the customer trunk's sell rate deck and the carrier trunk's cost rate deck.\n- Performs longest-prefix-match on the DNIS against both rate decks.\n- Calculates `customer_cost` and `vendor_cost` using each trunk's `decimal_precision` and `rounding_direction`.\n- Applies billing increments (`billing_increment_initial` + `billing_increment_step`).\n- Writes the final CDR to the `cdrs` table on the **write primary**.\n- If the trunk is prepaid, reconciles the earlier reservation by converting it to a debit for the actual cost.\n- Both `customer_cost` and `vendor_cost` are mandatory on every CDR.\n\n**Request payload:**\n```json\n{\n  \"sip_call_id\": \"abc123@opensips\",\n  \"customer_trunk_id\": 10,\n  \"carrier_trunk_id\": 20,\n  \"carrier_gateway_id\": 1,\n  \"ani\": \"6125550100\",\n  \"dnis\": \"61400123456\",\n  \"start_time\": \"2026-01-15T08:30:00Z\",\n  \"answer_time\": \"2026-01-15T08:30:05Z\",\n  \"end_time\": \"2026-01-15T08:31:30Z\",\n  \"duration_seconds\": 90,\n  \"sip_response_code\": 200\n}\n```\n\n**Fields:**\n- `sip_call_id` (required) — unique SIP Call-ID for deduplication\n- `customer_trunk_id` (required) — the ingress customer trunk\n- `carrier_trunk_id` (required) — the egress carrier trunk used\n- `carrier_gateway_id` (required) — the specific gateway IP used\n- `ani` (required) — calling party number\n- `dnis` (required) — called party number\n- `start_time` (required) — RFC3339 timestamp of INVITE\n- `answer_time` — RFC3339 timestamp of 200 OK (null if unanswered)\n- `end_time` (required) — RFC3339 timestamp of BYE or failure\n- `duration_seconds` (required) — billable duration in seconds (0 for failed calls)\n- `sip_response_code` — final SIP status code\n\n**Response (201 Created):** Returns the created CDR with calculated `customer_cost`, `customer_currency`, `vendor_cost`, and `vendor_currency`.\n\n**HTTP status codes:**\n- `201 Created` — CDR ingested and billed\n- `400 Bad Request` — invalid timestamps, missing required fields, or invalid trunk IDs\n- `500 Internal Server Error` — CDR processing or database write failed","operationId":"POST_/api/cdrs/ingest","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestCDRRequest"}}},"description":"Request body for handler.IngestCDRRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestResult"}}},"description":"OK"},"default":{"description":""}},"summary":"Ingest CDR","tags":["Billing APIs - CDRs"]}},"/api/cdrs/search":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CDRHandler).Search`\n\n---\n\nQueries CDRs by date range, trunk, and other filters.\n\n**Purpose:**\nThe primary CDR query interface for billing reconciliation, troubleshooting, and reporting. Returns billed CDRs with both customer and vendor costs.\n\n**How it works:**\n- Queries `cdrs` from the **read replica**.\n- Supports flexible filters: date range, customer trunk, carrier trunk, SIP Call-ID.\n- Results include `customer_cost`, `customer_currency`, `vendor_cost`, and `vendor_currency` on every record.\n- Default limit is 1000 records max per query.\n\n**Query parameters:**\n- `start_time_from` — RFC3339, inclusive lower bound\n- `start_time_to` — RFC3339, inclusive upper bound\n- `customer_trunk_id` — optional, filter by customer trunk\n- `carrier_trunk_id` — optional, filter by carrier trunk\n- `sip_call_id` — optional, find a specific call\n- `limit` — max records (1–1000)\n- `offset` — records to skip\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"sip_call_id\": \"abc123@opensips\",\n      \"customer_trunk_id\": 10,\n      \"carrier_trunk_id\": 20,\n      \"ani\": \"6125550100\",\n      \"dnis\": \"61400123456\",\n      \"start_time\": \"2026-01-15T08:30:00Z\",\n      \"duration_seconds\": 90,\n      \"customer_cost\": \"0.0375\",\n      \"customer_currency\": \"AUD\",\n      \"vendor_cost\": \"0.0300\",\n      \"vendor_currency\": \"AUD\",\n      \"status\": \"answered\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — CDRs returned\n- `400 Bad Request` — invalid query parameters\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/cdrs/search","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CDR"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"Search CDRs","tags":["Billing APIs - CDRs"]}},"/api/customer-accounts":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerAccountHandler).List`\n\n---\n\nReturns all customer accounts with optional pagination.\n\n**Purpose:**\nCustomer accounts are the top-level billing entities in the wholesale switch. Each account can have multiple customer trunks (SIP ingress endpoints). This endpoint lists all accounts for management and reporting.\n\n**How it works:**\n- Queries `customer_accounts` from the **read replica**.\n- Supports `limit` and `offset` query parameters for pagination.\n- Returns account metadata including currency, GST settings, invoice period, and payment terms.\n- Accounts with `status = closed` are still returned (soft-deleted); filter client-side if needed.\n\n**Query parameters:**\n- `limit` — max records to return (default: unlimited)\n- `offset` — records to skip\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"Acme Telecom\",\n      \"currency\": \"AUD\",\n      \"gst_applicable\": true,\n      \"gst_treatment\": \"inclusive\",\n      \"invoice_period_days\": 30,\n      \"payment_due_days\": 7,\n      \"status\": \"active\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": {\n    \"page\": 1,\n    \"page_size\": 20,\n    \"total\": 1\n  }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — accounts returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/customer-accounts","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CustomerAccountWithTrunks"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Customer Accounts","tags":["Customer APIs - Accounts"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerAccountHandler).Create`\n\n---\n\nCreates a new wholesale customer account.\n\n**Purpose:**\nOnboards a new wholesale customer. The account holds billing configuration (currency, GST, invoice terms) that is inherited by all customer trunks created under it.\n\n**How it works:**\n- Inserts a new record into `customer_accounts` on the **write primary**.\n- Applies defaults: `invoice_period_days=30`, `payment_due_days=7`, `status=active`.\n- The account is immediately available for trunk creation.\n- GST fields (`gst_applicable`, `gst_treatment`) are stored for future invoicing but are NOT applied to CDR costs (GST is calculated at invoice time, not call time).\n\n**Request payload:**\n```json\n{\n  \"name\": \"Acme Telecom\",\n  \"currency\": \"AUD\",\n  \"gst_applicable\": true,\n  \"gst_treatment\": \"inclusive\",\n  \"invoice_period_days\": 30,\n  \"payment_due_days\": 7,\n  \"status\": \"active\"\n}\n```\n\n**Fields:**\n- `name` (required) — human-readable account name\n- `currency` (required) — `AUD` or `USD`; all trunks under this account should use the same currency\n- `gst_applicable` — whether GST applies to this account (informational for invoicing)\n- `gst_treatment` — `inclusive` or `exclusive` (required if `gst_applicable` is true)\n- `invoice_period_days` — billing cycle length in days (min: 1, default: 30)\n- `payment_due_days` — days after invoice before overdue (min: 1, default: 7)\n- `status` — `active` (default), `suspended`, or `closed`\n\n**Response (201 Created):** Returns the created account record with `id` and `created_at`.\n\n**HTTP status codes:**\n- `201 Created` — account created\n- `400 Bad Request` — invalid currency, missing name, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/customer-accounts","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerAccountRequest"}}},"description":"Request body for handler.CreateCustomerAccountRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerAccount"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Customer Account","tags":["Customer APIs - Accounts"]}},"/api/customer-accounts/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerAccountHandler).Delete`\n\n---\n\nSoft-deletes a customer account by setting its `status` to `closed`.\n\n**Purpose:**\nDecommissions a customer account without destroying historical data. Closed accounts are retained for CDR history and audit trails.\n\n**How it works:**\n- Does NOT physically delete the row from `customer_accounts`.\n- Sets `status = closed` on the **write primary**.\n- Prevents new trunks from being created under this account.\n- Existing trunks under the account remain in their current state (they must be closed individually).\n- All historical CDRs and balance transactions remain intact.\n\n**Path parameter:**\n- `id` — customer account ID\n\n**HTTP status codes:**\n- `204 No Content` — account closed\n- `400 Bad Request` — invalid ID format\n- `500 Internal Server Error` — database update failed","operationId":"DELETE_/api/customer-accounts/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Customer Account","tags":["Customer APIs - Accounts"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerAccountHandler).Get`\n\n---\n\nRetrieves a single customer account by its database ID.\n\n**Purpose:**\nFetches the full details of a customer account for display or edit pre-population.\n\n**Path parameter:**\n- `id` — customer account ID\n\n**Response:** Returns the full account record including all billing and GST fields.\n\n**HTTP status codes:**\n- `200 OK` — account returned\n- `400 Bad Request` — invalid ID format\n- `404 Not Found` — no account with that ID","operationId":"GET_/api/customer-accounts/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerAccountWithTrunks"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Customer Account","tags":["Customer APIs - Accounts"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerAccountHandler).Update`\n\n---\n\nUpdates an existing customer account.\n\n**Purpose:**\nModifies account-level settings such as billing terms, currency, or GST treatment. Changes do not cascade to existing CDRs but will affect future invoicing.\n\n**How it works:**\n- Fetches the existing account by ID (returns 404 if not found).\n- Updates all provided fields on the **write primary**.\n- Sets `updated_at` to the current time.\n- The `status` field is optional in the update body — if omitted, the current status is preserved.\n\n**Path parameter:**\n- `id` — customer account ID\n\n**Request payload:** Same shape as Create. All fields except `status` are required.\n\n**Note:** Setting `status` to `closed` effectively soft-deletes the account. Existing trunks remain active but no new trunks can be created under a closed account.\n\n**HTTP status codes:**\n- `200 OK` — account updated; returns the updated record\n- `400 Bad Request` — invalid ID or validation failure\n- `404 Not Found` — no account with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/customer-accounts/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerAccountRequest"}}},"description":"Request body for handler.UpdateCustomerAccountRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerAccount"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Customer Account","tags":["Customer APIs - Accounts"]}},"/api/customer-trunks":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerTrunkHandler).List`\n\n---\n\nReturns all customer trunks with optional filtering.\n\n**Purpose:**\nCustomer trunks are SIP ingress endpoints that receive calls from wholesale customers. Each trunk belongs to a customer account and defines capacity limits (CPS, ports, per-minute rate), billing mode (prepaid/postpaid), and routing parameters (tech prefix). This endpoint lists all configured trunks.\n\n**How it works:**\n- Queries `customer_trunks` from the **read replica**.\n- Can filter by `customer_account_id` to list trunks for a specific account.\n- Can filter by `status` (`active`, `suspended`, `closed`).\n- Supports `limit` and `offset` for pagination.\n- Monetary fields (`credit_limit`, `warning_threshold`) are returned as decimal strings, never floats.\n\n**Query parameters:**\n- `customer_account_id` — optional, filter by parent account\n- `status` — optional, filter by `active`, `suspended`, or `closed`\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 10,\n      \"customer_account_id\": 1,\n      \"name\": \"Acme Primary\",\n      \"currency\": \"AUD\",\n      \"cps_limit\": 10,\n      \"port_limit\": 30,\n      \"billing_mode\": \"postpaid\",\n      \"credit_limit\": \"5000.00\",\n      \"tech_prefix\": \"9999\",\n      \"status\": \"active\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — trunks returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/customer-trunks","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CustomerTrunk"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Customer Trunks","tags":["Customer APIs - Trunks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerTrunkHandler).Create`\n\n---\n\nCreates a new customer trunk (SIP ingress endpoint).\n\n**Purpose:**\nProvisions a new SIP ingress point for a wholesale customer. Once created, the trunk can receive calls after IP auth or SIP registration records are added and a rate deck is assigned.\n\n**How it works:**\n- Inserts a new `customer_trunks` record linked to a `customer_account` on the **write primary**.\n- Applies sensible defaults for limits and billing settings (see below).\n- `credit_limit` and `warning_threshold` are parsed as `decimal.Decimal` strings — never float64 — to avoid floating-point rounding errors.\n- The trunk starts in `active` status and is immediately available for call processing.\n- A corresponding `balances` row is automatically created by the balance service.\n\n**Request payload:**\n```json\n{\n  \"customer_account_id\": 1,\n  \"name\": \"Acme Primary\",\n  \"currency\": \"AUD\",\n  \"cps_limit\": 10,\n  \"port_limit\": 30,\n  \"per_minute_rate_limit\": 300,\n  \"billing_mode\": \"postpaid\",\n  \"credit_limit\": \"5000.00\",\n  \"warning_threshold\": \"1000.00\",\n  \"auto_cutoff_enabled\": false,\n  \"auto_cutoff_behaviour\": \"block_new\",\n  \"tech_prefix\": \"9999\",\n  \"sip_trusted\": false,\n  \"billing_increment_initial\": 1,\n  \"billing_increment_step\": 1,\n  \"decimal_precision\": 4,\n  \"rounding_direction\": \"standard\",\n  \"gst_applicable\": true,\n  \"gst_treatment\": \"inclusive\"\n}\n```\n\n**Fields:**\n- `customer_account_id` (required) — parent account ID\n- `name` (required) — human-readable trunk name\n- `currency` (required) — `AUD` or `USD`\n- `cps_limit` — max calls per second (default: 10)\n- `port_limit` — max concurrent calls (default: 30)\n- `per_minute_rate_limit` — max calls per rolling 60s window (default: 300)\n- `billing_mode` — `prepaid` or `postpaid` (default: `postpaid`)\n- `credit_limit` — postpaid credit ceiling as decimal string (e.g., `\"5000.00\"`)\n- `warning_threshold` — balance level that triggers a warning notification\n- `auto_cutoff_enabled` — whether to auto-block calls when credit exhausted\n- `auto_cutoff_behaviour` — `block_new` (reject new calls) or `terminate_all` (cut active calls)\n- `tech_prefix` — string prepended to DNIS for routing decisions, stripped before egress\n- `sip_trusted` — if true, PAI/PPI/RPID identity headers are trusted without verification\n- `billing_increment_initial` — first billing block in seconds (default: 1)\n- `billing_increment_step` — subsequent billing block in seconds (default: 1)\n- `decimal_precision` — 4, 5, or 6 decimal places for cost calculations (default: 4)\n- `rounding_direction` — `up`, `down`, or `standard` (default: `standard`)\n- `gst_applicable` — whether GST applies to this trunk\n- `gst_treatment` — `inclusive` or `exclusive`\n\n**Response (201 Created):** Returns the created trunk record with `id` and `created_at`.\n\n**HTTP status codes:**\n- `201 Created` — trunk created\n- `400 Bad Request` — invalid currency, invalid decimal values, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/customer-trunks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerTrunkRequest"}}},"description":"Request body for handler.CreateCustomerTrunkRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerTrunk"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Customer Trunk","tags":["Customer APIs - Trunks"]}},"/api/customer-trunks/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerTrunkHandler).Delete`\n\n---\n\nSoft-deletes a customer trunk by setting its `status` to `closed`.\n\n**Purpose:**\nDecommissions a customer trunk without destroying historical CDRs or balance records.\n\n**How it works:**\n- Does NOT physically delete the trunk from `customer_trunks`.\n- Sets `status = closed` on the **write primary**.\n- New INVITEs from this trunk will be rejected at the auth stage.\n- Existing active calls on the trunk are NOT terminated — they complete normally.\n- All historical CDRs and balance transactions remain intact for billing.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**HTTP status codes:**\n- `204 No Content` — trunk closed\n- `400 Bad Request` — invalid ID format\n- `500 Internal Server Error` — database update failed","operationId":"DELETE_/api/customer-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Customer Trunk","tags":["Customer APIs - Trunks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerTrunkHandler).Get`\n\n---\n\nRetrieves a customer trunk by its database ID.\n\n**Purpose:**\nFetches the full configuration of a customer trunk including all billing, routing, and capacity settings.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**Response:** Returns the full trunk record including all fields from Create. Monetary values are decimal strings.\n\n**HTTP status codes:**\n- `200 OK` — trunk returned\n- `400 Bad Request` — invalid ID format\n- `404 Not Found` — no trunk with that ID","operationId":"GET_/api/customer-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerTrunk"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Customer Trunk","tags":["Customer APIs - Trunks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*CustomerTrunkHandler).Update`\n\n---\n\nUpdates an existing customer trunk.\n\n**Purpose:**\nModifies a trunk's configuration — capacity limits, billing settings, tech prefix, or status. Changes to rate limits take effect immediately on the next OpenSIPS call check.\n\n**How it works:**\n- Fetches the trunk by ID (returns 404 if not found).\n- Updates all fields from the request body on the **write primary**.\n- Setting `credit_limit` or `warning_threshold` to `null` clears the value.\n- Setting `credit_limit` or `warning_threshold` to a string parses it as a decimal.\n- The `status` field is optional — if omitted, the current status is preserved.\n- Sets `updated_at` to now.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**Request payload:** Same shape as Create. All fields except `status` are required.\n\n**Note:** Changes to `cps_limit`, `port_limit`, or `per_minute_rate_limit` take effect immediately on the next OpenSIPS INVITE check. No restart or reload is needed.\n\n**HTTP status codes:**\n- `200 OK` — trunk updated; returns the updated record\n- `400 Bad Request` — invalid ID, invalid decimal, or validation failure\n- `404 Not Found` — no trunk with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/customer-trunks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerTrunkRequest"}}},"description":"Request body for handler.UpdateCustomerTrunkRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerTrunk"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Customer Trunk","tags":["Customer APIs - Trunks"]}},"/api/customer-trunks/{id}/rate-deck":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).UnassignCustomerTrunkRateDeck`\n\n---\n\nRemoves the active rate deck assignment from a customer trunk.\n\n**Purpose:**\nDetaches the sell rate deck from a trunk. After unassignment, the trunk has no pricing and calls will be rejected with `no route` until a new deck is assigned.\n\n**How it works:**\n- Sets `superseded_at` on the current active assignment on the **write primary**.\n- This is a soft unassignment — the assignment record is preserved for audit history.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**HTTP status codes:**\n- `204 No Content` — deck unassigned\n- `400 Bad Request` — invalid trunk ID\n- `500 Internal Server Error` — database update failed","operationId":"DELETE_/api/customer-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Unassign Customer Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).GetCustomerTrunkRateDeck`\n\n---\n\nReturns the active rate deck assigned to a customer trunk.\n\n**Purpose:**\nShows which sell rate deck is currently active for a customer trunk. This deck's rates determine what the customer is charged for calls.\n\n**How it works:**\n- Queries `rate_deck_assignments` on the **read replica** for the most recent active assignment where `superseded_at IS NULL`.\n- Returns the full rate deck metadata.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**HTTP status codes:**\n- `200 OK` — assignment returned\n- `404 Not Found` — no active rate deck assigned","operationId":"GET_/api/customer-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerTrunkRateDeck"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Customer Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).AssignCustomerTrunkRateDeck`\n\n---\n\nAssigns a sell rate deck to a customer trunk.\n\n**Purpose:**\nSets the pricing for a customer's calls. Without an assigned rate deck, calls from this trunk cannot be routed (the LCR engine needs a sell price to calculate margin).\n\n**How it works:**\n- Inserts a new `rate_deck_assignments` record linking the trunk to the deck on the **write primary**.\n- Automatically supersedes any previous assignment by setting its `superseded_at` timestamp.\n- The new assignment is effective immediately for the LCR engine.\n- Only one assignment can be active at a time per trunk.\n\n**Path parameter:**\n- `id` — customer trunk ID\n\n**Request payload:**\n```json\n{\n  \"rate_deck_id\": 1\n}\n```\n\n**Response:**\n```json\n{\n  \"message\": \"rate deck assigned\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — deck assigned\n- `400 Bad Request` — invalid trunk ID or rate deck ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/customer-trunks/:id/rate-deck","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRateDeckRequest"}}},"description":"Request body for handler.AssignRateDeckRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Assign Customer Trunk Rate Deck","tags":["Pricing APIs - Rate Decks"]}},"/api/exchange-rates":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*ExchangeRateHandler).List`\n\n---\n\nReturns all exchange rates with optional filtering.\n\n**Purpose:**\nExchange rates enable cross-currency routing. When a customer trunk is in AUD but a carrier trunk is in USD, the LCR engine uses the current exchange rate to normalise costs into a single currency for comparison.\n\n**How it works:**\n- Queries `exchange_rates` from the **read replica**.\n- Can filter by `from_currency` and `to_currency` query parameters.\n- Supports `limit` and `offset` for pagination.\n- Rates are decimal strings (e.g., `\"1.5500\"` means 1 USD = 1.55 AUD).\n\n**Query parameters:**\n- `from_currency` — optional, e.g., `USD`\n- `to_currency` — optional, e.g., `AUD`\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"from_currency\": \"USD\",\n      \"to_currency\": \"AUD\",\n      \"rate\": \"1.5500\",\n      \"effective_at\": \"2026-01-01T00:00:00Z\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — rates returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/exchange-rates","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ExchangeRate"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Exchange Rates","tags":["Pricing APIs - Exchange Rates"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*ExchangeRateHandler).Create`\n\n---\n\nAdds a new exchange rate between two currencies.\n\n**Purpose:**\nDefines the conversion rate for cross-currency cost calculations. Without a current rate for a currency pair, the LCR engine cannot compare costs across different currencies.\n\n**How it works:**\n- Parses `rate` as a `decimal.Decimal` string.\n- Inserts into `exchange_rates` on the **write primary**.\n- If `effective_at` is omitted, defaults to the current time.\n- The LCR engine uses the most recent rate where `effective_at \u003c= now`.\n\n**Request payload:**\n```json\n{\n  \"from_currency\": \"USD\",\n  \"to_currency\": \"AUD\",\n  \"rate\": \"1.5500\",\n  \"effective_at\": \"2026-01-01T00:00:00Z\"\n}\n```\n\n**Fields:**\n- `from_currency` (required) — source currency code (`AUD` or `USD`)\n- `to_currency` (required) — target currency code (`AUD` or `USD`); must differ from `from_currency`\n- `rate` (required) — how many `to_currency` units one `from_currency` unit buys (e.g., `\"1.5500\"` means 1 USD buys 1.55 AUD)\n- `effective_at` — RFC3339 timestamp; only rates with `effective_at \u003c= now` are considered current (default: now)\n\n**Response (201 Created):** Returns the created exchange rate record.\n\n**HTTP status codes:**\n- `201 Created` — rate created\n- `400 Bad Request` — invalid rate value, invalid currency, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/exchange-rates","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExchangeRateRequest"}}},"description":"Request body for handler.CreateExchangeRateRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeRate"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Exchange Rate","tags":["Pricing APIs - Exchange Rates"]}},"/api/exchange-rates/current":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*ExchangeRateHandler).Current`\n\n---\n\nReturns the most recent effective exchange rate for a currency pair.\n\n**Purpose:**\nThe LCR engine's primary exchange rate lookup. Returns the rate that should be used for cost normalisation right now.\n\n**How it works:**\n- Finds the exchange rate with the latest `effective_at` that is \u003c= now for the given currency pair.\n- Returns `404` if no rate exists for the pair.\n\n**Query parameters:**\n- `from_currency` (required) — e.g., `USD`\n- `to_currency` (required) — e.g., `AUD`\n\n**Response example:**\n```json\n{\n  \"from_currency\": \"USD\",\n  \"to_currency\": \"AUD\",\n  \"rate\": \"1.5500\",\n  \"effective_at\": \"2026-01-01T00:00:00Z\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — current rate returned\n- `400 Bad Request` — missing required query parameters\n- `404 Not Found` — no rate exists for this currency pair","operationId":"GET_/api/exchange-rates/current","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeRate"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Current Exchange Rate","tags":["Pricing APIs - Exchange Rates"]}},"/api/exchange-rates/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*ExchangeRateHandler).Delete`\n\n---\n\nRemoves an exchange rate by ID.\n\n**Purpose:**\nDeletes a historical or erroneous exchange rate entry.\n\n**How it works:**\n- Physically deletes the `exchange_rates` record from the **write primary**.\n\n**Path parameter:**\n- `id` — exchange rate ID\n\n**Warning:** Deleting the only current rate for a currency pair will cause the LCR engine to fail currency conversion for affected routes.\n\n**HTTP status codes:**\n- `204 No Content` — rate deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/exchange-rates/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Exchange Rate","tags":["Pricing APIs - Exchange Rates"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*ExchangeRateHandler).Get`\n\n---\n\nRetrieves a single exchange rate by its database ID.\n\n**Path parameter:**\n- `id` — exchange rate ID\n\n**HTTP status codes:**\n- `200 OK` — rate returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no rate with that ID","operationId":"GET_/api/exchange-rates/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeRate"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Exchange Rate","tags":["Pricing APIs - Exchange Rates"]}},"/api/health":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*HealthHandler).Check`\n\n---\n\nReturns the health status of the API and its dependent services.\n\n**Purpose:**\nThis is the primary liveness/readiness probe for the softswitch API. Load balancers, Kubernetes, and monitoring systems should poll this endpoint to determine whether the API instance is healthy and able to serve traffic.\n\n**What it checks:**\n- **Valkey connectivity:** Pings the local Valkey cache (used for LCR result caching, block rule lookups, and rate deck scheduler state).\n- If Valkey is unreachable, the API is still partially functional but LCR lookups will be slower (fallback to direct DB queries) and the block rule cache may be stale.\n\n**Authentication:**\nThis endpoint is **exempt from IP-based authentication**. It can be called from any source IP without an entry in the `auth_ips` table. This allows load balancer health checks to work without whitelisting every probe source.\n\n**Response (healthy):**\n```json\n{\n  \"status\": \"ok\",\n  \"valkey\": \"ok\"\n}\n```\n\n**Response (valkey unreachable):**\n```json\n{\n  \"status\": \"ok\",\n  \"valkey\": \"unreachable\"\n}\n```\n\n**Response (valkey disabled):**\n```json\n{\n  \"status\": \"ok\",\n  \"valkey\": \"disabled\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — always returned while the API process is running (the endpoint does not return 503 on dependency failure; check the `valkey` field instead)","operationId":"GET_/api/health","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"System Health Check","tags":["Admin APIs - System"]}},"/api/lcr-config":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRConfigHandler).List`\n\n---\n\nReturns all LCR configuration entries.\n\n**Purpose:**\nLCR config controls margin protection and caching behaviour for the Least Cost Routing engine. System-wide defaults apply when `rate_deck_id` is null; per-deck overrides take precedence for specific rate decks.\n\n**How it works:**\n- Queries `lcr_config` from the **read replica**.\n- Includes system-wide defaults (`rate_deck_id = NULL`) and per-rate-deck overrides.\n- The LCR engine merges these settings at lookup time: per-deck overrides win, falling back to system defaults.\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"rate_deck_id\": null,\n      \"block_if_cost_exceeds_sell\": true,\n      \"acceptable_loss_pct\": \"5.00\",\n      \"cache_ttl_seconds\": 300,\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — configs returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/lcr-config","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/LCRConfig"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List LCR Config","tags":["Routing APIs - LCR"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRConfigHandler).Create`\n\n---\n\nCreates a new LCR configuration entry.\n\n**Purpose:**\nSets margin-protection rules for the LCR engine. These rules determine whether a route is profitable enough to use.\n\n**How it works:**\n- If `rate_deck_id` is omitted (null), the config applies system-wide as the default.\n- If `rate_deck_id` is set, it overrides the system default for that specific rate deck.\n- `acceptable_loss_pct` is parsed as a `decimal.Decimal`.\n- Defaults `cache_ttl_seconds` to 300 if 0.\n\n**Request payload:**\n```json\n{\n  \"rate_deck_id\": null,\n  \"block_if_cost_exceeds_sell\": true,\n  \"acceptable_loss_pct\": \"5.00\",\n  \"cache_ttl_seconds\": 300\n}\n```\n\n**Fields:**\n- `rate_deck_id` — optional; when set, this config overrides the system default for that specific rate deck\n- `block_if_cost_exceeds_sell` — if true, routes where carrier cost \u003e= customer sell price are discarded (no negative margin)\n- `acceptable_loss_pct` — decimal percentage (e.g., `\"5.00\"` for 5%) of loss that is still acceptable before blocking\n- `cache_ttl_seconds` — how long LCR results are cached in Valkey (default: 300)\n\n**Response (201 Created):** Returns the created config record.\n\n**HTTP status codes:**\n- `201 Created` — config created\n- `400 Bad Request` — invalid acceptable_loss_pct or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/lcr-config","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLCRConfigRequest"}}},"description":"Request body for handler.CreateLCRConfigRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LCRConfig"}}},"description":"Created"},"default":{"description":""}},"summary":"Create LCR Config","tags":["Routing APIs - LCR"]}},"/api/lcr-config/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRConfigHandler).Delete`\n\n---\n\nRemoves a per-rate-deck LCR config override.\n\n**Path parameter:**\n- `id` — LCR config ID\n\n**Warning:** Deleting a system-wide default config (where `rate_deck_id` is null) is strongly discouraged because the LCR engine will have no fallback margin rules.\n\n**HTTP status codes:**\n- `204 No Content` — config deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/lcr-config/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete LCR Config","tags":["Routing APIs - LCR"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRConfigHandler).Get`\n\n---\n\nRetrieves an LCR config entry by its database ID.\n\n**Path parameter:**\n- `id` — LCR config ID\n\n**HTTP status codes:**\n- `200 OK` — config returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no config with that ID","operationId":"GET_/api/lcr-config/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LCRConfig"}}},"description":"OK"},"default":{"description":""}},"summary":"Get LCR Config","tags":["Routing APIs - LCR"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRConfigHandler).Update`\n\n---\n\nUpdates an existing LCR config entry.\n\n**Purpose:**\nAdjusts margin protection thresholds or cache TTL. Changes take effect immediately — the LCR engine reads config on every lookup.\n\n**Path parameter:**\n- `id` — LCR config ID\n\n**Request payload:** Same shape as Create.\n\n**HTTP status codes:**\n- `200 OK` — config updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no config with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/lcr-config/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLCRConfigRequest"}}},"description":"Request body for handler.UpdateLCRConfigRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LCRConfig"}}},"description":"OK"},"default":{"description":""}},"summary":"Update LCR Config","tags":["Routing APIs - LCR"]}},"/api/lcr/lookup":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*LCRHandler).Lookup`\n\n---\n\nRuns the full LCR engine for a customer trunk + destination and returns the sorted carrier list with costs and margin status.\n\n**Purpose:**\nThe definitive debugging tool for understanding routing decisions. Shows exactly which carriers are considered, their costs, margins, and why each is allowed or blocked.\n\n**How it works:**\n- Looks up the customer trunk's assigned sell rate deck.\n- Finds all carrier trunks with matching cost rate decks in the allowlist.\n- Calculates per-carrier cost using longest-prefix-match and currency conversion via exchange rates.\n- Sorts by cost ascending, then applies LCR config margin filters.\n- Returns the FULL candidate list (not just the winner) so you can see why carriers were rejected.\n\n**Request payload:**\n```json\n{\n  \"customer_trunk_id\": 10,\n  \"dnis\": \"61400123456\"\n}\n```\n\n**Response example:**\n```json\n[\n  {\n    \"carrier_trunk_id\": 20,\n    \"carrier_name\": \"Carrier A — Sydney\",\n    \"cost\": \"0.0200\",\n    \"sell\": \"0.0250\",\n    \"margin_pct\": \"20.00\",\n    \"allowed\": true,\n    \"reason\": \"ok\"\n  },\n  {\n    \"carrier_trunk_id\": 21,\n    \"carrier_name\": \"Carrier B — Melbourne\",\n    \"cost\": \"0.0260\",\n    \"sell\": \"0.0250\",\n    \"margin_pct\": \"-4.00\",\n    \"allowed\": false,\n    \"reason\": \"cost_exceeds_sell\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — LCR result returned\n- `400 Bad Request` — invalid customer_trunk_id or missing dnis\n- `500 Internal Server Error` — LCR engine error","operationId":"POST_/api/lcr/lookup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LCRLookupRequest"}}},"description":"Request body for handler.LCRLookupRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LCRResult"}}},"description":"OK"},"default":{"description":""}},"summary":"LCR Debug Lookup","tags":["Routing APIs - LCR"]}},"/api/metrics/cps":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*MetricsHandler).GetCPS`\n\n---\n\nReturns real-time CPS and port utilisation from KeyDB.\n\n**Purpose:**\nLive monitoring of call rates and concurrent call counts per trunk. OpenSIPS increments KeyDB counters on every INVITE (CPS) and dialog creation (ports). This endpoint reads those counters for dashboards and alerting.\n\n**How it works:**\n- Reads KeyDB counters at keys `rl:\u003ctrunk_id\u003e` (CPS), `ports:\u003ctrunk_id\u003e` (concurrent calls), and `per_min:\u003ctrunk_id\u003e` (rolling 60s count).\n- If `trunk_id` is specified, returns metrics for that single trunk.\n- If no `trunk_id` is given, scans all `per_min:*` keys and returns metrics for every active trunk.\n- Counters are approximate (OpenSIPS increments them asynchronously) and reset periodically (CPS counters reset each second).\n\n**Query parameters:**\n- `trunk_type` — optional, `customer` or `carrier`\n- `trunk_id` — optional, filter to a specific trunk\n\n**Response example (single trunk):**\n```json\n{\n  \"trunk_type\": \"customer\",\n  \"trunk_id\": 10,\n  \"cps_current\": 3,\n  \"ports_current\": 12,\n  \"per_minute_current\": 45,\n  \"timestamp\": \"2026-01-15T08:30:00Z\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — metrics returned\n- `400 Bad Request` — invalid query parameters\n- `500 Internal Server Error` — KeyDB read failed","operationId":"GET_/api/metrics/cps","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/CPSPortMetricsResponse"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"Get CPS and Port Metrics","tags":["Admin APIs - Metrics"]}},"/api/metrics/per-minute":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*MetricsHandler).GetPerMinute`\n\n---\n\nReturns per-minute call counts from KeyDB.\n\n**Purpose:**\nReads the rolling 60-second call counters for rate limit monitoring. These are the same counters checked by `POST /api/metrics/per-minute/check`.\n\n**How it works:**\n- Reads KeyDB keys at `per_min:\u003ctrunk_id\u003e`.\n- If `trunk_id` is specified, returns the count for that single trunk.\n- If no `trunk_id` is given, scans all `per_min:*` keys and returns counts for every trunk.\n- Counters auto-expire after 60 seconds of inactivity.\n\n**Query parameters:**\n- `trunk_id` — optional, filter to a specific trunk\n\n**Response example (single trunk):**\n```json\n{\n  \"trunk_id\": 10,\n  \"per_minute_current\": 45,\n  \"timestamp\": \"2026-01-15T08:30:00Z\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — metrics returned\n- `400 Bad Request` — invalid query parameters\n- `500 Internal Server Error` — KeyDB read or scan failed","operationId":"GET_/api/metrics/per-minute","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/PerMinuteRateLimitResponse"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Per-Minute Rate Limit Metrics","tags":["Admin APIs - Metrics"]}},"/api/metrics/per-minute/check":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*MetricsHandler).CheckPerMinute`\n\n---\n\nIncrements and checks the rolling 60-second per-minute rate limit counter in KeyDB.\n\n**Purpose:**\nThe runtime rate-limit enforcement endpoint. OpenSIPS calls this on every INVITE (after CPS check) to ensure the trunk hasn't exceeded its per-minute call limit. Returns `allowed: false` with HTTP 429 if the limit is exceeded.\n\n**How it works:**\n- Reads the current counter value from KeyDB at `per_min:\u003ctrunk_id\u003e`.\n- If `current \u003e= limit`, returns `429 Too Many Requests` with `allowed: false`.\n- Otherwise, atomically increments the counter (`INCR`) and sets a 60-second TTL on first increment.\n- The counter is a simple integer — it does NOT use a sliding window algorithm. It resets 60 seconds after the first call in the window.\n\n**Request payload:**\n```json\n{\n  \"trunk_id\": 10,\n  \"limit\": 300\n}\n```\n\n**Fields:**\n- `trunk_id` (required) — the trunk to check\n- `limit` (required) — the per-minute rate limit to enforce\n\n**Response (allowed):**\n```json\n{\n  \"allowed\": true,\n  \"current\": 45,\n  \"limit\": 300\n}\n```\n\n**Response (exceeded):**\n```json\n{\n  \"allowed\": false,\n  \"current\": 301,\n  \"limit\": 300\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — call allowed (counter incremented)\n- `429 Too Many Requests` — rate limit exceeded\n- `400 Bad Request` — invalid trunk_id or limit\n- `500 Internal Server Error` — KeyDB operation failed","operationId":"POST_/api/metrics/per-minute/check","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PerMinuteRateLimitRequest"}}},"description":"Request body for handler.PerMinuteRateLimitRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PerMinuteRateLimitResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Check Per-Minute Rate Limit","tags":["Admin APIs - Metrics"]}},"/api/opensips-nodes":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsNodeHandler).List`\n\n---\n\nReturns all registered OpenSIPS proxy nodes.\n\n**Purpose:**\nTracks the OpenSIPS instances in the cluster. The routing sync process (`POST /api/opensips/routing/sync`) uses this registry to know which nodes to send `dr_reload` MI commands to after rebuilding routing tables.\n\n**How it works:**\n- Queries `opensips_nodes` from the **read replica**.\n- Can filter by `status` (`active`, `draining`, `offline`).\n- Each node has an `mi_socket_address` (FIFO path, UDP address, or XMLRPC URL) for sending MI commands.\n- `last_seen_at` is updated on successful MI communication.\n\n**Query parameters:**\n- `status` — optional filter (`active`, `draining`, `offline`)\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"hostname\": \"opensips-01.syd.crazytel.internal\",\n      \"mi_socket_address\": \"/var/run/opensips/mi_fifo\",\n      \"region\": \"sydney\",\n      \"status\": \"active\",\n      \"last_seen_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — nodes returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/opensips-nodes","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/OpensipsNode"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List OpenSIPS Nodes","tags":["Admin APIs - OpenSIPS Nodes"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsNodeHandler).Create`\n\n---\n\nRegisters a new OpenSIPS proxy node for MI command coordination.\n\n**Purpose:**\nAdds an OpenSIPS instance to the cluster registry. Once registered, the node will receive `dr_reload` commands during routing syncs.\n\n**How it works:**\n- Inserts an `opensips_nodes` record on the **write primary**.\n- Sets `last_seen_at` to the current time.\n- Defaults `status` to `active`.\n\n**Request payload:**\n```json\n{\n  \"hostname\": \"opensips-01.syd.crazytel.internal\",\n  \"mi_socket_address\": \"/var/run/opensips/mi_fifo\",\n  \"region\": \"sydney\",\n  \"status\": \"active\"\n}\n```\n\n**Fields:**\n- `hostname` (required) — FQDN of the OpenSIPS node\n- `mi_socket_address` (required) — path or address for MI commands (FIFO path, `udp:host:port`, or `http://host:port/xmlrpc`)\n- `region` (optional) — datacenter or region identifier for geographic awareness\n- `status` — `active` (default), `draining` (skip during syncs), or `offline`\n\n**Response (201 Created):** Returns the created node record.\n\n**HTTP status codes:**\n- `201 Created` — node registered\n- `400 Bad Request` — validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/opensips-nodes","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOpensipsNodeRequest"}}},"description":"Request body for handler.CreateOpensipsNodeRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsNode"}}},"description":"Created"},"default":{"description":""}},"summary":"Register OpenSIPS Node","tags":["Admin APIs - OpenSIPS Nodes"]}},"/api/opensips-nodes/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsNodeHandler).Delete`\n\n---\n\nPermanently removes an OpenSIPS node from the registry.\n\n**Purpose:**\nDecommissions a node that has been permanently removed from the cluster.\n\n**How it works:**\n- Physically deletes the `opensips_nodes` record from the **write primary**.\n\n**Path parameter:**\n- `id` — OpenSIPS node ID\n\n**Warning:** Deleting an active node will prevent routing syncs from reaching it. Ensure the node is truly decommissioned before deregistering.\n\n**HTTP status codes:**\n- `204 No Content` — node deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/opensips-nodes/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Deregister OpenSIPS Node","tags":["Admin APIs - OpenSIPS Nodes"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsNodeHandler).Get`\n\n---\n\nRetrieves a single OpenSIPS node by its database ID.\n\n**Path parameter:**\n- `id` — OpenSIPS node ID\n\n**HTTP status codes:**\n- `200 OK` — node returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no node with that ID","operationId":"GET_/api/opensips-nodes/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsNode"}}},"description":"OK"},"default":{"description":""}},"summary":"Get OpenSIPS Node","tags":["Admin APIs - OpenSIPS Nodes"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsNodeHandler).Update`\n\n---\n\nUpdates an existing OpenSIPS node registration.\n\n**Purpose:**\nChanges a node's hostname, MI address, region, or status. Setting `status` to `draining` gracefully removes the node from routing syncs ahead of maintenance.\n\n**How it works:**\n- Fetches the node by ID.\n- Updates all fields on the **write primary**.\n- The `status` field is optional — if omitted, the current status is preserved.\n- Updates `last_seen_at` to now.\n\n**Path parameter:**\n- `id` — OpenSIPS node ID\n\n**Request payload:** Same shape as Create.\n\n**HTTP status codes:**\n- `200 OK` — node updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no node with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/opensips-nodes/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOpensipsNodeRequest"}}},"description":"Request body for handler.UpdateOpensipsNodeRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsNode"}}},"description":"OK"},"default":{"description":""}},"summary":"Update OpenSIPS Node","tags":["Admin APIs - OpenSIPS Nodes"]}},"/api/opensips/call/admit":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsHandler).Admit`\n\n---\n\nChecks prepaid/postpaid headroom for an OpenSIPS call and reserves the estimated customer cost.\n\n**Purpose:**\nThe financial gate in the SIP pipeline. Called after auth but before routing. Ensures the customer has sufficient funds/credit before the switch spends resources on LCR lookups and carrier routing.\n\n**How it works:**\n- Calculates the estimated call cost from the customer trunk's sell rate deck using longest-prefix-match on the DNIS.\n- Checks balance headroom (same logic as `POST /api/call-admit/check`).\n- If sufficient, atomically reserves the estimated cost by creating a `reserve` transaction.\n- The reservation is later reconciled when the CDR is ingested (converted to a debit for the actual cost).\n\n**Request payload (from OpenSIPS):**\n```json\n{\n  \"customer_trunk_id\": 10,\n  \"dnis\": \"61400123456\",\n  \"call_id\": \"abc123@opensips\"\n}\n```\n\n**Response (allowed):**\n```json\n{\n  \"allowed\": true,\n  \"headroom\": \"2499.50\"\n}\n```\n\n**Response (rejected):**\n```json\n{\n  \"allowed\": false,\n  \"reject_code\": 402,\n  \"reject_reason\": \"insufficient balance\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — always returned; check the `allowed` field\n- `400 Bad Request` — malformed request\n- `500 Internal Server Error` — unexpected error","operationId":"POST_/api/opensips/call/admit","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsAdmitRequest"}}},"description":"Request body for service.OpensipsAdmitRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsAdmitResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"OpenSIPS Call Admission","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/opensips/call/auth":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsHandler).Auth`\n\n---\n\nAuthorises an OpenSIPS ingress INVITE by source IP.\n\n**Purpose:**\nThe first call-processing endpoint in the SIP pipeline. OpenSIPS calls this on every incoming INVITE to identify the customer trunk, normalise numbers, check block rules, and return rate limits.\n\n**How it works:**\n- Matches the `source_ip` against `trunk_ip_auth` CIDR entries to identify the customer trunk.\n- Normalises ANI and DNIS (strips leading `+`, validates E.164 length).\n- Checks block rules (ANI/DNIS deny lists) via the in-memory KeyDB cache.\n- Returns runtime limits (`cps_limit`, `port_limit`, `per_minute_rate_limit`) for OpenSIPS to enforce locally.\n- Also returns `sip_trusted` flag — if true, OpenSIPS will trust PAI/PPI/RPID identity headers from this trunk.\n\n**Request payload (from OpenSIPS):**\n```json\n{\n  \"source_ip\": \"203.0.113.10\",\n  \"call_id\": \"abc123@opensips\",\n  \"ani\": \"6125550100\",\n  \"dnis\": \"61400123456\",\n  \"ruri\": \"sip:61400123456@sip.crazytel.com.au\",\n  \"from\": \"\\\"Alice\\\" \u003csip:6125550100@sip.crazytel.com.au\u003e\",\n  \"to\": \"\\\"Bob\\\" \u003csip:61400123456@sip.crazytel.com.au\u003e\",\n  \"pai\": \"\",\n  \"ppi\": \"\",\n  \"rpid\": \"\",\n  \"privacy\": \"none\"\n}\n```\n\n**Response (allowed):**\n```json\n{\n  \"allowed\": true,\n  \"trunk_type\": \"customer\",\n  \"customer_trunk_id\": 10,\n  \"ani\": \"6125550100\",\n  \"dnis\": \"61400123456\",\n  \"cps_limit\": 10,\n  \"port_limit\": 30,\n  \"per_minute_rate_limit\": 300,\n  \"sip_trusted\": false\n}\n```\n\n**Response (rejected — IP not authorised):**\n```json\n{\n  \"allowed\": false,\n  \"reject_code\": 403,\n  \"reject_reason\": \"source ip not authorised\"\n}\n```\n\n**Response (rejected — blocked ANI/DNIS):**\n```json\n{\n  \"allowed\": false,\n  \"reject_code\": 403,\n  \"reject_reason\": \"dnis blocked\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — always returned; check the `allowed` field for the auth decision\n- `400 Bad Request` — malformed request body\n- `500 Internal Server Error` — unexpected processing error","operationId":"POST_/api/opensips/call/auth","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsAuthRequest"}}},"description":"Request body for service.OpensipsAuthRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsAuthResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"OpenSIPS Call Auth","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/opensips/call/failover":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsHandler).Failover`\n\n---\n\nDetermines whether a carrier SIP failure code should trigger retry on the next carrier or be returned to the customer.\n\n**Purpose:**\nCalled after receiving a non-2xx final response from a carrier. Tells OpenSIPS whether to try the next carrier in the LCR list or pass the failure code back to the calling party.\n\n**How it works:**\n- Looks up `carrier_sip_failover_rules` on the **read replica** for the carrier trunk and failure code.\n- Returns `retry_next` if the code is configured for retry (e.g., 503, 504, 408).\n- Returns `return_to_customer` if the code should be passed through (e.g., 404, 486, 603).\n- If no rule exists for the code, defaults to `retry_next`.\n\n**Request payload (from OpenSIPS):**\n```json\n{\n  \"customer_trunk_id\": 10,\n  \"carrier_trunk_id\": 20,\n  \"failure_code\": 503\n}\n```\n\n**Response:**\n```json\n{\n  \"action\": \"retry_next\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — action returned\n- `400 Bad Request` — malformed request\n- `500 Internal Server Error` — lookup failed (defaults to `retry_next`)","operationId":"POST_/api/opensips/call/failover","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailoverRequest"}}},"description":"Request body for handler.FailoverRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailoverResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"OpenSIPS Failover Action","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/opensips/call/route":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsHandler).Route`\n\n---\n\nReturns the next carrier gateway URI selected by the Go LCR engine.\n\n**Purpose:**\nThe routing decision endpoint. Called after successful auth and admission. Returns the SIP URI of the cheapest eligible carrier gateway. On failure, OpenSIPS calls this again with `attempt=2` and the previous carrier marked as tried.\n\n**How it works:**\n- Runs the full LCR engine for the customer trunk + DNIS.\n- Selects the cheapest valid carrier that has not already been tried (via `tried_carrier_trunk_ids`).\n- Applies round-robin or failover selection across the carrier's gateway IPs based on `multi_ip_mode`.\n- Returns the `destination_uri` (e.g., `sip:61400123456@203.0.113.50:5060`) for OpenSIPS to forward the INVITE to.\n- Also returns the carrier's capacity limits for OpenSIPS to enforce locally.\n\n**Request payload (from OpenSIPS):**\n```json\n{\n  \"customer_trunk_id\": 10,\n  \"ani\": \"6125550100\",\n  \"dnis\": \"61400123456\",\n  \"attempt\": 1,\n  \"previous_carrier_trunk_id\": null,\n  \"previous_gateway_id\": null,\n  \"previous_failure_code\": null,\n  \"tried_carrier_trunk_ids\": []\n}\n```\n\n**Response (route found):**\n```json\n{\n  \"allowed\": true,\n  \"carrier_trunk_id\": 20,\n  \"carrier_gateway_id\": 1,\n  \"destination_uri\": \"sip:61400123456@203.0.113.50:5060\",\n  \"outbound_dnis\": \"61400123456\",\n  \"cps_limit\": 50,\n  \"port_limit\": 200,\n  \"sip_trusted\": false\n}\n```\n\n**Response (no route):**\n```json\n{\n  \"allowed\": false,\n  \"reject_code\": 404,\n  \"reject_reason\": \"no route available\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — always returned; check the `allowed` field\n- `400 Bad Request` — malformed request\n- `500 Internal Server Error` — LCR engine error","operationId":"POST_/api/opensips/call/route","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsRouteRequest"}}},"description":"Request body for service.OpensipsRouteRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsRouteResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"OpenSIPS Call Route","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/opensips/register/auth":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).RegisterAuth`\n\n---\n\nVerifies digest credentials for OpenSIPS registration-based trunk authentication.\n\n**Purpose:**\nCalled by OpenSIPS at REGISTER time to validate a customer trunk's digest credentials. This is the runtime authentication endpoint — it must respond quickly (sub-50ms) to avoid delaying SIP processing.\n\n**How it works:**\n- Receives digest parameters extracted by OpenSIPS from the REGISTER request's `Authorization` header.\n- Looks up the username+domain in `trunk_sip_registrations` on the **read replica**.\n- Validates the `auth_response` against the stored `password_hash` using the configured algorithm (MD5, SHA-256, or SHA-512-256).\n- Supports both qop=auth and legacy (no qop) digest modes.\n- Uses constant-time comparison to prevent timing attacks.\n- Returns `allowed: true` with the `customer_trunk_id` on success.\n\n**Request payload (from OpenSIPS):**\n```json\n{\n  \"source_ip\": \"203.0.113.10\",\n  \"username\": \"acme_trunk_01\",\n  \"realm\": \"sip.crazytel.com.au\",\n  \"auth_response\": \"Digest username=\\\"acme_trunk_01\\\", realm=\\\"sip.crazytel.com.au\\\", nonce=\\\"...\\\", uri=\\\"sip:sip.crazytel.com.au\\\", response=\\\"a1b2c3d4...\\\"\",\n  \"method\": \"REGISTER\",\n  \"uri\": \"sip:sip.crazytel.com.au\"\n}\n```\n\n**Response (allowed):**\n```json\n{\n  \"allowed\": true,\n  \"customer_trunk_id\": 10\n}\n```\n\n**Response (rejected):**\n```json\n{\n  \"allowed\": false,\n  \"reject_code\": 403,\n  \"reject_reason\": \"invalid credentials\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — always returned; check the `allowed` field for the auth decision","operationId":"POST_/api/opensips/register/auth","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAuthRequest"}}},"description":"Request body for handler.RegisterAuthRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterAuthResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"OpenSIPS Register Auth","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/opensips/routing/sync":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*OpensipsHandler).SyncRouting`\n\n---\n\nRebuilds OpenSIPS drouting runtime tables from Go-owned configuration and pushes them to all active OpenSIPS nodes.\n\n**Purpose:**\nThe master sync operation. Rebuilds the entire routing table snapshot (gateways, carriers, rules, groups) from the Go API's configuration and pushes it to every active OpenSIPS node via MI `dr_reload` commands. Call this after any routing-affecting change: new trunks, rate deck assignments, gateway IP changes, or allowlist modifications.\n\n**How it works:**\n- Queries active customer trunks, carrier trunks, rate decks, assignments, and gateway IPs from the **read replica**.\n- Builds a complete routing snapshot compatible with OpenSIPS `drouting` module tables.\n- Writes the snapshot to the OpenSIPS routing database tables on the **write primary**.\n- Sends `dr_reload` MI command to every active OpenSIPS node in the `opensips_nodes` registry.\n- Nodes with `status = draining` or `offline` are skipped.\n- Returns per-node reload results and any warnings.\n\n**Response example:**\n```json\n{\n  \"gateways\": 5,\n  \"carriers\": 3,\n  \"rules\": 12,\n  \"groups\": 2,\n  \"reload_results\": [\n    {\n      \"node_id\": 1,\n      \"hostname\": \"opensips-01.syd.crazytel.internal\",\n      \"success\": true\n    }\n  ],\n  \"warnings\": []\n}\n```\n\n**Warnings (non-exhaustive):**\n- If no complete routes are generated, a warning explains possible causes (missing rate decks, mismatched currencies, no carrier IPs, empty allowlists, etc.).\n- If no active OpenSIPS nodes are registered, the data is rebuilt but no reload is sent.\n\n**HTTP status codes:**\n- `200 OK` — sync completed (check `reload_results` for per-node status)\n- `500 Internal Server Error` — sync process failed","operationId":"POST_/api/opensips/routing/sync","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpensipsRoutingSyncResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Sync OpenSIPS Routing","tags":["OpenSIPS Internal APIs - Runtime"]}},"/api/rate-deck-updates":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckUpdateHandler).List`\n\n---\n\nReturns all rate deck updates with optional status filter.\n\n**Purpose:**\nTracks the lifecycle of rate deck revisions. When rates change, an update record is created to notify customers, track acknowledgements, and optionally suspend trunks that haven't downloaded the new rates.\n\n**How it works:**\n- Queries `rate_deck_updates` from the **read replica**.\n- Can filter by `status` (`pending`, `active`, `superseded`).\n- Supports `limit` and `offset` for pagination.\n\n**Query parameters:**\n- `status` — optional, `pending`, `active`, or `superseded`\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"rate_deck_id\": 1,\n      \"update_type\": \"customer\",\n      \"status\": \"pending\",\n      \"effective_at\": \"2026-02-01T00:00:00Z\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — updates returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/rate-deck-updates","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RateDeckUpdate"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Rate Deck Updates","tags":["Pricing APIs - Rate Deck Updates"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckUpdateHandler).Create`\n\n---\n\nQueues a new immediate or scheduled rate deck update.\n\n**Purpose:**\nInitiates a rate revision workflow. When a rate deck's contents change, this endpoint creates an update record that triggers customer notifications and optional suspension for non-acknowledgement.\n\n**How it works:**\n- Inserts a `rate_deck_updates` record with `status = pending` on the **write primary**.\n- The background Rate Deck Scheduler picks up pending updates:\n  - If `effective_at` is in the past or present, the scheduler activates it immediately.\n  - If `effective_at` is in the future, the scheduler waits until that time.\n- When activated, customer trunks assigned to the rate deck are notified (notification record created) and optionally suspended until they acknowledge the download via `POST /api/rate-deck-updates/:id/download/:trunk_id`.\n\n**Request payload:**\n```json\n{\n  \"rate_deck_id\": 1,\n  \"update_type\": \"customer\",\n  \"effective_at\": \"2026-02-01T00:00:00Z\"\n}\n```\n\n**Fields:**\n- `rate_deck_id` (required) — the rate deck being updated\n- `update_type` (required) — `customer` or `carrier`\n- `effective_at` (required) — RFC3339 timestamp when the new rates take effect\n\n**Response (201 Created):** Returns the created update record.\n\n**HTTP status codes:**\n- `201 Created` — update queued\n- `400 Bad Request` — invalid effective_at format or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-deck-updates","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRateDeckUpdateRequest"}}},"description":"Request body for handler.CreateRateDeckUpdateRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeckUpdate"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Rate Deck Update","tags":["Pricing APIs - Rate Deck Updates"]}},"/api/rate-deck-updates/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckUpdateHandler).Delete`\n\n---\n\nRemoves a pending rate deck update.\n\n**Purpose:**\nCancels a scheduled rate revision before it activates. Only pending updates can be deleted — active or superseded updates are immutable.\n\n**How it works:**\n- Deletes the `rate_deck_updates` record from the **write primary** only if `status` is `pending`.\n- Active or superseded updates cannot be deleted (they are part of the audit trail).\n\n**Path parameter:**\n- `id` — rate deck update ID\n\n**Warning:** Deleting a pending update prevents the scheduled rate change from occurring. The existing rates remain in effect.\n\n**HTTP status codes:**\n- `204 No Content` — update deleted\n- `400 Bad Request` — invalid ID or update is not in pending status\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/rate-deck-updates/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Rate Deck Update","tags":["Pricing APIs - Rate Deck Updates"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckUpdateHandler).Get`\n\n---\n\nRetrieves a rate deck update by ID.\n\n**Path parameter:**\n- `id` — rate deck update ID\n\n**HTTP status codes:**\n- `200 OK` — update returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no update with that ID","operationId":"GET_/api/rate-deck-updates/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeckUpdate"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Rate Deck Update","tags":["Pricing APIs - Rate Deck Updates"]}},"/api/rate-deck-updates/{id}/download/{trunk_id}":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckUpdateHandler).DownloadRateDeck`\n\n---\n\nConfirms a customer has downloaded the new rate deck, triggering reinstatement if suspended.\n\n**Purpose:**\nThe customer acknowledgement endpoint. When a customer downloads their new rate deck, they call this endpoint to confirm receipt. If their trunk was suspended due to non-acknowledgement of the rate update, it is immediately reinstated.\n\n**How it works:**\n- Records the download timestamp in `rate_deck_customer_notifications` on the **write primary**.\n- If the customer trunk was suspended (due to the rate update workflow), it is immediately reinstated to `active` status.\n- Reinstatement failure is logged but does not fail the request — the download is still recorded.\n\n**Path parameters:**\n- `id` — rate deck update ID\n- `trunk_id` — customer trunk ID\n\n**Response:**\n```json\n{\n  \"message\": \"rate deck downloaded\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — download recorded, trunk reinstated if applicable\n- `400 Bad Request` — invalid IDs\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-deck-updates/:id/download/:trunk_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"trunk_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Download Rate Deck","tags":["Pricing APIs - Rate Deck Updates"]}},"/api/rate-decks":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).List`\n\n---\n\nReturns all rate decks with optional filtering.\n\n**Purpose:**\nRate decks are collections of per-destination prefix rates. Customer rate decks define **sell prices** (what you charge customers). Carrier rate decks define **buy/cost prices** (what carriers charge you). The LCR engine uses both to find the cheapest profitable route for each call.\n\n**How it works:**\n- Queries `rate_decks` from the **read replica**.\n- Can filter by `deck_type` (`customer` or `carrier`).\n- Supports `limit` and `offset` for pagination.\n- Each deck has a currency — when assigned to a trunk, the currencies must match.\n\n**Query parameters:**\n- `deck_type` — optional, `customer` or `carrier`\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"Acme Customer Rates — Jan 2026\",\n      \"deck_type\": \"customer\",\n      \"currency\": \"AUD\",\n      \"status\": \"active\",\n      \"created_at\": \"2026-01-15T08:30:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 1 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — decks returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/rate-decks","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RateDeck"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Rate Decks","tags":["Pricing APIs - Rate Decks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).Create`\n\n---\n\nCreates a new rate deck for customer sell pricing or carrier buy pricing.\n\n**Purpose:**\nCreates a container for per-destination rates. A rate deck must exist before rates can be added and before it can be assigned to a trunk.\n\n**How it works:**\n- Inserts a `rate_decks` record on the **write primary**.\n- The deck starts empty — use `POST /api/rate-decks/:id/rates` or the bulk endpoint to populate it.\n- Customer decks define what you charge; carrier decks define what you pay.\n\n**Request payload:**\n```json\n{\n  \"name\": \"Acme Customer Rates — Jan 2026\",\n  \"deck_type\": \"customer\",\n  \"currency\": \"AUD\",\n  \"description\": \"January 2026 rate revision for Acme Telecom\"\n}\n```\n\n**Fields:**\n- `name` (required) — human-readable deck name\n- `deck_type` (required) — `customer` (sell) or `carrier` (buy/cost)\n- `currency` (required) — `AUD` or `USD`; must match the trunk currency when assigned\n- `description` (optional) — free-text notes\n\n**Response (201 Created):** Returns the created deck record.\n\n**HTTP status codes:**\n- `201 Created` — deck created\n- `400 Bad Request` — invalid deck_type, invalid currency, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-decks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRateDeckRequest"}}},"description":"Request body for handler.CreateRateDeckRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeck"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Rate Deck","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).Delete`\n\n---\n\nPermanently deletes a rate deck and all associated rates.\n\n**Purpose:**\nRemoves a rate deck entirely. This is a hard delete — the deck, all its rates, and all allowlist entries are physically removed.\n\n**How it works:**\n- Physically deletes the `rate_decks` record from the **write primary**.\n- Cascades to delete all `rate_deck_rates` and `rate_deck_allowlist` entries for this deck.\n- If the deck was assigned to any trunks, those assignments are also removed.\n- Returns `204 No Content`.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Warning:** This action is irreversible. Consider creating a new deck and superseding the old one via rate deck updates rather than deleting an active deck.\n\n**HTTP status codes:**\n- `204 No Content` — deck deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/rate-decks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Rate Deck","tags":["Pricing APIs - Rate Decks"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).Get`\n\n---\n\nRetrieves a rate deck by its database ID.\n\n**Purpose:**\nFetches deck metadata. To get the actual prefix rates, use `GET /api/rate-decks/:id/rates`.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**HTTP status codes:**\n- `200 OK` — deck returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no deck with that ID","operationId":"GET_/api/rate-decks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeck"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Rate Deck","tags":["Pricing APIs - Rate Decks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).Update`\n\n---\n\nUpdates an existing rate deck's metadata.\n\n**Purpose:**\nRenames a deck, changes its type, or updates the description. Does NOT modify the rates within the deck.\n\n**How it works:**\n- Fetches the deck by ID (returns 404 if not found).\n- Updates `name`, `deck_type`, `currency`, and `description` on the **write primary**.\n- Sets `updated_at` to now.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Request payload:** Same shape as Create.\n\n**Warning:** Changing the `currency` of an assigned rate deck will cause billing mismatches until the trunk's currency is also updated.\n\n**HTTP status codes:**\n- `200 OK` — deck updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no deck with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/rate-decks/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRateDeckRequest"}}},"description":"Request body for handler.UpdateRateDeckRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeck"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Rate Deck","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/allowlist":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).ListAllowlist`\n\n---\n\nReturns all carrier trunks on the allowlist for a rate deck.\n\n**Purpose:**\nThe allowlist controls which carrier trunks the LCR engine may consider when routing calls for a customer rate deck. Only carriers on the allowlist are eligible for route selection. This is the primary mechanism for restricting which upstream carriers a customer's traffic can use.\n\n**How it works:**\n- Queries `rate_deck_allowlist` on the **read replica** for the given deck.\n- Each entry links a carrier trunk ID to this rate deck.\n- The LCR engine filters its candidate list to only carriers present in the allowlist before sorting by cost.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"rate_deck_id\": 1,\n    \"carrier_trunk_id\": 20,\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — allowlist returned\n- `400 Bad Request` — invalid deck ID\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/rate-decks/:id/allowlist","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RateDeckCarrierAllowlist"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Rate Deck Allowlist","tags":["Pricing APIs - Rate Decks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).AddAllowlistEntry`\n\n---\n\nAdds a carrier trunk to a rate deck's allowlist.\n\n**Purpose:**\nGrants a carrier trunk eligibility to carry traffic for this rate deck. Without this, the carrier will never be selected by the LCR engine regardless of its cost.\n\n**How it works:**\n- Inserts into `rate_deck_allowlist` on the **write primary**.\n- The carrier becomes immediately eligible for LCR selection.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Request payload:**\n```json\n{\n  \"carrier_trunk_id\": 20\n}\n```\n\n**Response (201 Created):** Returns the created allowlist entry.\n\n**HTTP status codes:**\n- `201 Created` — carrier added to allowlist\n- `400 Bad Request` — invalid carrier trunk ID\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-decks/:id/allowlist","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAllowlistEntryRequest"}}},"description":"Request body for handler.AddAllowlistEntryRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeckCarrierAllowlist"}}},"description":"Created"},"default":{"description":""}},"summary":"Add to Rate Deck Allowlist","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/allowlist/{carrier_trunk_id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).RemoveAllowlistEntry`\n\n---\n\nRemoves a carrier trunk from a rate deck's allowlist.\n\n**Purpose:**\nRevokes a carrier's eligibility to carry traffic for this rate deck. The carrier is immediately excluded from LCR route selection.\n\n**How it works:**\n- Deletes the `rate_deck_allowlist` entry matching both the deck ID and carrier trunk ID.\n- Returns `204 No Content`.\n\n**Path parameters:**\n- `id` — rate deck ID\n- `carrier_trunk_id` — carrier trunk ID to remove\n\n**Warning:** If this was the last allowed carrier for a customer rate deck, the LCR engine will return `no route` for all calls under that deck.\n\n**HTTP status codes:**\n- `204 No Content` — carrier removed from allowlist\n- `400 Bad Request` — invalid IDs\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/rate-decks/:id/allowlist/:carrier_trunk_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"carrier_trunk_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Remove from Rate Deck Allowlist","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/rates":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).ListRates`\n\n---\n\nReturns all prefix rates for a given rate deck.\n\n**Purpose:**\nLists the per-destination rates that define pricing for this deck. The LCR engine uses longest-prefix-match to find the most specific rate for a dialled number.\n\n**How it works:**\n- Queries `rate_deck_rates` on the **read replica**, sorted by prefix descending (longest first).\n- Supports `limit` and `offset` for pagination.\n- Each rate has a `rate_per_minute` (decimal string), `billing_increment_initial`/`step` overrides, and `effective_at` timestamp.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Query parameters:**\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"rate_deck_id\": 1,\n      \"prefix\": \"614\",\n      \"destination_name\": \"Australia Mobile\",\n      \"rate_per_minute\": \"0.0250\",\n      \"billing_increment_initial\": 1,\n      \"billing_increment_step\": 1,\n      \"effective_at\": \"2026-01-01T00:00:00Z\"\n    },\n    {\n      \"id\": 2,\n      \"rate_deck_id\": 1,\n      \"prefix\": \"61\",\n      \"destination_name\": \"Australia\",\n      \"rate_per_minute\": \"0.0100\",\n      \"billing_increment_initial\": 1,\n      \"billing_increment_step\": 1,\n      \"effective_at\": \"2026-01-01T00:00:00Z\"\n    }\n  ],\n  \"meta\": { \"page\": 1, \"page_size\": 20, \"total\": 2 }\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — rates returned\n- `400 Bad Request` — invalid deck ID\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/rate-decks/:id/rates","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/RateDeckRate"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Rate Deck Rates","tags":["Pricing APIs - Rate Decks"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).CreateRate`\n\n---\n\nAdds a single prefix rate to a rate deck.\n\n**Purpose:**\nDefines the price for calls to a specific destination prefix. The LCR engine matches dialled numbers against these prefixes using longest-prefix-match.\n\n**How it works:**\n- Parses `rate_per_minute` as a `decimal.Decimal` string (never float64).\n- Inserts into `rate_deck_rates` on the **write primary**.\n- If `effective_at` is omitted, defaults to the current time.\n- Longer prefixes take precedence: a call to `61400123456` matches `614` before `61`.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Request payload:**\n```json\n{\n  \"prefix\": \"614\",\n  \"destination_name\": \"Australia Mobile\",\n  \"rate_per_minute\": \"0.0250\",\n  \"billing_increment_initial\": 1,\n  \"billing_increment_step\": 1,\n  \"effective_at\": \"2026-01-01T00:00:00Z\"\n}\n```\n\n**Fields:**\n- `prefix` (required) — dialled number prefix; longer prefixes are matched first\n- `destination_name` (required) — human-readable destination name (e.g., \"Australia Mobile\")\n- `rate_per_minute` (required) — price per minute as a decimal string (e.g., `\"0.0250\"`)\n- `billing_increment_initial` — first billing block in seconds (overrides trunk default)\n- `billing_increment_step` — subsequent billing block in seconds (overrides trunk default)\n- `effective_at` — RFC3339 timestamp when this rate becomes active (default: now)\n\n**Response (201 Created):** Returns the created rate record.\n\n**HTTP status codes:**\n- `201 Created` — rate created\n- `400 Bad Request` — invalid rate value, invalid effective_at, or validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-decks/:id/rates","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRateDeckRateRequest"}}},"description":"Request body for handler.CreateRateDeckRateRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeckRate"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Rate Deck Rate","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/rates/bulk":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).BulkCreateRates`\n\n---\n\nBulk imports rates into a rate deck with conflict handling.\n\n**Purpose:**\nEfficiently populates a rate deck with many prefix rates at once — for example, importing a carrier's full rate sheet. All rates are inserted in a single database transaction for atomicity.\n\n**How it works:**\n- Accepts an array of rate objects (minimum 1).\n- Parses each `rate_per_minute` as a decimal string.\n- Uses `ON CONFLICT (rate_deck_id, prefix)` with the chosen strategy to handle duplicate prefixes.\n- The entire batch succeeds or fails as one transaction.\n\n**Path parameter:**\n- `id` — rate deck ID\n\n**Request payload:**\n```json\n{\n  \"rates\": [\n    {\n      \"prefix\": \"614\",\n      \"destination_name\": \"Australia Mobile\",\n      \"rate_per_minute\": \"0.0250\",\n      \"billing_increment_initial\": 1,\n      \"billing_increment_step\": 1,\n      \"effective_at\": \"2026-01-01T00:00:00Z\"\n    },\n    {\n      \"prefix\": \"61\",\n      \"destination_name\": \"Australia\",\n      \"rate_per_minute\": \"0.0100\",\n      \"billing_increment_initial\": 1,\n      \"billing_increment_step\": 1,\n      \"effective_at\": \"2026-01-01T00:00:00Z\"\n    }\n  ],\n  \"on_conflict\": \"skip\"\n}\n```\n\n**Conflict strategies (`on_conflict`):**\n- `skip` (default) — ignore rows that conflict on `(rate_deck_id, prefix)`; existing rates are preserved\n- `overwrite` — replace the existing rate with the new values\n- `error` — abort the entire bulk import on the first conflict\n\n**Response (201 Created):**\n```json\n{\n  \"message\": \"rates imported\",\n  \"count\": 2\n}\n```\n\n**HTTP status codes:**\n- `201 Created` — rates imported\n- `400 Bad Request` — invalid rate value, invalid on_conflict, or empty rates array\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/rate-decks/:id/rates/bulk","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateRateDeckRateRequest"}}},"description":"Request body for handler.BulkCreateRateDeckRateRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkImportResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Bulk Import Rate Deck Rates","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/rates/csv":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).UploadRatesCSV`\n\n---\n\nImports a carrier or customer rate deck from a multipart CSV file.\n\n**Purpose:**\nWholesale VoIP carriers provide rate sheets in CSV format with industry-standard fields such as destination prefix, destination name, per-minute cost, billing increment (`1/1`, `6/6`, `60/60`), optional per-call rating, and effective dates. This endpoint lets admins upload those files directly instead of manually converting them to JSON.\n\n**How it works:**\n- Accepts `multipart/form-data` with a CSV file field named `file` (or `csv`).\n- Parses the CSV using Go's RFC 4180-compatible CSV reader, so destination names with spaces, commas, brackets, ampersands, and other special characters are supported when quoted by the CSV.\n- Header matching is flexible and case-insensitive. Columns may appear in any order; unknown columns are ignored.\n- Required columns: `prefix`, `destination_name`, `rate`.\n- Optional columns: `billing_increment` (`1/1`, `6/6`, `1/6`), `rate_type` (`per_minute` or `per_call`), `effective_at`.\n- Optional upload form fields: `effective_at` (upload-level default), `effective_now` (force current UTC time), and `on_conflict` (`skip`, `overwrite`, `error`).\n- All date/time values are parsed as UTC/GMT0. Per-row `effective_at` overrides the upload-level default; if no effective date is supplied, the current UTC time is used.\n- If `billing_increment` is omitted, the row defaults to `1/1`. If `rate_type` is omitted, the row defaults to `per_minute`.\n- Writes happen on the PostgreSQL primary via the repository bulk-import transaction.\n\n**Example CSV:**\n```csv\nprefix,destination_name,rate,billing_increment,rate_type,effective_at\n614,Australia Mobile,0.0250,1/1,per_minute,2026-02-01\n611300,Australia 1300 Per Call,0.1200,,per_call,2026-02-01\n```\n\n**HTTP status codes:**\n- `201 Created` — rates imported\n- `400 Bad Request` — invalid file, missing required columns, invalid row data, invalid effective_at, or invalid rate_type\n- `500 Internal Server Error` — database import failed","operationId":"POST_/api/rate-decks/:id/rates/csv","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CSVRateDeckUploadRequest"}}},"description":"Request body for handler.CSVRateDeckUploadRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CSVRateImportResponse"}}},"description":"Created"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CSVRateImportResponse"}}},"description":"Bad Request"},"default":{"description":""}},"summary":"Upload Rate Deck CSV","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-decks/{id}/rates/{rate_id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).DeleteRate`\n\n---\n\nRemoves a prefix rate from a rate deck.\n\n**Purpose:**\nDeletes a specific destination rate. After deletion, calls to that prefix will fall through to a shorter matching prefix, or fail with `no route` if no prefix matches.\n\n**How it works:**\n- Fetches the rate and verifies ownership by the deck.\n- Physically deletes the row from `rate_deck_rates` on the **write primary**.\n\n**Path parameters:**\n- `id` — rate deck ID\n- `rate_id` — rate record ID\n\n**Warning:** If a trunk's assigned rate deck loses the only matching prefix for a destination, calls to that destination will fail with `no route`.\n\n**HTTP status codes:**\n- `204 No Content` — rate deleted\n- `400 Bad Request` — rate doesn't belong to deck\n- `404 Not Found` — rate not found\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/rate-decks/:id/rates/:rate_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rate_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Rate Deck Rate","tags":["Pricing APIs - Rate Decks"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).UpdateRate`\n\n---\n\nUpdates an existing rate in a rate deck.\n\n**Purpose:**\nModifies a specific prefix rate — change the price, destination name, or billing increments.\n\n**How it works:**\n- Fetches the rate by `rate_id` and verifies it belongs to the specified deck.\n- Updates all fields on the **write primary**.\n\n**Path parameters:**\n- `id` — rate deck ID\n- `rate_id` — rate record ID\n\n**Request payload:** Same shape as Create Rate. All fields are required.\n\n**Note:** Updating a rate does not retroactively change CDRs that have already been generated. The new rate applies to future calls only.\n\n**HTTP status codes:**\n- `200 OK` — rate updated; returns the updated record\n- `400 Bad Request` — rate doesn't belong to deck, or validation failure\n- `404 Not Found` — rate not found\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/rate-decks/:id/rates/:rate_id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"rate_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRateDeckRateRequest"}}},"description":"Request body for handler.UpdateRateDeckRateRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateDeckRate"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Rate Deck Rate","tags":["Pricing APIs - Rate Decks"]}},"/api/rate-lookup":{"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*RateDeckHandler).RateLookup`\n\n---\n\nPerforms a longest-prefix-match rate lookup for debugging and validation.\n\n**Purpose:**\nTests which rate (and which prefix) would match a given dialled number in a specific rate deck. Essential for verifying rate deck contents before production use or troubleshooting unexpected billing.\n\n**How it works:**\n- Takes a `rate_deck_id` and a `destination` (dialled number).\n- Queries `rate_deck_rates` using `ORDER BY prefix DESC` to find the longest matching prefix.\n- Returns the matched prefix, rate, destination name, and deck currency.\n- Returns `404` if no prefix matches the destination.\n\n**Request payload:**\n```json\n{\n  \"rate_deck_id\": 1,\n  \"destination\": \"61400123456\"\n}\n```\n\n**Response example:**\n```json\n{\n  \"rate_deck_id\": 1,\n  \"prefix\": \"614\",\n  \"destination_name\": \"Australia Mobile\",\n  \"rate_per_minute\": \"0.0250\",\n  \"currency\": \"AUD\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — matching rate found\n- `400 Bad Request` — invalid rate_deck_id or missing destination\n- `404 Not Found` — no matching prefix for the destination\n- `500 Internal Server Error` — database query failed","operationId":"POST_/api/rate-lookup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLookupRequest"}}},"description":"Request body for handler.RateLookupRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLookupResponse"}}},"description":"OK"},"default":{"description":""}},"summary":"Rate Lookup","tags":["Pricing APIs - Rate Decks"]}},"/api/sip-registrations":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).List`\n\n---\n\nReturns all SIP registration records with optional filtering.\n\n**Purpose:**\nSIP registrations provide digest-based authentication for customer trunks as an alternative to IP-based auth. When a customer sends a SIP REGISTER, OpenSIPS calls `POST /api/opensips/register/auth` to verify credentials against these records.\n\n**How it works:**\n- Queries `trunk_sip_registrations` from the **read replica**.\n- Can filter by `customer_trunk_id`.\n- Each record stores a pre-computed HA1 hash (not the plaintext password).\n- Supports multiple hash algorithms: MD5, SHA-256, SHA-512-256.\n\n**Query parameters:**\n- `customer_trunk_id` — optional\n- `limit` / `offset` — pagination\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"customer_trunk_id\": 10,\n    \"username\": \"acme_trunk_01\",\n    \"domain\": \"sip.crazytel.com.au\",\n    \"hash_algorithm\": \"MD5\",\n    \"status\": \"active\",\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — registrations returned\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/sip-registrations","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TrunkSIPRegistration"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List SIP Registrations","tags":["Admin APIs - Authentication"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).Create`\n\n---\n\nAdds a new SIP digest auth registration for a customer trunk.\n\n**Purpose:**\nCreates the credential record that OpenSIPS uses to authenticate REGISTER requests from customers using digest authentication.\n\n**How it works:**\n- Inserts a `trunk_sip_registrations` record on the **write primary**.\n- The `password_hash` must be a pre-computed HA1 digest: `MD5(username:realm:password)` (or SHA-256/SHA-512-256 equivalent).\n- OpenSIPS calls `POST /api/opensips/register/auth` which verifies the REGISTER's `auth_response` against this stored hash.\n- Defaults `status` to `active`.\n\n**Request payload:**\n```json\n{\n  \"customer_trunk_id\": 10,\n  \"username\": \"acme_trunk_01\",\n  \"domain\": \"sip.crazytel.com.au\",\n  \"password_hash\": \"5f4dcc3b5aa765d61d8327deb882cf99\",\n  \"hash_algorithm\": \"MD5\",\n  \"status\": \"active\"\n}\n```\n\n**Fields:**\n- `customer_trunk_id` (required) — trunk this registration belongs to\n- `username` (required) — SIP username for REGISTER\n- `domain` (required) — SIP domain / realm\n- `password_hash` (required) — pre-calculated HA1 digest hex string\n- `hash_algorithm` (required) — `MD5`, `SHA-256`, or `SHA-512-256`\n- `status` — `active` (default) or `disabled`\n\n**Response (201 Created):** Returns the created registration record (includes `password_hash` — handle response securely).\n\n**HTTP status codes:**\n- `201 Created` — registration created\n- `400 Bad Request` — validation failure\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/sip-registrations","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrunkSIPRegistrationRequest"}}},"description":"Request body for handler.CreateTrunkSIPRegistrationRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkSIPRegistration"}}},"description":"Created"},"default":{"description":""}},"summary":"Create SIP Registration","tags":["Admin APIs - Authentication"]}},"/api/sip-registrations/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).Delete`\n\n---\n\nRemoves a SIP registration record.\n\n**Purpose:**\nDecommissions a set of SIP credentials. After deletion, REGISTER attempts using these credentials will be rejected with `403 Forbidden`.\n\n**How it works:**\n- Physically deletes the row from `trunk_sip_registrations` on the **write primary**.\n\n**Path parameter:**\n- `id` — registration record ID\n\n**Warning:** Deleting an active registration will immediately prevent the associated trunk from authenticating via REGISTER. Ensure the trunk has an alternative auth method (e.g., IP auth) if it should remain operational.\n\n**HTTP status codes:**\n- `204 No Content` — registration deleted\n- `400 Bad Request` — invalid ID\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/sip-registrations/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete SIP Registration","tags":["Admin APIs - Authentication"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).Get`\n\n---\n\nRetrieves a SIP registration record by ID.\n\n**Path parameter:**\n- `id` — registration record ID\n\n**Security note:** The response includes the `password_hash` field. Handle this response data securely — do not log it or expose it in UIs.\n\n**HTTP status codes:**\n- `200 OK` — registration returned\n- `400 Bad Request` — invalid ID\n- `404 Not Found` — no registration with that ID","operationId":"GET_/api/sip-registrations/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkSIPRegistration"}}},"description":"OK"},"default":{"description":""}},"summary":"Get SIP Registration","tags":["Admin APIs - Authentication"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkSIPRegistrationHandler).Update`\n\n---\n\nUpdates an existing SIP registration record.\n\n**Purpose:**\nChanges credentials, hash algorithm, or status. The new `password_hash` takes effect immediately for the next REGISTER attempt.\n\n**Path parameter:**\n- `id` — registration record ID\n\n**Request payload:** Same shape as Create. All fields are required.\n\n**HTTP status codes:**\n- `200 OK` — registration updated; returns the updated record\n- `400 Bad Request` — validation failure\n- `404 Not Found` — no registration with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/sip-registrations/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTrunkSIPRegistrationRequest"}}},"description":"Request body for handler.UpdateTrunkSIPRegistrationRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkSIPRegistration"}}},"description":"OK"},"default":{"description":""}},"summary":"Update SIP Registration","tags":["Admin APIs - Authentication"]}},"/api/trunk-ip-auth":{"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkIPAuthHandler).List`\n\n---\n\nReturns all trunk IP authentication records with optional filtering.\n\n**Purpose:**\nTrunk IP auth records map source IP ranges (CIDRs) to specific trunks. When OpenSIPS receives an inbound SIP INVITE, it calls `POST /api/opensips/call/auth` which uses these records to identify which customer trunk the call belongs to based on the source IP. This endpoint lists all configured mappings.\n\n**How it works:**\n- Queries the `trunk_ip_auth` table on the **read replica**.\n- Supports optional filtering by `trunk_type` (`customer` or `carrier`) and `trunk_id`.\n- Each record binds a CIDR range to exactly one trunk. Multiple CIDRs can map to the same trunk (e.g., a customer with offices in multiple locations).\n- OpenSIPS performs longest-prefix CIDR matching at call time to find the most specific match.\n\n**Query parameters:**\n- `trunk_type` — optional, `customer` or `carrier`\n- `trunk_id` — optional, filter to a specific trunk\n\n**Response example:**\n```json\n[\n  {\n    \"id\": 1,\n    \"trunk_type\": \"customer\",\n    \"trunk_id\": 5,\n    \"cidr\": \"203.0.113.0/24\",\n    \"description\": \"Acme Corp SIP range\",\n    \"created_at\": \"2026-01-15T08:30:00Z\"\n  }\n]\n```\n\n**HTTP status codes:**\n- `200 OK` — records returned\n- `400 Bad Request` — invalid query parameters\n- `500 Internal Server Error` — database query failed","operationId":"GET_/api/trunk-ip-auth","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TrunkIPAuth"},"type":"array"}}},"description":"OK"},"default":{"description":""}},"summary":"List Trunk IP Auth Records","tags":["Admin APIs - Authentication"]},"post":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkIPAuthHandler).Create`\n\n---\n\nAdds a new IP/CIDR allowlist entry to a trunk for SIP source-based authentication.\n\n**Purpose:**\nCreates the binding between a source IP range and a trunk. This is the primary mechanism for identifying inbound SIP traffic — without a matching record, OpenSIPS cannot determine which customer is sending the call and will reject it.\n\n**How it works:**\n- Validates the CIDR notation (e.g., `203.0.113.0/24`) using Go's `net.ParseCIDR`, which normalises the network address.\n- Inserts the record into `trunk_ip_auth` on the **write primary**.\n- The record takes effect immediately — the next OpenSIPS `POST /api/opensips/call/auth` call will match against it.\n- No cache invalidation is needed because OpenSIPS queries the Go API in real time (not a local cache).\n\n**Request payload:**\n```json\n{\n  \"trunk_type\": \"customer\",\n  \"trunk_id\": 5,\n  \"cidr\": \"203.0.113.0/24\",\n  \"description\": \"Acme Corp SIP range\"\n}\n```\n\n**Fields:**\n- `trunk_type` (required) — `customer` or `carrier`\n- `trunk_id` (required) — the database ID of the trunk this CIDR belongs to\n- `cidr` (required) — IPv4 or IPv6 CIDR range (e.g., `203.0.113.0/24`, `2001:db8::/32`)\n- `description` (optional) — human-readable label\n\n**Response (201 Created):** Returns the created record with `id` and `created_at`.\n\n**HTTP status codes:**\n- `201 Created` — record created\n- `400 Bad Request` — invalid CIDR format, invalid trunk_type, or missing required field\n- `500 Internal Server Error` — database write failed","operationId":"POST_/api/trunk-ip-auth","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrunkIPAuthRequest"}}},"description":"Request body for handler.CreateTrunkIPAuthRequest","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkIPAuth"}}},"description":"Created"},"default":{"description":""}},"summary":"Create Trunk IP Auth Record","tags":["Admin APIs - Authentication"]}},"/api/trunk-ip-auth/{id}":{"delete":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkIPAuthHandler).Delete`\n\n---\n\nPermanently deletes a trunk IP auth record by ID.\n\n**Purpose:**\nRemoves a CIDR-to-trunk binding. After deletion, SIP traffic from that IP range will no longer be associated with the trunk.\n\n**How it works:**\n- Physically deletes the row from `trunk_ip_auth` on the **write primary**.\n- Returns `204 No Content` on success.\n- The deletion is immediate — the next INVITE from that IP range will fail authentication with `403 Forbidden` (\"source ip not authorised\").\n\n**Path parameter:**\n- `id` — the trunk IP auth record ID\n\n**Warning:** This is a hard delete, not a soft delete. There is no undo. Consider disabling the trunk instead if you may need to restore access later.\n\n**HTTP status codes:**\n- `204 No Content` — record deleted\n- `400 Bad Request` — invalid ID format\n- `500 Internal Server Error` — database delete failed","operationId":"DELETE_/api/trunk-ip-auth/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}},"description":"No Content"},"default":{"description":""}},"summary":"Delete Trunk IP Auth Record","tags":["Admin APIs - Authentication"]},"get":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkIPAuthHandler).Get`\n\n---\n\nRetrieves a single trunk IP auth record by its database ID.\n\n**Purpose:**\nFetches the details of a specific CIDR-to-trunk binding for inspection or verification.\n\n**Path parameter:**\n- `id` — the trunk IP auth record ID\n\n**Response:** Returns the full record including `trunk_type`, `trunk_id`, `cidr`, and `description`.\n\n**HTTP status codes:**\n- `200 OK` — record returned\n- `400 Bad Request` — invalid ID format\n- `404 Not Found` — no record with that ID exists","operationId":"GET_/api/trunk-ip-auth/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkIPAuth"}}},"description":"OK"},"default":{"description":""}},"summary":"Get Trunk IP Auth Record","tags":["Admin APIs - Authentication"]},"put":{"description":"#### Controller: \n\n`softswitch/internal/handler.(*TrunkIPAuthHandler).Update`\n\n---\n\nUpdates an existing trunk IP auth record.\n\n**Purpose:**\nModifies a CIDR-to-trunk binding — for example, narrowing a CIDR range, reassigning it to a different trunk, or updating the description.\n\n**How it works:**\n- Fetches the existing record by ID (returns 404 if not found).\n- Validates the new CIDR via `net.ParseCIDR`.\n- Updates all fields (`trunk_type`, `trunk_id`, `cidr`, `description`) on the **write primary**.\n- Changes take effect immediately for subsequent OpenSIPS auth calls.\n\n**Path parameter:**\n- `id` — the trunk IP auth record ID\n\n**Request payload:**\n```json\n{\n  \"trunk_type\": \"customer\",\n  \"trunk_id\": 5,\n  \"cidr\": \"203.0.113.0/26\",\n  \"description\": \"Acme Corp updated range\"\n}\n```\n\n**HTTP status codes:**\n- `200 OK` — record updated; returns the updated record\n- `400 Bad Request` — invalid ID, invalid CIDR, or missing required field\n- `404 Not Found` — no record with that ID\n- `500 Internal Server Error` — database write failed","operationId":"PUT_/api/trunk-ip-auth/:id","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTrunkIPAuthRequest"}}},"description":"Request body for handler.UpdateTrunkIPAuthRequest","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrunkIPAuth"}}},"description":"OK"},"default":{"description":""}},"summary":"Update Trunk IP Auth Record","tags":["Admin APIs - Authentication"]}}}}