Skip to main content
The same grammar as routingTable.conf, with the per-unit amount where routing puts the target.
Rules are grouped under a [scope] header and evaluated top to bottom within it. The first match wins, so specific rules go above the catch-all.

A scope is one side of the trade

[*] is sell side only. A vendor never inherits it, so a cost with no rule stays unknown rather than quietly becoming the sale price — which would report a margin of zero on traffic you are in fact paying for.
Nothing in the schema distinguishes a vendor scope from an account scope; the gateway tells them apart by which lookup asks. fireflo import --dry-run classifies every scope and names the ones it can classify as neither — which is how a scope that will load and silently match nothing gets caught.

Amounts

Decimal, at most 4 decimal places. Parsed exactly, never through floating point, so 0.0450 means 0.0450.
smsg.money.scale controls how many places a total is published with. Per-message rates always keep four, because 0.0450 shown as 0.05 is 11% out on every message. See Rating.

Conditions

Attributes are the routing engine’s: to, from, body, product, systemId, owner_id, type and the rest. matches is a regular expression and is anchored — ^61.* matches a full number, a bare 61 does not. Combine conditions with ~~, which must appear in all three positions:

The catch-all

Without a default, a message that matches nothing is recorded as an UNKNOWN rate, not as free. That is the honest outcome, and it means the message is delivered and billed to nobody until you notice. smsg.rating.unrated.refuse turns it into a refusal instead.

Worked shape

Cost and price live in the same file and are the same grammar, which is why the control panel groups them as What we pay and What we charge rather than mixing them in one list. In database mode this file is not read after startup; the same data lives in app_rate_rule and its condition tables. See Database and How pricing works.