Invoicing
Bill a customer for sales you have already made, discount an invoice before it is issued, correct one already sent with a void or a credit note, and record payments, refunds and statements against their balance.
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 record it here: a payment against an invoice, a part payment, a prepayment left as credit, or a refund out of credit you already hold for them. Every record is yours - nothing here checks that the money actually moved, the same honesty "mark it paid" always had, now with a shape for the ways a payment is never that tidy: more than the invoice, less than it, or none at all yet.
Raise an invoice
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.
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
customerIdalone, the invoice takes every sale of theirs that is not on an invoice yet, oldest first.saleIdsbills only the sales you name;GET /sales?invoiced=falselists the ones still waiting. dueInDaysis 14 when left out.taxBpsadds tax as a share of the subtotal, in basis points, rounded to the nearest minor unit; left out, there is no tax.noteis free text.- Invoices are numbered
INV-0001,INV-0002and 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; sendcurrencyto 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.
| Error | Why |
|---|---|
nothing_to_invoice | The customer has no sales waiting to be invoiced |
mixed_currencies | The sales are in more than one currency |
sale_unavailable | A sale you named is not this customer's, or is already on an invoice |
invoice_not_open | The invoice is already paid, void or fully credited |
invoice_not_voidable | The invoice carries a credit note, so it has to stay standing |
invalid_paid_at | paidAt is in the future, or more than a year before the invoice was issued |
invalid_discount | A discount names a sale that is not on this invoice |
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": "[email protected]",
"defaultDueDays": 30,
"defaultTaxBps": 2000,
"invoiceNote": "Thank you for your business"
}'- Send only what changes;
nullclears a field.GET /accountreads it all back underbilling,nullfor anything you have not set andaddressLines: []for none. defaultDueDays,defaultTaxBpsandinvoiceNoteare used on a new invoice only when that invoice's owndueInDays,taxBpsornoteis left out; sending one,0included, always wins over the default. With neither a default nor a value sent,dueInDaysis 14.- Every invoice and credit note keeps
issuer: a copy of your name and billing details as they stood the moment it was issued. Changing your billing details later never rewrites a document already issued, andissuerisnullon one made before you had set any. - Live and sandbox are separate accounts, so each keeps billing details of its own.
Tax
defaultTaxBps above is a flat rate typed in by hand - fine for an account that only ever
charges one rate, but a reseller selling across borders needs the rate for where each
customer is, and reverse charge for an EU business with a valid VAT number. The tax rules
engine (issue #341) is the alternative: a list of rules on your account, read at
GET /account/tax/rules and replaced whole at PUT /account/tax/rules.
curl -X PUT https://resellerhq.com/api/v1/account/tax/rules \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{ "country": "IN", "region": "Karnataka", "components": [
{ "name": "CGST", "rateBps": 900 }, { "name": "SGST", "rateBps": 900 }
] },
{ "country": "IN", "components": [{ "name": "IGST", "rateBps": 1800 }] }
]
}'- Each rule names where it applies - a
country(an ISO 3166-1 alpha-2 code, orEUfor any current member state), optionally aregion, optionally whether the customer must (or must not) carry a validated tax number (business: "required"or"none") - and what it charges, one or more named components, each its own rate. A new invoice, or a billing run row, is taxed by the first rule in the list whose country, region and business condition all match; no match at all means no tax, same as leavingdefaultTaxBpsunset. Sending this list at all, even empty, turns the rules engine on for new invoices from now on, ahead ofdefaultTaxBps. - Compound tax is two or more components on one rule: CGST and SGST above both show as their own line on the invoice, each at its own rate, rather than one combined figure.
- Reverse charge (
reverseCharge: trueon a rule) charges nothing: the invoice shows 0% and a legal note instead, with your own tax number and the customer's as they stood at issue. Pair it withbusiness: "required"so it only fires for a business with a validated number, not a consumer in the same country or bloc. - Presets are one-click starting points, built from your own home country and state or
region (
PATCH /account/billing'scountryandregion, set those first):POST /account/tax/presets/eu(your own rate at home - left for you to fill in - plus reverse charge for an EU business with a validated number),/uk(VAT at the public 20% standard rate),/in(CGST and SGST in your home state, IGST across state lines) and/none(clears the list). Each replaces the whole list; a preset is a starting point to edit, not tax advice - check it with whoever handles your tax before you rely on it. - Per-customer overrides beat every rule:
PATCH /customers/:idwithtaxOverride: { "type": "exempt" }charges nothing at all, or{ "type": "fixed", "rateBps": 500, "name": "Flat fee" }charges one flat named line instead of walking the rules.nullputs the customer back on the account rules. A customer's owncountryandregion(also set onPATCH /customers/:id) are what a rule is matched against. - Checking an EU VAT number:
POST /customers/:id/tax-id/validatechecks the customer'staxIdagainst the EU Commission's VIES service and stores the result with its date, refusing withnot_euwhen the customer has notaxIdor no EUcountry. VIES being down is not a failure of the customer: the check answersvalid: null, "could not be checked", the number is kept as it was, and you choose whether to treat it as valid anyway -taxIdTrusted: trueonPATCH /customers/:id- until you check again. ChangingtaxIditself clears bothtaxIdCheckandtaxIdTrusted. - The flat rate still works: a single invoice's own
taxBps(POST /invoices) always overrides whatever the rules engine would have charged, for the odd one that needs a manual figure. - Issued invoices are never recalculated. An invoice keeps the
taxLines(and, on a reverse charge, thereverseChargenote and both tax numbers) it was issued with; changing your rules afterwards only changes what the next invoice charges. A billing run's own preview rows carrytaxLinesthe same way.
| Error | Why |
|---|---|
home_country_required | A preset other than none needs your home country set first |
home_region_required | The Indian preset needs your home state or region set first |
not_eu | The customer has no taxId, or no country in the EU, to check |
Discount an invoice
A discount is money off before an invoice is issued: send it in the POST /invoices body,
alongside the sales being billed.
curl -X POST https://resellerhq.com/api/v1/invoices \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b3a1f2e-5c6d-4e7a-8f9b-1a2b3c4d5e6f" \
-d '{
"customerId": "cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA",
"discounts": [
{ "saleId": "sal_01K6XJ4Q8ZT3M9R5V2B7WDHNCA", "percentBps": 1000 },
{ "amount": 500, "description": "Loyalty credit" }
]
}'- A discount is either a line discount, naming the
saleIdit comes off, or a whole-invoice discount, applying to the sales' subtotal, whensaleIdis left out. - A discount is either a percentage,
percentBpsfrom 1 to 10,000 (1000is 10%), or a flat amount,amountin the invoice's currency above zero - exactly one of the two. - Each discount becomes its own line, after every sale line, with a negative
totaland nosaleIdof its own, in the order you sent them. Leavedescriptionout and one is written for you:Discount, 10% off northwind.com,Discount, northwind.com,Discount, 10%or plainDiscount, depending on whether it is a line or whole-invoice discount and a percentage or an amount. - A line discount cannot take that line below zero; discounts together cannot take the
invoice's subtotal below zero. Going past either is refused with
discount_too_largeand amaximum, what was left to discount. - Tax is worked out after the discount: it is a share of the subtotal with every line, discounts included, already added up. An invoice holds at most 20 discounts.
Mark it paid
curl -X POST https://resellerhq.com/api/v1/invoices/inv_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/pay \
-H "Authorization: Bearer $RHQ_API_KEY"The quick path for the common case: the whole invoice, paid in full, today. Behind it, this
now records a payment for the full amount due - the same record [Record a payment](#record-a-
payment) below makes by hand, numbered in its own PMT- sequence and listed on the invoice
like any other. paidAt is when the money arrived, and is now when left out. An invoice is
open until it is paid in full, voided, or credited in full, which closes it as credited
instead - see Void or credit below. paid and void are both final: a
paid invoice cannot be voided or paid again, and a void one cannot be reopened.
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.
Record a payment
For everything "mark it paid" is too blunt for: part of an invoice, more than one invoice at once, a prepayment with nothing to apply it to yet, or an amount that overshoots and leaves change.
curl -X POST https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/payments \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b1e2d3c-4f5a-4b6c-8d7e-1f2a3b4c5d6e" \
-d '{
"amount": 15000,
"currency": "USD",
"method": "bank_transfer",
"reference": "Wire #4471",
"applyToInvoiceIds": ["inv_01K6XJ4Q8ZT3M9R5V2B7WDHNCA"]
}'amountis in minor units ofcurrency, anddateis when the money arrived - now, when left out.methodisbank_transfer,cash,cardorother;referenceis free text, for a wire number or a cheque.applyToInvoiceIdsnames which open invoices to pay, in that order, each paid up to what it owes before moving to the next; left out, it pays the oldest open invoices incurrencyfirst. Whatever is left once every named invoice is paid in full stays as credit on the customer's account rather than being lost or refused - a payment larger than what was named always leaves the rest as credit, never an error.- Two payments recorded for the same invoice, even at once, can never overpay it: each is capped at what the invoice still owes at the moment it is applied, and the rest of either one becomes credit.
- A payment has its own number,
PMT-0001and up, in its own gapless sequence, and its own shareable receipt:POST /payments/{id}/shareandDELETE /payments/{id}/share, readable atGET /public/payments/{token}with no API key, the same shapeGET /invoices/{id}/sharealready has.GET /customers/{id}/paymentsandGET /payments/{id}read them back. - An invoice carries
paidAmount, what has been collected against it so far, andamountDue, nowtotallesscreditedlesspaidAmount-0once it is not open.
| Error | Why |
|---|---|
invalid_payment | The amount is zero or negative, or an invoice named is not this customer's |
invoice_not_payable | An invoice named is not open |
Customer balance and credit
curl https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/balance \
-H "Authorization: Bearer $RHQ_API_KEY"Alongside the figures in What a customer owes below, each row now
carries credit: payments not yet applied to an invoice, less refunds already taken out of
them - money the customer can have applied to their next invoice, or back. It builds up from
an overpayment, a prepayment with nothing to apply it to, or a credit note issued against an
invoice that was already paid.
curl -X POST https://resellerhq.com/api/v1/invoices/inv_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/apply-credit \
-H "Authorization: Bearer $RHQ_API_KEY"Draws on the customer's credit, oldest payment first, to pay down this one invoice as far as
the credit in its own currency goes - no body, since the invoice already names its own
customer. Refused with invoice_not_payable when the invoice is not open, or no_credit when
there is none in that currency. Nothing here is automatic: a billing run does not reach for a
customer's credit on its own, and neither does anything else - every application is a request
you make.
Refunds
Money handed back out of a customer's credit, when they are owed more than their next invoice will use.
curl -X POST https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/refunds \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3a4b5c6d-7e8f-4a9b-9c0d-1e2f3a4b5c6d" \
-d '{ "amount": 5000, "currency": "USD", "method": "bank_transfer", "reference": "Refund #118" }'Refused with insufficient_credit when amount is more than the customer's credit in that
currency - a refund can never put a customer's balance below what they actually hold. A
refund has its own number, RFD-0001 and up, but no receipt of its own and no public link: it
carries no document, only a record. GET /customers/{id}/refunds lists them, newest first.
As with a payment, ResellerHQ does not move this money. Recording a refund is you telling your own books that you already paid it back, by whatever means you actually used.
Statements
curl "https://resellerhq.com/api/v1/customers/cus_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/statement?from=2026-10-01T00:00:00.000Z&to=2026-10-31T23:59:59.999Z" \
-H "Authorization: Bearer $RHQ_API_KEY"One row set per currency the customer had activity in over the range: an opening balance, then
every invoice, credit note, payment and refund dated inside it with a running balance after
each, and a closing balance that is opening plus every row added together. amount is under
zero on a payment or a credit note - both bring down what is owed - and above zero on an
invoice or a refund. GET /customers/{id}/statement.csv answers the same rows as text/csv,
for a bookkeeper who wants the file rather than the JSON. The dashboard's own Statement page
(reached from a customer's Manage menu) prints the same thing, with the same honesty: a
record, never a bill of its own.
The public share link
curl -X POST https://resellerhq.com/api/v1/invoices/inv_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/share \
-H "Authorization: Bearer $RHQ_API_KEY"This 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.
A credit note has a public link of its own, POST /credit-notes/{id}/share and
DELETE /credit-notes/{id}/share, with the same meaning, readable at
GET /public/credit-notes/{token}.
Void or credit
Both withdraw something about an invoice. Which one is right depends on whether the invoice was ever correct:
- Void an invoice that should never have existed: it was raised on the wrong sales, for
the wrong customer, or by mistake.
POST /invoices/{id}/voidwithdraws it entirely and frees its sales to be billed again, on a new invoice that takes the next number. A void invoice is never paid, never credited, and nothing is owed on it. - Credit an invoice that was correctly issued and has already been sent: the work was done and billed right, but some or all of it now needs to come off, for a refund, a cancellation or a goodwill adjustment. A credit note corrects it without touching the original, which the customer may already have a copy of.
Once you have sent an invoice to a customer, voiding it is rarely the right move: they are holding a document that says one thing, and voiding it makes your records say another with no link between them. A credit note names the invoice it corrects, so both documents keep telling the same story.
The two are not interchangeable once either has happened. An invoice that already carries a
credit note can no longer be voided: POST /invoices/{id}/void answers
invoice_not_voidable, because the credit note names that invoice and has a number of its
own in your records, so the invoice has to stay standing. Credit the rest of it instead,
which closes it as credited. The same holds the other way: a void invoice takes no credit
note (invoice_not_creditable), there being nothing owed on it to correct.
Issue a credit note
curl -X POST https://resellerhq.com/api/v1/invoices/inv_01K6XJ4Q8ZT3M9R5V2B7WDHNCA/credit-notes \
-H "Authorization: Bearer $RHQ_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c4d5e6f-1a2b-4c3d-9e8f-0a1b2c3d4e5f" \
-d '{
"reason": "Number cancelled after the invoice was sent",
"lines": [{ "saleId": "sal_01K6XJ4Q8ZT3M9R5V2B7WDHNCA" }]
}'- Credit either
lines, naming the invoice lines to credit (leave a line'samountout to credit everything still left on it, or give one to credit only part), oramount, a flat sum off the invoice's subtotal with no lines named - never both. reasonis required, and kept with the credit note for good.GET /invoices/{id}/creditableshows what is still left to credit: each line with itscreditedso far and what remainscreditable, and the invoice's ownsubtotal,taxandtotalstill uncredited.- A credit note has its own number,
CN-0001and up, in its own gapless sequence across your account, never shared with invoice numbers. - Its tax carries the same proportion as the lines it credits: crediting a line that is 40% of the invoice's subtotal credits 40% of its tax, whatever rate the invoice was taxed at. Crediting an invoice down to nothing always comes out to its exact total, to the penny, whatever the rounding on the way.
- A credit note can never exceed what is still left uncredited on the invoice, on a line or
across it: asking for more answers
credit_too_largewith acreditablefigure, what was left before tax. - A credit note is never edited and never deleted. To correct a mistaken credit note, issue an invoice again for the amount, or a second credit note if there is more to take off.
- A
voidinvoice, or one alreadycreditedin full, cannot take another credit note:invoice_not_creditable.
curl https://resellerhq.com/api/v1/credit-notes/crn_01K6XJ4Q8ZT3M9R5V2B7WDHNCA \
-H "Authorization: Bearer $RHQ_API_KEY"GET /invoices/{id}/credit-notes lists the credit notes against one invoice, newest first.
GET /credit-notes lists every credit note on the account, newest first, narrowed with
customerId, invoiceId or q, which looks in the credit note's number and the customer's
name and email.
What a credit note does to the invoice
- Crediting part of an open invoice lowers what is still owed on it: the invoice stays
open, andamountDuefalls by the credit'stotal. - Crediting an open invoice in full closes it as
credited, notpaid- nothing more is owed on it, and it was never marked paid.creditedAtrecords when that happened. - Crediting a paid invoice, in part or in full, stays
paid: the money already arrived, so there is nothing left to collect. What the credit comes to is instead money owed back to the customer, which shows up on their balance.
Every invoice carries credited, what its credit notes come to so far, tax included, 0
when it has none, and amountDue, what is still owed: total less credited on an open
invoice, and 0 on one that is paid, void or fully credited.
Crediting an invoice also lowers revenue in the period it was credited:
GET /sales/summary's price, and every groupBy split of it, is net of what credit notes
have taken off, each line converted at its own sale's rate rather than the invoice's. Profit
falls by the same amount, since wholesale cost does not change, and can go below zero for a
sale credited for more than its margin. A flat-amount credit note (amount with no lines)
names no sale and so cannot be netted off any sale or group - see
Sales and margin.
What a customer owes
GET /customers/{id}/balance answers with one row for each currency the customer has been
sold in:
| Field | What it holds |
|---|---|
uninvoiced | Sold but not on an invoice yet. Before tax, since tax is set on the invoice. Under zero when the customer is owed a credit |
open | On invoices waiting to be paid, overdue ones included, net of any credit notes against them |
overdue | The part of open whose due date has passed |
paid | On invoices you marked paid, net of any credit notes against them |
credited | What credit notes have taken off invoices that are not void, credited ones included |
credit | Payments not yet applied to an invoice, less refunds already taken out of them - see Customer balance and credit |
Four webhook events follow an invoice: invoice.created, invoice.paid,
invoice.voided and credit_note.issued. The first three carry the invoice's id,
number, customerId, currency, total and status; credit_note.issued carries the
credit note's own id, number, invoiceId, invoiceNumber, customerId, currency,
subtotal, tax, total and reason.
Not built yet
- Sending an invoice, a credit note or a receipt 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.