Customer pricing

Price lists set what each of your customers pays on top of the wholesale price, with volume tiers, contract prices for one customer, prices fixed on single items, and lists in other currencies.

Your wallet is always charged the wholesale price. A price list says what a customer pays you for the same thing, and every purchase made for a customer is recorded with both figures, so you can bill them and see your margin.

Nothing here collects money from your customers: you bill them yourself, from the sales this API records.

What sets a price

The first of these that applies sets what a customer pays for one unit:

  1. A price fixed on the item itself: one domain, one licence, one number.
  2. A contract price agreed with that customer, while it is in date.
  3. The last price they paid for that item, when their list holds renewals.
  4. A rule on their price list.
  5. The list's markup.

Every sale says which it was in level: override, contract, list, or none when you have no list and nothing above applied, so the customer pays wholesale.

Price lists

A list has a markup for everything, and rules for the exceptions.

curl -X POST https://resellerhq.com/api/v1/price-lists \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Agency",
    "markupBps": 2000,
    "rounding": "99",
    "rules": [
      { "product": "domain", "match": "com", "action": "renew", "kind": "fixed", "value": 1899 },
      { "product": "domain_privacy", "kind": "markup", "value": 0 },
      { "product": "number", "match": "US", "term": "30d", "kind": "add", "value": 300 },
      { "product": "licence", "kind": "markup", "value": 3500 }
    ]
  }'
  • markupBps is the markup over wholesale for anything no rule covers, in basis points: 2000 is 20%.
  • rounding rounds worked-out prices up: 99 to the next amount ending in .99, 00 to the next whole dollar, none not at all.
  • allowBelowCost is false unless you set it. While it is false, a price that would fall under wholesale is raised to wholesale. This guards every level above, a price fixed on an item included.
  • renewals is follow unless you set it. See Renewals.
  • currency and rate are US dollars unless you set them. See Currency.

Your first list is the default: every customer without a list of their own is priced from it. With no list at all, sales are recorded at the wholesale price.

Rules

FieldWhat it does
productnumber, domain, domain_privacy or licence. domain_privacy is WHOIS privacy, sold per year per domain beside the name itself.
matchNarrows the rule: a domain ending (com), a country code (US) or a licence plan id. Leave it out to cover the whole product.
termNarrows a number rule to one rental length, such as 30d.
actionnew for first purchases, renew for renewals and extensions, restore for bringing a domain back out of redemption. Leave it out to cover all three.
minQuantityA volume tier, 2 or more. The rule only fits when at least this many are bought together. Leave it out for any quantity.
kindmarkup adds a share of wholesale, add adds a fixed amount, fixed is the price itself.
valueBasis points for markup; cents of the list's currency for add and fixed.

The narrowest rule that fits an item sets its price: a rule naming an ending beats one for all domains, and one that also names renew beats both. Two rules cannot cover exactly the same items, unless their volume tiers differ.

Volume tiers

Give two rules the same aim and different minQuantity, and the highest tier the quantity reaches wins:

[
  { "product": "licence", "kind": "fixed", "value": 1000 },
  { "product": "licence", "minQuantity": 10, "kind": "fixed", "value": 900 },
  { "product": "licence", "minQuantity": 50, "kind": "fixed", "value": 800 }
]

Nine seats are $10.00 each, ten are $9.00 each, fifty are $8.00 each. The tier sets the price of every unit, not only of those past it.

The quantity is what is bought together:

  • Domains: the years in one registration or renewal.
  • Numbers: the numbers asked for in one order. A renewal is one number.
  • Licences: the seats on the licence. A renewal counts all of them, and a seat added later is priced by the size the licence reaches, so the eleventh seat gets the tier of ten.

Narrowest still comes first: a rule naming a plan beats a tier for all licences, whatever the quantity.

Prices are for one unit: a year of a domain, a year of privacy on one domain, a seat for a month, one rental of a number.

A domain ending whose privacy the registrar includes for nothing has a wholesale privacy cost of zero, so { "product": "domain_privacy", "kind": "markup", "value": 0 } — the rule in the list above — passes privacy on at cost: nothing at all on those endings, and your own cost with no margin on the rest. An add or fixed rule charges your own amount for it anyway, even where it costs you nothing. Giving privacy away on an ending that does cost you is a price under wholesale like any other, so it needs allowBelowCost on the list; without it a fixed of 0 is quietly raised back to what you paid.

markup and add rules follow the wholesale price when it changes. A fixed price does not, which is why the floor above exists.

