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 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, void or fully credited
invoice_not_voidableThe invoice carries a credit note, so it has to stay standing
invalid_paid_atpaidAt is in the future, or more than a year before the invoice was issued
invalid_discountA 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; 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 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, 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.

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, or EU for any current member state), optionally a region, 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 leaving defaultTaxBps unset. Sending this list at all, even empty, turns the rules engine on for new invoices from now on, ahead of defaultTaxBps.
  • 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: true on 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 with business: "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's country and region, 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/:id with taxOverride: { "type": "exempt" } charges nothing at all, or { "type": "fixed", "rateBps": 500, "name": "Flat fee" } charges one flat named line instead of walking the rules. null puts the customer back on the account rules. A customer's own country and region (also set on PATCH /customers/:id) are what a rule is matched against.
  • Checking an EU VAT number: POST /customers/:id/tax-id/validate checks the customer's taxId against the EU Commission's VIES service and stores the result with its date, refusing with not_eu when the customer has no taxId or no EU country. VIES being down is not a failure of the customer: the check answers valid: null, "could not be checked", the number is kept as it was, and you choose whether to treat it as valid anyway - taxIdTrusted: true on PATCH /customers/:id - until you check again. Changing taxId itself clears both taxIdCheck and taxIdTrusted.
  • 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, the reverseCharge note 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 carry taxLines the same way.
ErrorWhy
home_country_requiredA preset other than none needs your home country set first
home_region_requiredThe Indian preset needs your home state or region set first
not_euThe 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 saleId it comes off, or a whole-invoice discount, applying to the sales' subtotal, when saleId is left out.
  • A discount is either a percentage, percentBps from 1 to 10,000 (1000 is 10%), or a flat amount, amount in the invoice's currency above zero - exactly one of the two.
  • Each discount becomes its own line, after every sale line, with a negative total and no saleId of its own, in the order you sent them. Leave description out and one is written for you: Discount, 10% off northwind.com, Discount, northwind.com, Discount, 10% or plain Discount, 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_large and a maximum, 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"]
  }'
  • amount is in minor units of currency, and date is when the money arrived - now, when left out. method is bank_transfer, cash, card or other; reference is free text, for a wire number or a cheque.
  • applyToInvoiceIds names 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 in currency first. 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-0001 and up, in its own gapless sequence, and its own shareable receipt: POST /payments/{id}/share and DELETE /payments/{id}/share, readable at GET /public/payments/{token} with no API key, the same shape GET /invoices/{id}/share already has. GET /customers/{id}/payments and GET /payments/{id} read them back.
  • An invoice carries paidAmount, what has been collected against it so far, and amountDue, now total less credited less paidAmount - 0 once it is not open.
ErrorWhy
invalid_paymentThe amount is zero or negative, or an invoice named is not this customer's
invoice_not_payableAn 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.

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}/void withdraws 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's amount out to credit everything still left on it, or give one to credit only part), or amount, a flat sum off the invoice's subtotal with no lines named - never both.
  • reason is required, and kept with the credit note for good.
  • GET /invoices/{id}/creditable shows what is still left to credit: each line with its credited so far and what remains creditable, and the invoice's own subtotal, tax and total still uncredited.
  • A credit note has its own number, CN-0001 and 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_large with a creditable figure, 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 void invoice, or one already credited in 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, and amountDue falls by the credit's total.
  • Crediting an open invoice in full closes it as credited, not paid - nothing more is owed on it, and it was never marked paid. creditedAt records 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:

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, net of any credit notes against them
overdueThe part of open whose due date has passed
paidOn invoices you marked paid, net of any credit notes against them
creditedWhat credit notes have taken off invoices that are not void, credited ones included
creditPayments 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.