Discounts
A discount is a record in your company: a name, what it takes off, and the
products, customers and days it applies to. It takes effect only on a document
line or a document that references its id. Invoices, quotes, order
confirmations and credit notes reference discounts in discountIds; how a
document applies them is in
Billing documents.
Discounts live under the base URL
https://api.ledgerbee.com/api/v1 and use
the product scopes:
products-read— list and get discounts, and ask where they apply.products-write— create, update and delete discounts, their amounts, their assignments and their codes.discounts-check— look up one discount code and check a cart, and nothing else. Give this scope to a webshop's key so its checkout can check the code a shopper types without being able to list your discounts.
Authenticate with the x-api-key header. Ids in paths and bodies are UUIDs
and match in any letter case; responses return them upper-case.
What a discount takes off
Each discount has a type, fixed when it is created, and a scope.
type | What it takes off | Figures it carries |
|---|---|---|
percentage | percentagePct percent, above 0 and at most 100 | percentagePct; no amounts |
fixed | An amount, per eligible line or once per document (see scope) | One amount per currency |
per_unit | An amount off each unit on the line; line scope only | One amount per currency |
scope | Where it applies |
|---|---|
line | To each eligible line on its own. |
document | Once per document, after every line discount, when the customer matches and at least one line's product is eligible. |
A fixed or per_unit discount states a positive amount in each currency it
supports, in major units ("50.00"). Amounts are never converted: a document
in a currency the discount has no amount for is refused with
DISCOUNT_NO_AMOUNT_IN_CURRENCY. Add a currency with
PUT /v1/discounts/{discountId}/amounts/{currency} and remove one with
DELETE on the same path.
The name is printed as the reason on the e-invoice allowance, so give it a
name the customer can read. priority orders discounts the server picks
itself (lowest first), and combinable: false stops a line from referencing
the discount together with another one. maxRedemptions and maxPerCustomer
limit how often it can be used; null means no limit. On PATCH, these two
limits and durationInPeriods are the only discount fields that accept null. requiresCode: true
makes the discount apply only through one of its codes: a document
or an applicability check that names it by id alone is refused with the reason
codeRequired, and GET /v1/discounts/applicable leaves it out.
durationInPeriods limits how many billing periods the discount applies to on
a subscription; null means every period (see
Codes on a subscription).
Redemption limits
A use is counted when an invoice that references the discount is booked: one use per invoice, however many of its lines the discount reduces. Saving or editing a draft and sending a quote use nothing, and invoices imported from another system are never counted. A booking that would go past a limit is refused and the invoice stays a draft. When a credit note leaves nothing creditable on an invoice, the invoice's uses are given back; a partial credit note keeps them.
A use claimed through a code also counts against the
code's own maxRedemptions. The limits are judged in this order, and the first
one used up is the one named: the discount's maxRedemptions, the customer's
maxPerCustomer, then the code's maxRedemptions.
Assignments
An assignment says where a discount applies: the products, the customers and a window of days. A discount with no assignment applies nowhere, and a discount can carry several assignments.
| Field | Set | Null, or left out on create |
|---|---|---|
productId | that product | every product, unless productGroupId is set |
productGroupId | every product in that product group | no group |
customerId | that customer | every customer, unless customerGroupId is set |
customerGroupId | every customer in that customer group | no group |
startsAt | the first day it applies | from any day |
endsAt | the last day it applies | it never ends |
An assignment sets at most one of productId and productGroupId, and at most
one of customerId and customerGroupId. A group is read when a document is
checked, so a product or customer moved into the group is covered from then on,
and one moved out is not. Deleting a product group or a customer group deletes
the assignments aimed at it.
startsAt and endsAt are calendar days (YYYY-MM-DD) and both are
inclusive. On PATCH, a field left out keeps its value and a field sent as
null clears it.
| Operation | Endpoint | Scope |
|---|---|---|
| List discounts | GET /v1/discounts | products-read |
| Get a discount | GET /v1/discounts/{discountId} | products-read |
| Create a discount | POST /v1/discounts | products-write |
| Update a discount | PATCH /v1/discounts/{discountId} | products-write |
| Delete a discount | DELETE /v1/discounts/{discountId} | products-write |
| Set an amount | PUT /v1/discounts/{discountId}/amounts/{currency} | products-write |
| Remove an amount | DELETE /v1/discounts/{discountId}/amounts/{currency} | products-write |
| Add an assignment | POST /v1/discounts/{discountId}/assignments | products-write |
| Change an assignment | PATCH /v1/discounts/{discountId}/assignments/{assignmentId} | products-write |
| Remove an assignment | DELETE /v1/discounts/{discountId}/assignments/{assignmentId} | products-write |
| Discounts that apply | GET /v1/discounts/applicable | products-read |
| Whether one applies | GET /v1/discounts/{discountId}/applicability | products-read |
| List a discount's codes | GET /v1/discounts/{discountId}/codes | products-read |
| Add a code | POST /v1/discounts/{discountId}/codes | products-write |
| Change a code | PATCH /v1/discounts/{discountId}/codes/{codeId} | products-write |
| Delete a code | DELETE /v1/discounts/{discountId}/codes/{codeId} | products-write |
| Look up a code | GET /v1/discounts/codes/{code} | discounts-check or products-read |
| Check a cart | POST /v1/discounts/check | discounts-check or products-read |
The list of discounts is paginated (page up to 100,000, limit up to 100)
and filters by active, type and scope. The list of a discount's codes is
paginated the same way, oldest code first. Full request and response schemas
are in the API Reference.
Applicability
A discount applies to a product for a customer on a day when it is active and one of its assignments matches all three:
- its
productIdis the product, or itsproductGroupIdis the product's group, or both are null; - its
customerIdis the customer, or itscustomerGroupIdis the customer's group, or both are null; startsAtis null or on or before the day, andendsAtis null or on or after it.
GET /v1/discounts/applicable?customerId=…&productId=…&asOf=YYYY-MM-DD lists
every discount that applies, ordered by priority. A discount whose redemption
limit has no use left, in total or for this customer, is left out. It does not
look at currency, so check amounts before referencing a fixed or per-unit
discount on a document in another currency.
GET /v1/discounts/{discountId}/applicability takes the same query and
answers for one discount:
Code
remaining is the uses left under maxRedemptions and remainingForCustomer
the uses left for this customer under maxPerCustomer; each is null when the
discount has no such limit. Asking holds no use, so a booking can still be
refused if another booking takes the last use first. The reasons are checked
in the order of the table:
reason | Meaning |
|---|---|
null | It applies. |
unknown | Your company has no discount with this id. |
inactive | The discount has active: false. |
codeRequired | The discount has requiresCode: true, so it applies only through one of its codes. |
notAssigned | No assignment names this product and customer, or all of them. |
notInGroup | An assignment would match, but the product or the customer is not in the group it targets. |
outsideWindow | An assignment matches the product and customer, but not on this day. |
maxRedemptionsReached | Every use under maxRedemptions is taken. |
maxPerCustomerReached | This customer has taken every use under maxPerCustomer. |
Codes
A code is the text a customer types to claim a discount, such as SUMMER25
in a webshop's "apply code" field. A discount can carry many codes. The
discount's assignments, window and limits still apply to a code, and the code
can add three limits of its own.
| Field | Meaning |
|---|---|
code | 1 to 64 characters with no space inside. Matched without regard to case or surrounding spaces; normalizedCode is the upper-cased text a lookup compares. |
customerId | The one customer who may use the code; it must be a customer the discount's assignments cover. Null: any customer the discount applies to. |
maxRedemptions | Uses allowed through this code, on top of the discount's own limits. Null: no limit of its own. |
expiresAt | The last calendar day the code can be used, inclusive (YYYY-MM-DD). Null: it never expires. |
active | false switches the code off without touching the discount or its other codes. |
Only one active code in your company can hold a given text. Switching a code off frees its text, so a new code, on this discount or another, can reuse it. A code that a document references, or that has been used, cannot be deleted; switch it off instead.
Code
GET /v1/discounts/codes/{code} answers the code a shopper typed, the
discount it claims (without its assignments) and whether it applies. Pass
customerId for a known customer; without it the shopper is a guest, to whom
only assignments for every customer apply and a code made for one customer
does not. Pass productId to judge one product, and asOf=YYYY-MM-DD for a
day other than today.
Code
Code
The example shows only some of the code's fields; the full shape is in the
API Reference. A code your company does not have answers 200 with
reason: codeUnknown and code and discount null; a code of another company
answers the same. The lookup checks the discount's own reasons first, then the
code's, in this order:
reason | Meaning |
|---|---|
codeUnknown | Your company has no code with this text. |
inactive … maxPerCustomerReached | The discount's own reasons, as in Applicability, except codeRequired: a code is how a code-only discount applies. |
codeInactive | The code has active: false. |
codeExpired | The day is after the code's expiresAt. |
codeCustomerMismatch | The code is for another customer, or for a customer and none was given. |
codeMaxRedemptionsReached | Every use under the code's maxRedemptions is taken. |
Like the applicability check, a lookup holds no use.
Codes on an invoice
An invoice takes the codes a customer gave in discountCodes, beside
discountIds, on POST /v1/invoices and PATCH /v1/invoices/{invoiceId}.
Send the same list your checkout sent to the cart check, and
the invoice prices the way the check did:
- Each code is matched like the code lookup and claims its discount, which then
applies as if named by id. A discount with
requiresCode: trueapplies this way and no other. - A line-scope discount lands on every line whose product it covers. A document-scope one applies to the document.
- A discount named both by id and through a code, or through two codes, applies once and carries the first code.
Code
Each applied discount in the response carries code, the code's text as it was
created, when a code claimed it; a discount named by id has no code. The
invoice stores the code, so a credit note of the invoice gives the discount back
with the same code.
A code is judged on the invoice date for the invoice's customer. A code that is
unknown, switched off, expired on that date or made for another customer is
refused with 400 DISCOUNT_NOT_APPLICABLE: details.reason is codeUnknown,
codeInactive, codeExpired or codeCustomerMismatch, and details.code is
the code as matched. The code's maxRedemptions is not judged when the invoice
is saved, only when it is booked, as for the discount's own limits. A booking
that would pass it is refused with 400 DISCOUNT_CODE_REDEMPTION_LIMIT_REACHED
and the invoice stays a draft. The booked use names the code, so the code's
uses and the lookup's remainingForCode count it.
An update restates every line. A discount the invoice carries through a code
keeps its code when the update names the discount again by id, so reading an
invoice and writing it back does not need the code. The kept code is judged
again on the update's date and customer. Codes, like discountIds, are sent
together with lines.
Codes on quotes and order confirmations
A quote and an order confirmation take discountCodes the same way, on
POST /v1/quotes, PATCH /v1/quotes/{id}, POST /v1/order-confirmations
and PATCH /v1/order-confirmations/{id}. Each applied discount
carries code, and an update that names the discount again keeps its code.
Saving one uses none of the code's limit.
Converting carries each code with its discount, so the customer does not give the code again:
| Conversion | A code switched off since the quote | A code that expired after the quote's date | A different customer on the payload |
|---|---|---|---|
discountPricing: "asQuoted" | Kept as quoted | Kept as quoted | Kept as quoted |
discountPricing: "current", or none | 400 DISCOUNT_NOT_APPLICABLE, codeInactive | Applies | 400 DISCOUNT_NOT_APPLICABLE, codeCustomerMismatch |
The code is judged on the date the conversion chain starts at, the same day
the discount is: the quote's date, or an order confirmation's own date when it
was made without a quote. A document converted as quoted keeps the code on
every later edit. A conversion
payload's own discountCodes are resolved as on any save.
Quote to order confirmation to invoice carries the code all the way. Only
booking the invoice counts the code's use, and a code at its limit refuses that
booking with 400 DISCOUNT_CODE_REDEMPTION_LIMIT_REACHED. When a conversion's
response carries a warning about a discount the quote claimed through a code,
the warning names that code in discountCode.
Codes on a subscription
A subscription takes a code in discountCode on POST /v1/subscriptions, and
a buyer's code in the partner checkout
quote and session, or on a checkout vouch (POST /v1/portal-checkout/vouch).
The code claims its discount for the subscription itself; add-ons created with
it do not take it.
The discount bills on every period that starts before it ends. A discount with
durationInPeriods lasts that many billing periods from the first one billed,
so a trial does not use it up. After that the subscription bills at its
regular price. A worked example, from the code to the invoices, is in
Partner-Hosted Checkout.
The subscription takes one use of the discount and one of the code when it is
created, whatever number of periods it bills. The limits are judged then,
under a lock on the discount, and a code with no use left is refused with
400 DISCOUNT_REDEMPTION_LIMIT_REACHED,
DISCOUNT_CUSTOMER_REDEMPTION_LIMIT_REACHED or
DISCOUNT_CODE_REDEMPTION_LIMIT_REACHED; nothing is created. A checkout also
judges them when it quotes and when it opens the card window, so a buyer is
told before a card is asked for.
Code
Codes on a pricing page
A buyer can enter a code before choosing a plan. The plan catalogue reads take
it as discountCode: a query parameter on GET /v1/portal/plans and
GET /v1/portal/plans/{planId}, and a body field on
POST /v1/portal/plans/resolve. Each subscription card then carries
discount, the code priced for that card's first period exactly as the
checkout quote prices it for a
checkout starting today:
| Field | Meaning |
|---|---|
firstPeriodNetMinor | The card's first invoice with the code, before VAT, in minor units of the card's currency. One-time charges on that invoice are included. |
firstPeriodAmountOffMinor | What the code takes off that invoice, before VAT. Positive; 0 when it touches none of the card's lines. |
endsAt | YYYY-MM-DD: a period starting on or after this day bills at the regular price. Null when the discount has no end. |
code, name, type, durationInPeriods | As on the quote. |
A card the code cannot price carries discount: null - a call-to-action card,
or a fixed discount with no amount in the card's currency. A code that is
unknown, used up or not for this buyer refuses the whole read with its
DISCOUNT_* code, as the quote does. On resolve with a customerId, that
customer's own uses count against the discount's per-customer limit. Without
discountCode, no card carries discount and the read is unchanged.
A read naming a code spends one use of the code-attempt budget the quote and the session share (see Rate limits). Pass the code on to the checkout quote and session when the buyer picks a plan; it is judged again there, and again under the discount's lock when the subscription is created.
Code
The hosted portal's plans page does the same: the buyer enters the code above the plans, each plan shows its first period with the code and the day the regular price starts, and the code carries into the checkout the buyer opens.
Check a cart
POST /v1/discounts/check prices a webshop cart before any order exists: what
each discount and code takes off the lines, the ones that do not apply and why,
and the uses left. It creates no document and holds no use. Booking the invoice
counts the use and checks every limit again, so a cart that checked clean can
still be refused at booking when the last use went to someone else.
| Field | Meaning |
|---|---|
customerId | The customer buying. Omitted: a guest, to whom only assignments for every customer apply, a code made for one customer does not apply, and no per-customer use is counted. |
currency | The cart's currency (ISO 4217). A fixed or per-unit discount needs an amount in it. |
date | The calendar day the cart is priced on (YYYY-MM-DD). |
lines | 1 to 200 lines of productId, quantity (negative for a returned item), unitPrice (a decimal string) and an optional vatRatePct. |
codes | Up to 20 codes the shopper typed, matched like the code lookup. |
discountIds | Up to 50 discounts applied by id. |
The discounts in discountIds apply first, then the codes, each in the order
sent. A discount named twice, by id and by a code or by two codes, applies once.
A line discount is named once for the whole cart and lands on every line whose product it covers, so a Widget-only code takes its share off each Widget line. A document discount is taken from the net of the lines it covers, a returned line included: on Widget 1,000, Gadget 500 and a returned Widget −200, a 10% whole-order code takes 130.
Send vatRatePct on every line to get each document discount's split by VAT
rate. When a line the discount falls on has none, vatSplitAvailable is false
and vatSplit is null; the reduction itself is still returned.
Code
Code
Each line's originalNet is the line after its line discounts and before any
document discount. Every amount is in the cart's currency.
A code or discount that does not apply comes back in refusals while the rest
of the cart is still priced. Its reason is one of the reasons in
Applicability and Codes, or the error code a
document referencing it would be refused with:
reason | Meaning |
|---|---|
codeUnknown … codeMaxRedemptionsReached | The code's own reasons, as for the code lookup. |
unknown | Your company has no discount with an id in discountIds. |
inactive … maxPerCustomerReached | The discount's reasons, as in Applicability. A line discount that covers none of the cart's lines gives the reason for the line closest to applying. |
DISCOUNT_NO_AMOUNT_IN_CURRENCY | A fixed or per-unit discount has no amount in the cart's currency. |
DISCOUNT_NOT_COMBINABLE | A discount that may not be combined shares a line or the cart with another; lineIndex names the line, and is null when the two are document discounts. |
DISCOUNT_EXCEEDS_LINE / DISCOUNT_EXCEEDS_DOCUMENT | The discount would take the line, or the lines it covers, below zero. |
limits has one row per discount of your company that the cart named (an
unknown id or a codeUnknown code has none): remaining and
remainingForCustomer as in Applicability (the latter null
for a guest), and remainingForCode for a discount claimed through a code with
its own limit.
The cart check and the code lookup share a budget of 600 uses a minute per API
key; an OAuth caller is counted per grant, or per OAuth client for a token
issued without a grant. A lookup uses one. A cart check uses one per code it
names, and one when it names no code, so a check with 20 codes uses 20. A check
that needs more than is left is refused whole, and none of its codes is tried.
Past the budget they answer 429 with code
DISCOUNT_CHECK_RATE_LIMITED and a Retry-After header giving the seconds to
wait. A POST /v1/subscriptions, a checkout quote or session, or a checkout
vouch that names a discountCode uses one from the same budget. The budget
stops one key from guessing codes.
Worked example
A 10% loyalty discount for one customer, on every product, with no end date:
Code
Assign it to the customer, leaving the product and the dates out:
Code
A Black Friday discount of 50 DKK or 8 EUR off each document, for every customer and every product, from 27 to 30 November:
Code
On 28 November, GET /v1/discounts/applicable for the loyalty customer
returns both discounts. For any other customer it returns Black Friday only,
and on 1 December it returns the loyalty discount only. A document in SEK that
referenced Black Friday would be refused, because the discount has no SEK
amount.
Deleting and deactivating
DELETE /v1/discounts/{discountId} removes a discount with its amounts and
assignments. Once a document or a redemption records the discount, delete is
refused with DISCOUNT_IN_USE; send "active": false instead, which stops it
applying anywhere and keeps the record the documents point to.
Discounts on subscriptions
A discount may name a product that plans and customer subscriptions carry, and a plan or subscription may carry a product a discount names. A customer subscription applies a discount only when it references it, by id or through a code (Codes on a subscription); an assignment alone never reaches a subscription's invoices.
Reference discounts when you create the subscription, with discountIds on
POST /v1/subscriptions, or change them later with
discountIds and discountEndDates on a
product change. Each
referenced discount bills on every billing period that starts before its end
date. A discount with durationInPeriods gets an end date that many periods
after the first period billed, counted after any trial; one without has no end
date unless you set one. The subscription takes one use of each discount's
limits when the discount is attached, however many periods it bills, so a limit
is refused when you attach, never at a billing run.
A subscription's discounts lists what it references, in order, each with the
end date it carries:
Code
A plan replacement keeps the references. targetsNoProduct is true for a
line-scope discount that names none of the subscription's products: it takes
nothing off until one of its products comes back or you assign it to one of
these.
A subscription bills each period on the discount's terms as they were when that period's version started: what it takes off, its amount in the subscription's currency, its scope and whether it combines. So an edit to those terms cannot reach a period that is already running.
PATCH /v1/discounts/{discountId}, PUT /v1/discounts/{discountId}/amounts/{currency}
and DELETE /v1/discounts/{discountId}/amounts/{currency} refuse such an edit
with 409 DISCOUNT_IN_USE_ON_SUBSCRIPTIONS while live subscriptions reference
the discount, and save nothing. details.total counts the subscriptions and
details.subscriptions lists them when your key has subscriptions-read;
without it the list is empty, and the name and customerName of each entry
in subscriptionChanges below are null. To make the edit, send
"applyToSubscriptions": true in the body (on the DELETE, the query
?applyToSubscriptions=true); accepting needs a key with subscriptions-write,
and without it the edit is refused as above. The edit is saved, and each subscription takes
the new terms from its next billing period as a new version:
Code
A subscription that cannot take the new version is listed with
"outcome": "failed" and the reason in errorCode; it keeps billing the old
terms, and the other subscriptions and the saved edit are not affected. A
rename, a priority or limit change, an amount in a currency none of the
subscriptions bills in, and switching the discount on or off are not refused:
the first ones change nothing a subscription bills, and whether a discount is
active is read when each period is billed.
Errors
Errors use the standard envelope described in Errors, with
the code in code.
| Condition | What you observe | Fix |
|---|---|---|
| No discount with this id in your company | 404 DISCOUNT_NOT_FOUND | Check the id; discounts of other companies are not visible. |
| The assignment does not exist on this discount | 404 DISCOUNT_ASSIGNMENT_NOT_FOUND | Read the discount and use an id from assignments. |
| An assignment names a product, customer, product group or customer group that does not exist in your company | 404 DISCOUNT_PRODUCT_NOT_FOUND / 404 DISCOUNT_CUSTOMER_NOT_FOUND / 404 DISCOUNT_PRODUCT_GROUP_NOT_FOUND / 404 DISCOUNT_CUSTOMER_GROUP_NOT_FOUND | Use an id from your own catalogue or customer list. The applicability reads do not look the ids up: an unknown product or customer belongs to no group, so it matches only an assignment that names no product (or no customer) and no group. |
| No amount in that currency to remove | 404 DISCOUNT_AMOUNT_NOT_FOUND | Read amounts first. |
percentagePct is 0, negative, above 100 or has more than 4 decimals | 400 DISCOUNT_INVALID_PERCENTAGE | Send a value above 0 and at most 100, with at most 4 decimals. |
percentagePct sent for a fixed or per-unit discount | 400 DISCOUNT_PERCENTAGE_NOT_ALLOWED | State amounts per currency instead. |
| Amounts sent for a percentage discount | 400 DISCOUNT_AMOUNT_NOT_ALLOWED | Leave amounts out. |
| An amount of 0 or below, or with more than 12 digits before the decimal point | 400 DISCOUNT_INVALID_AMOUNT | Send a positive amount below 1,000,000,000,000. |
The same currency twice in amounts | 400 DISCOUNT_DUPLICATE_CURRENCY | Send each currency once. |
| A currency LedgerBee does not support | 400 DISCOUNT_UNSUPPORTED_CURRENCY | Use a supported ISO 4217 code. |
A different type on update | 400 DISCOUNT_TYPE_IMMUTABLE | Create a new discount of the other type. |
A per_unit discount with document scope, on create or update | 400 DISCOUNT_PER_UNIT_NEEDS_LINE_SCOPE | Use line scope, or a fixed discount for an amount off the document. |
maxRedemptions or maxPerCustomer below 1 | 400 DISCOUNT_INVALID_LIMIT | Send a whole number of at least 1, or null. |
startsAt after endsAt | 400 DISCOUNT_INVALID_WINDOW | Swap or correct the dates. |
An assignment holding both productId and productGroupId, or both customerId and customerGroupId, after the write | 400 DISCOUNT_ASSIGNMENT_TARGET_CONFLICT (details.pair is product or customer) | Set one of the pair; on PATCH, send null for the other in the same request. |
Booking an invoice that would pass the discount's maxRedemptions | 400 DISCOUNT_REDEMPTION_LIMIT_REACHED | Remove the discount from the invoice or raise the limit. |
Booking an invoice that would pass the customer's maxPerCustomer | 400 DISCOUNT_CUSTOMER_REDEMPTION_LIMIT_REACHED | Remove the discount from the invoice or raise the limit. |
| Deleting a discount a document or redemption records | 409 DISCOUNT_IN_USE | Deactivate it with "active": false. |
| Adding a code, or switching one back on, while another active code holds the same text | 409 DISCOUNT_CODE_TAKEN | Switch the other code off first, or choose another text. |
| A code that is empty, longer than 64 characters or has a space inside | 400 DISCOUNT_CODE_INVALID | details.reason is empty, tooLong or whitespace. |
| A code for a customer the discount's assignments do not cover | 400 DISCOUNT_CODE_CUSTOMER_NOT_ASSIGNED | Assign the discount to the customer first, or leave customerId out. |
| The code does not exist on this discount | 404 DISCOUNT_CODE_NOT_FOUND | List the discount's codes and use an id from there. |
More than 600 uses in a minute from one API key, or from one OAuth grant (one OAuth client for a token issued without a grant): a code lookup uses one, a cart check one per code it names (one when it names none), and a POST /v1/subscriptions, a checkout quote or session, or a checkout vouch that names a discountCode uses one | 429 DISCOUNT_CHECK_RATE_LIMITED | Wait the seconds in Retry-After. |
A cart check names a customerId your company does not have | 404 DISCOUNT_CUSTOMER_NOT_FOUND | Send a customer of your company, or leave customerId out for a guest. |
| Deleting a code a document references or that has been used | 409 DISCOUNT_CODE_IN_USE | Switch it off with "active": false. |
A code in an invoice's, quote's or order confirmation's discountCodes (or carried by a conversion "current") that is unknown, switched off, expired on the document date (for a conversion, the date its chain starts at) or for another customer | 400 DISCOUNT_NOT_APPLICABLE (details.reason, details.code) | Remove the code, or correct the customer or the date. |
Booking an invoice that would pass the maxRedemptions of the code it claimed a discount through | 400 DISCOUNT_CODE_REDEMPTION_LIMIT_REACHED (details.discountCodeId, details.code, details.limit, details.used) | Remove the code from the invoice or raise the code's limit. |
An edit that changes what the discount takes off while live subscriptions reference it, without applyToSubscriptions | 409 DISCOUNT_IN_USE_ON_SUBSCRIPTIONS (details.total, details.subscriptions) | Send applyToSubscriptions: true to move them onto the new terms from their next billing period, or keep the discount as it is. |
| A malformed body field or query parameter, including an id or a date in them | 400 with details.validationErrors | Correct the field named in the error. |
A discountId or assignmentId in the path that is not a UUID | 400 | Send the id as a UUID. |
| The key lacks the scope | 403 INSUFFICIENT_PERMISSIONS | Add the scope the endpoint names: products-read for a read, products-write for a write (it does not include reads). discounts-check reaches only the code lookup and the cart check. |