Markup is a share of the wholesale price and margin is a share of the selling price, so they are different numbers: 20% markup on $10.00 sells at $12.00, which is a 16.7% margin.

Put a customer on a list

curl -X PATCH https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "priceListId": "prl_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" }'

Send null to put them back on the default list.

The same route also takes addressLines and taxId: a customer can carry a billing address and tax number, printed in the Bill to block of their invoices.

DELETE /price-lists/{id} removes a list. Customers on it go back to the default list, the one whose isDefault is true. The default itself can only be removed once another list has been made the default with PATCH /price-lists/{id} and { "isDefault": true }, or when it is the only list left.

Buying for a customer

Every number order, domain registration and licence purchase is a sale to a customer: customerId is required on each, and a request that leaves it out answers 400 validation_error. Your wallet is charged wholesale as always; the sale records what the customer pays.

# A number
curl -X POST https://resellerhq.com/api/v1/numbers/orders \
  -H "Authorization: Bearer $RHQ_API_KEY" -H "Content-Type: application/json" \
  -d '{ "country": "US", "duration": "30d", "customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" }'

# A domain
curl -X POST https://resellerhq.com/api/v1/domains \
  -H "Authorization: Bearer $RHQ_API_KEY" -H "Content-Type: application/json" \
  -d '{ "domain": "northwind-telecom.com", "years": 1, "customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" }'

# Mailbox licences
curl -X POST https://resellerhq.com/api/v1/licences \
  -H "Authorization: Bearer $RHQ_API_KEY" -H "Content-Type: application/json" \
  -d '{ "plan": "google-workspace-starter", "domain": "northwind-telecom.com", "seats": 5, "customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" }'

Every later renewal, extension and added seat of that item is a sale to the same customer: a renewal, seat change or cancellation takes no customerId of its own. An unknown customerId answers 404 not_found, on these routes and on the catalogue routes below, where it is optional instead.

Contract prices for one customer

A customer can hold rules of their own. They have the same shape as list rules, and each can carry startsAt and endsAt. A contract rule that fits a purchase and is in date beats everything on the customer's list, however narrow the list's rule is.

curl -X PUT https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/contract-rules \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "product": "domain", "match": "com", "kind": "fixed", "value": 1499 },
      {
        "product": "licence", "kind": "markup", "value": 1000,
        "startsAt": "2026-11-01T00:00:00.000Z", "endsAt": "2027-11-01T00:00:00.000Z"
      }
    ]
  }'
  • PUT replaces the whole set. Send an empty list to clear it.
  • GET /customers/{id}/contract-rules reads it back.
  • A rule counts from startsAt up to, but not including, endsAt. Leave either out for no limit on that side. Outside its dates a rule is ignored and the list prices the purchase.
  • Among contract rules the same order holds as on a list: narrowest first, then the highest volume tier reached.
  • Two rules for the same items and the same tier must not be in date at the same time. This year's price and next year's may follow one another; two that overlap are refused with duplicate_rule, since which one a purchase got would be down to their order.
  • add and fixed values are in the currency of the customer's list.

A price fixed on one item

A domain, a licence and a number can each carry customerPrice: what this customer pays for one unit of this item from now on, whatever their contract and list say. It is set through the item's own PATCH route, and null clears it.

curl -X PATCH https://resellerhq.com/api/v1/domains/dom_01K6XJ4Q8ZT3M9R5V2B7WDHNCA \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "customerPrice": 1999 }'
ItemRouteThe price is for
DomainPATCH /domains/{id}One year of each renewal.
LicencePATCH /licences/{id}One seat for a month: every renewal, and seats added later.
NumberPATCH /numbers/{id}One renewal.

The amount is in the currency of the customer's list. It changes nothing in your wallet and does not rewrite sales already made. A number's customerPrice is charged for a renewal of any length. Set on an item that was not bought for a customer, it does nothing, since no sale is recorded for that item.

Renewals

A list's renewals setting says what a renewal costs the customer after the list changes.

  • follow, the default, prices each renewal from the list as it is on the day.
  • hold charges what the list last charged for that item, so a customer who registered at $12.00 a year keeps renewing at $12.00 when the list goes up. First purchases still follow the list.

