Skip to main content

Rate cards

The subaccounts guide prices a child either on the flat reseller_margin_pct or on an inline rate deck (per-channel/destination markup cells). Both approaches inline the pricing on the child itself. A rate card is the next step up: a named, re-usable price book you draft once on your parent organization and assign to any number of subaccounts — the same pricing, draftable once and never re-entered, survives deployments and re-provisioning. Full endpoint catalog: Subaccounts API.

What a rate card is

A rate card is a named pricing template your parent org keeps in a library. Each card carries a stable id (rc_...), a human label, and one or both of the two cell families:
  • Lane cells — candidate per-lane rates captured from the what-if pricing preview (POST /billing/whatif-pricing/preview), kept verbatim. A lane is a channel/country/direction pair, such as “WhatsApp to Nigeria.”
  • Deck cells — channel[, destination] → margin_pct markup cells, the same shape an inline rate deck uses.
Both families are optional; a card with neither a lane cell nor a deck cell is dropped by the validator as noise. Parents may keep up to 50 cards; each card carries up to the same cell count an inline deck accepts.

Dashboard walkthrough

You manage rate cards on the parent organization’s Settings → Subaccounts → Rate cards tab (/settings/subaccounts/[id]). The page has two parts: the parent’s reusable card library, and the per-subaccount assignment.

Build the library

  1. Open Settings → Subaccounts, then pick any child subaccount and open its Pricing tab. The top card is Rate cards — your reusable library.
  2. Click New rate card. Give it a name (for example, “West Africa Gold”). The dashboard slugifies the name into a card id like rc_west_africa_gold.
  3. Add markup cells: a channel (sms, whatsapp, voice, …), an optional destination (NG, GB, US, …), and a margin percent (0–100). Leave destination blank to cover every destination on that channel.
  4. Click Save card. The card is appended to your library and the table shows its id, name, and cell counts.
  5. Delete a card from the library with the row’s trash icon. Subaccounts already assigned to that card then show a Stale link badge on their Pricing tab and fall back to their flat reseller margin until you re-assign a different card.
A card must carry at least one cell before it can be saved; the dashboard will not let you save an empty card.

Assign a card to a subaccount

Still on the child’s Pricing tab, below the library card:
  1. Find Assigned rate card.
  2. Choose a saved card from the dropdown, or choose None — inline deck to price the child from its own inline rate deck (or the flat margin when no deck exists).
  3. The assignment is stored immediately. From the next send onward, the child resolves against the selected card’s cells.
If you delete a card from the library, any child still pointing at it shows Stale link — card deleted. Re-assign or clear the link; the platform rejects an assignment to a deleted card with a 400 BAD_REQUEST.

Rate-card lifecycle: create, version, persist, assign

The library lives on the parent org. You replace it full-set with PUT /api/v1/subaccounts/rate-cards and read it with GET /api/v1/subaccounts/rate-cards. To version a card, PUT the full library again with the updated entry — the validator dedupes by id (last write wins), sorts by name, and returns the normalized library it actually stored, so the response is always the source of truth. Because the PUT is full-set, the workflow for versioning is: GET the library, edit one card in the array, PUT the whole array back. An empty array clears the library. Margin in a deck cell is a markup percent over your wholesale cost — the same margin_pct semantics as the inline rate deck (0–100%). Draft it in one place and reuse it for every subaccount on that plan tier.

Assignment

Point a subaccount at a saved card once:
The parent’s library is the single source of truth: you can only assign a card that still exists in the library — assigning an orphan id returns 400 BAD_REQUEST. To un-assign, send rate_card_id: null and the child falls back to its flat margin or its own inline deck. Read the current assignment back on GET /api/v1/subaccounts/{id}/rate-deck, whose response carries both the inline deck and the assigned_rate_card_id so the dashboard shows which named card the child prices against.

Limits

  • Library cap: a parent org can store at most 50 named rate cards.
  • Per-card cell cap: each card can hold at most 200 total cells (lane_cells + deck_cells combined). That is the same cap as a subaccount’s inline rate deck.
  • Inline-deck limits: when you assign a card with deck cells, those cells are resolved exactly like an inline deck, so the same rate-deck limits and matching rules that apply to PUT /api/v1/subaccounts/:id/rate-deck apply here. A card cell is a channel/destination/margin_pct tuple, the same shape as an inline deck entry.
The API returns 400 BAD_REQUEST when either cap is exceeded or when every entry in a non-empty payload is invalid.

Safe concurrent versioning of the full-set PUT