What is held is the unit price of the item's last renewal that the list priced or, when it has not been renewed yet, of its first purchase.

  • Only a price the list set is held. A price fixed on the item and a contract price in date both come first while they stand; once the item price is cleared or the contract ends, the renewal goes back to the price the list last charged, not to the one that was fixed.
  • Seats added to a licence are first purchases at the list's price on the day. They do not move the price the licence's renewals are held at.
  • A number is held per rental length. A month's renewal holds the last month's price; a length the number has never been sold for follows the list.
  • A held price is still raised to wholesale if wholesale has passed it and the list does not allow selling under cost.
  • An item with nothing to hold, or whose held sale was in another currency, follows the list.
  • A held renewal is recorded with level: "list" and ruleId: null.

Domain and licence renewals cost you the wholesale price the item was bought at. A number's renewal is priced from the provider on the day, so numbers are where wholesale can overtake a fixed price. See When a price falls under wholesale.

Currency

A list can charge in a currency other than US dollars. Set currency to its ISO code and rate to how much of it one US dollar buys, in millionths: 920000 says one dollar buys 0.92 of it. A list in USD always has a rate of 1000000.

curl -X POST https://resellerhq.com/api/v1/price-lists \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Europe", "markupBps": 2000, "rounding": "99", "currency": "EUR", "rate": 920000 }'
  • Wholesale is always in US cents. A price is marked up in dollars, converted at the rate, rounding up to the next cent, and then rounded in the list's currency. Wholesale of $10.00 at 20% is $12.00, which is €11.04, which rounds up to €11.99.
  • add and fixed values on the list, contract prices and prices fixed on items are all in the list's currency, as written. Moving a list to another currency does not convert them.
  • The floor is wholesale in the list's currency, rounded up to the next cent. A price under it is raised to it.
  • The rate is yours to keep up to date. Nothing here follows the market. It must be between 10000 (0.01) and 100000000000 (100,000).
  • A list can charge in USD, EUR, GBP, CAD, AUD, NZD, CHF, SEK, NOK, DKK, PLN, INR, SGD, HKD, AED, ZAR, BRL or MXN: currencies counted in hundredths, as every amount here is. Any other code is refused with invalid_currency.

Each sale stores currency, the rate on the day, price in that currency and priceUsd, the same price back in US cents. Profit and margin are always worked out from priceUsd, and GET /sales/summary reports US cents whatever the sales were charged in.

Before you change a list

POST /price-lists/{id}/impact takes the changes you are thinking of making, in the same shape as an update, and saves nothing.

curl -X POST https://resellerhq.com/api/v1/price-lists/prl_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/impact \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "markupBps": 3000 }'

It answers with:

  • customers: how many customers are on the list.
  • items: how many active domains, licences and numbers they hold.
  • data: up to 50 of those items, each with current and proposed, the unit price of its next renewal now and after the change. Contract prices, prices fixed on items and held renewals are all taken into account, so an item they cover shows no change.
  • changed: how many of the items in data would change price.

Send { "isDefault": true } to look at making the list the default: the customers with no list of their own are counted too, at what they pay today.

When you do update a list and something changes, a price_list.updated webhook event is sent with:

  • id and name of the list.
  • changed: the settings that moved, such as ["markupBps", "rules"].
  • customers and customerIds: how many customers the list prices and who they are, up to 1,000 ids.
  • items: how many active domains, licences and numbers those customers hold.

Use it to tell your customers. Saving a list with nothing different sends nothing.

A change applies from the next purchase or renewal. There is no effective date and no notice period yet: to give customers notice, tell them first and make the change on the day it should start. The event does not carry old and new prices; call impact before you save if you need them.

When a price falls under wholesale

A fixed price, a contract price or a price on one item can end up under what the item costs you. What happens is the list's allowBelowCost:

  • false, the default: the price is raised to wholesale. The sale has floored: true.
  • true: the sale is made at the price set, at a loss.

Either way a sale.below_cost webhook event is sent with the saleId, customerId, product, reference, the level and ruleId that set the price, unitCost, unitPrice, currency and raised, which is true when the price was raised. Use it to find the fixed price that needs another look.

Customer prices in the catalogue

The routes that list what you can buy take an optional customerId and then return, beside your wholesale price, what that customer pays for a first purchase of one unit:

RouteWholesaleAdded with customerId
GET /domains/searchprice, privacyPricecustomerPrice, customerCurrency, customerPrivacyPrice
POST /domains/checkprice, privacyPricecustomerPrice, customerCurrency, customerPrivacyPrice
GET /licences/planspricecustomerPrice, customerCurrency
GET /numbers/offersamount in each of pricescustomerPrice, customerCurrency in each
curl "https://resellerhq.com/api/v1/licences/plans?customerId=cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" \
  -H "Authorization: Bearer $RHQ_API_KEY"

The price is for a quantity of one, from the customer's contract and list. For a volume tier, ask for a quote with the quantity.

Quote before you sell

POST /price-lists/quote works out what a customer pays for items at the wholesale prices you give it, without buying anything. Take the wholesale price from the product's own route, such as a domain search or the licence plans.

curl -X POST https://resellerhq.com/api/v1/price-lists/quote \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA",
    "items": [
      { "product": "domain", "match": "com", "action": "new", "cost": 1319 },
      { "product": "licence", "match": "google-workspace-starter", "quantity": 25, "cost": 840 }
    ]
  }'
  • cost is always in US cents. quantity is how many are bought together, for volume tiers, and is 1 when left out.
  • Each answer has price and its currency, priceUsd, the level and ruleId that set it, and floored when it was raised to wholesale.
  • With customerId, the customer's contract prices count. Prices fixed on single items do not, since a quote names no item.

Sales and margin

Each purchase or renewal made with a customerId adds a sale: what it cost you, what the customer pays and in which currency, and the level, list and rule that set the price. The price is fixed when the sale is made, so changing a list later never rewrites what was already sold. The monthly renewal of a mailbox licence adds a sale too.

  • GET /sales lists them, newest first.
  • GET /sales/summary gives the totals, your profit and your margin, in US cents.

Both take the same filters, and with none cover the whole account:

FilterNarrows to
customerIdOne customer
productnumber, domain, domain_privacy or licence
priceListIdSales priced from one list
levelWhat set the price: override, contract, list or none
from, toSales made at or after from and before to

GET /sales/summary?groupBy= splits the totals as well: by product, customer, priceList, level or month (the calendar month in UTC, as 2026-10). The answer gains groups, one row for each value with its key and the same totals.

curl "https://resellerhq.com/api/v1/sales/summary?from=2026-10-01T00:00:00.000Z&groupBy=product" \
  -H "Authorization: Bearer $RHQ_API_KEY"

A sale carries floored, true when its price was raised to wholesale, and for a number its term. A sale keeps the id of the rule that priced it, not a copy of the rule: editing the rule later does not change the sale, but what the rule said then is not kept.

Cancelled numbers

A number that is cancelled and refunded in full was not sold, so:

  • A sale not yet invoiced is removed.
  • A sale on an open invoice takes the invoice with it. The invoice is voided, you get invoice.voided, and its other sales are free to go on a new one.
  • A sale on a paid invoice stays, and the invoice reads as it was paid. A credit is recorded beside it: a sale with creditOf set to the first one and quantity, cost, price and priceUsd under zero. The totals stop counting the number, the customer's uninvoiced balance goes down by what they are owed, and the credit comes off their next invoice.

Invoices

An invoice bills one customer for sales you have already made for them. Each line is a copy of a sale at the price it was made at, so an invoice reads the same for good.

ResellerHQ does not take payment from your customers. There is no card form, no payment link and no payout. You collect the money however you already do, by bank transfer, your own card processor or cash, and then mark the invoice paid here. paid is your own record that the money arrived, and nothing checks it.

curl -X POST https://resellerhq.com/api/v1/invoices \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2f1c9a52-7d0e-4a8e-9a53-0c1f6f3f2b11" \
  -d '{ "customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA", "dueInDays": 14, "taxBps": 2000 }'
  • With customerId alone, the invoice takes every sale of theirs that is not on an invoice yet, oldest first. saleIds bills only the sales you name; GET /sales?invoiced=false lists the ones still waiting.
  • dueInDays is 14 when left out. taxBps adds tax as a share of the subtotal, in basis points, rounded to the nearest minor unit; left out, there is no tax. note is free text.
  • Invoices are numbered INV-0001, INV-0002 and so on across your account. A number is never used twice, and a refused request uses none.
  • An invoice is in one currency. If the customer has sales in two, the request is refused with mixed_currencies; send currency to bill one of them at a time.
  • An invoice holds at most 500 sales. With more waiting, the oldest 500 go on it and the rest go on the next.
ErrorWhy
nothing_to_invoiceThe customer has no sales waiting to be invoiced
mixed_currenciesThe sales are in more than one currency
sale_unavailableA sale you named is not this customer's, or is already on an invoice
invoice_not_openThe invoice is already paid or void
invalid_paid_atpaidAt is in the future, or more than a year before the invoice was issued

nothing_to_invoice is also the answer when the sales come to less than nothing, which credits can do: a credit goes on an invoice with sales that cover it. mixed_currencies is also the answer when a sale you named is not in the currency you sent.