PUT /api/v1/subaccounts/rate-cards is a read-modify-write surface. The body you send becomes the entire library, so two admins editing at the same time can overwrite each other:
  1. Admin A GETs the library.
  2. Admin B GETs the same library.
  3. Admin A adds a card and PUTs.
  4. Admin B edits a different card and PUTs — Admin B’s payload does not contain Admin A’s new card, so Admin A’s card disappears.
There is no optimistic-lock token on this endpoint. To edit safely, either:
  • serialize edits within your team, or
  • always start from a fresh GET immediately before the PUT, and retry if a recent change from another session is missing.
The response body is the normalized library as stored; diff it against your intended state and re-send if anything is missing. The validator dedupes by id (last write wins) and sorts by name, so the response order may differ from your request order.

Validation error catalog

PUT /api/v1/subaccounts/rate-cards normalizes every card silently: malformed entries are dropped rather than persisted, and the remaining cards are returned. A 400 BAD_REQUEST is returned only in these cases: Deduping is by id last write wins: if the payload contains two cards with the same id, the second one replaces the first. The final library is sorted by name ascending, then by id ascending. PUT /api/v1/subaccounts/:id/assigned-rate-card returns 400 BAD_REQUEST when the supplied rate_card_id is not in the parent’s current library. Send null to clear the link.

Migration path: flat margin → inline deck → assigned rate card

Most reseller pricing evolves in three stages. You can move a subaccount forward or backward at any time without rewriting history.
  1. Flat margin only — set reseller_margin_pct on the subaccount. Every usage line prices at wholesale × (1 + reseller_margin_pct / 100). This is the simplest starting point.
  2. Inline rate deck — add per-channel/destination markup cells with PUT /api/v1/subaccounts/:id/rate-deck. The deck overrides the flat margin for matching lanes; anything not covered still falls back to the flat margin.
  3. Assigned rate card — build a named card in the parent’s library and assign it to the subaccount. The card’s deck cells are used in place of the inline deck. The inline deck stays stored on the child, but is ignored while an assigned card is active.
To roll back, clear the assignment (rate_card_id: null) and the child immediately falls back to its inline deck or flat margin. To move a deck from one child to many children, convert the inline deck into a rate card, save it to the library, then assign it to each child.

Precedence summary

When a subaccount sends traffic, the markup is resolved in this order:
  1. Assigned rate card — if the child has assigned_rate_card_id pointing to a card that still exists in the parent’s library, the card’s deck cells supply the markup.
  2. Inline rate deck — if no assigned card is active, the child’s own reseller_rate_deck cells supply the markup.
  3. Flat reseller margin — if neither card nor deck covers the lane, the child’s reseller_margin_pct is used.
Within a deck, the most specific match wins: a channel + destination cell beats a channel-only cell, which beats the flat margin. See Pricing and rate resolution for the full precedence model.

Impact and retroactive semantics

A rate card is a pointer, not a copy. Updating or deleting a card in the parent’s library changes pricing for every subaccount currently assigned to it.
  • Which children are affected: only direct children of the parent org that have the card assigned via assigned_rate_card_id. Sibling parent orgs cannot see or assign your cards.
  • When the change takes effect: at the next send. Rate resolution reads the current card at send time, so there is no batch delay.
  • Past usage: already-sent traffic is not re-rated. The usage statement for a past period reflects the card (or deck, or flat margin) that was active when the traffic was sent. If you need to model what a new card would have cost on historical traffic, use the what-if pricing simulator.
  • Deleting a card: assigned children fall back to their inline deck or flat margin immediately. The assignment row shows a stale-link badge until you clear or replace it.

Example: WhatsApp card with a 5% margin

Create the card in the parent library:
Assign it to a subaccount:
From that point, WhatsApp-to-Nigeria and WhatsApp-to-Ghana sends on sub_acme price at wholesale + 5%, and the parent’s reseller statement reports the marked-up price against the wholesale cost. The subaccount’s own admin never sees your wholesale side — the margin lands in your statement, not their dashboard.

Worked example: a lane cell captured from what-if pricing

Lane cells are how you persist a candidate rate card drafted in the what-if pricing simulator. Imagine you ran a preview for a West Africa voice plan and the simulator returned this lane:
You can save that lane verbatim on a rate card so the what-if preview can be replayed or assigned later:
Lane cells are kept exactly as supplied. They do not participate in the live usage-statement margin pipeline the way deck cells do; they are interpreted by the what-if/assignment surfaces. To make the same card bill live traffic, add matching deck_cells with channel/destination/margin_pct, or assign a separate deck-only card.