A sale is on at most one invoice that still stands. Each sale carries invoiceId, which is null until it is invoiced.

Your billing details

PATCH /account/billing sets how you are identified on invoices, and the defaults a new one falls back to when you do not say otherwise.

curl -X PATCH https://resellerhq.com/api/v1/account/billing \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "legalName": "Northwind Telecom Ltd",
    "addressLines": ["1 Market Street", "San Francisco, CA 94105"],
    "taxId": "12-3456789",
    "billingEmail": "billing@northwind.example",
    "defaultDueDays": 30,
    "defaultTaxBps": 2000,
    "invoiceNote": "Thank you for your business"
  }'
  • Send only what changes; null clears a field. GET /account reads it all back under billing, null for anything you have not set and addressLines: [] for none.
  • defaultDueDays, defaultTaxBps and invoiceNote are used on a new invoice only when that invoice's own dueInDays, taxBps or note is left out; sending one, 0 included, always wins over the default. With neither a default nor a value sent, dueInDays is 14.
  • Every invoice keeps issuer: a copy of your name and billing details as they stood the moment it was made. Changing your billing details later never rewrites an invoice already issued, and issuer is null on one made before you had set any.
  • Live and sandbox are separate accounts, so each keeps billing details of its own.
RouteWhat it does
GET /invoicesLists invoices, newest first. Narrow with customerId, status, overdue or q, which looks in the invoice number and the customer's name and email
GET /invoices/{id}One invoice with its lines
POST /invoices/{id}/payMarks it paid. paidAt is when the money arrived, and is now when left out
POST /invoices/{id}/voidWithdraws it and frees its sales to go on another invoice
POST /invoices/{id}/shareCreates a public link to the invoice, or returns the one it already has
DELETE /invoices/{id}/shareRevokes the public link; on an invoice with none, this is also a no-op

An invoice is open until you mark it paid or void. Both are final: a paid invoice cannot be voided or paid again, and a void one cannot be reopened. To correct an open invoice, void it and create a new one, which takes the next number.

The due date is a whole day. An open invoice has overdue: true from 24 hours after dueAt, when that day has ended everywhere, so an invoice made with dueInDays: 0 is due today and not overdue today. Nothing else happens to an overdue invoice, and no reminder is sent.

POST /invoices/{id}/share is how you hand an invoice to the customer it bills: it answers with { token, createdAt }, and the invoice is then readable at GET /public/invoices/{token} with no API key at all. Pressing the button twice is safe, since a second call returns the same token rather than minting a new one. The public page shows that one invoice and nothing else: no id, no customer id, no sale ids, not even the token itself. DELETE /invoices/{id}/share kills the link at once; sharing again afterwards hands out a different token, and the old one answers 404 for good.

What a customer owes

GET /customers/{id}/balance answers with one row for each currency the customer has been sold in:

FieldWhat it holds
uninvoicedSold but not on an invoice yet. Before tax, since tax is set on the invoice. Under zero when the customer is owed a credit
openOn invoices waiting to be paid, overdue ones included
overdueThe part of open whose due date has passed
paidOn invoices you marked paid

Three webhook events follow an invoice: invoice.created, invoice.paid and invoice.voided. Each carries the invoice's id, number, customerId, currency, total and status.

Errors and limits

An account holds at most 50 price lists. A list, and a customer's contract, hold at most 200 rules each.

ErrorStatusWhy
too_many_price_lists409The account already has 50 lists
too_many_rules422More than 200 rules were sent
duplicate_rule422Two rules cover exactly the same items, share an id, or are two contract prices in date at once
invalid_dates422A contract price ends before it starts
invalid_rate422A list in USD was given a rate other than 1000000, or another currency was given none
invalid_currency422The currency is not one a list can charge in
default_price_list409The default list cannot be removed while other lists exist

A rate or a rule outside its allowed range is a 400 validation_error, like any malformed request.

Not built yet

  • A date for a list change to take effect, and a notice period before it.
  • A customer price in the dashboard's renew, extend and change-seats dialogs. The sale is recorded at the right price all the same.
  • Lists that inherit from the default list. A customer's own list replaces it entirely.
  • Rule versions, a percentage uplift at renewal, and a floor that counts fees as well as wholesale.
  • Different prices for the length of a domain registration in years beyond volume tiers, and a set-up price for a number apart from its rental.
  • Sending an invoice to the customer, reminders, scheduled invoicing, and editing an open invoice.
  • Collecting payment from your customers. An invoice is a record; you take the money.