LedgerBee Developer
  • Getting started
  • Conventions
  • Products
  • Advisor firms
  • Configuration
  • API Reference
Subscriptions
Payment flowCard paymentsProducts & PricingDiscountsProduct Entitlements
Billing documents
Accounting
WebhooksPartner-Hosted Checkout
Customer Portal
Products

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.

typeWhat it takes offFigures it carries
percentagepercentagePct percent, above 0 and at most 100percentagePct; no amounts
fixedAn amount, per eligible line or once per document (see scope)One amount per currency
per_unitAn amount off each unit on the line; line scope onlyOne amount per currency
scopeWhere it applies
lineTo each eligible line on its own.
documentOnce 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.

FieldSetNull, or left out on create
productIdthat productevery product, unless productGroupId is set
productGroupIdevery product in that product groupno group
customerIdthat customerevery customer, unless customerGroupId is set
customerGroupIdevery customer in that customer groupno group
startsAtthe first day it appliesfrom any day
endsAtthe last day it appliesit 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.

OperationEndpointScope
List discountsGET /v1/discountsproducts-read
Get a discountGET /v1/discounts/{discountId}products-read
Create a discountPOST /v1/discountsproducts-write
Update a discountPATCH /v1/discounts/{discountId}products-write
Delete a discountDELETE /v1/discounts/{discountId}products-write
Set an amountPUT /v1/discounts/{discountId}/amounts/{currency}products-write
Remove an amountDELETE /v1/discounts/{discountId}/amounts/{currency}products-write
Add an assignmentPOST /v1/discounts/{discountId}/assignmentsproducts-write
Change an assignmentPATCH /v1/discounts/{discountId}/assignments/{assignmentId}products-write
Remove an assignmentDELETE /v1/discounts/{discountId}/assignments/{assignmentId}products-write
Discounts that applyGET /v1/discounts/applicableproducts-read
Whether one appliesGET /v1/discounts/{discountId}/applicabilityproducts-read
List a discount's codesGET /v1/discounts/{discountId}/codesproducts-read
Add a codePOST /v1/discounts/{discountId}/codesproducts-write
Change a codePATCH /v1/discounts/{discountId}/codes/{codeId}products-write
Delete a codeDELETE /v1/discounts/{discountId}/codes/{codeId}products-write
Look up a codeGET /v1/discounts/codes/{code}discounts-check or products-read
Check a cartPOST /v1/discounts/checkdiscounts-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 productId is the product, or its productGroupId is the product's group, or both are null;
  • its customerId is the customer, or its customerGroupId is the customer's group, or both are null;
  • startsAt is null or on or before the day, and endsAt is 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
{ "applicable": false, "reason": "maxPerCustomerReached", "remaining": 1, "remainingForCustomer": 0 }

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:

reasonMeaning
nullIt applies.
unknownYour company has no discount with this id.
inactiveThe discount has active: false.
codeRequiredThe discount has requiresCode: true, so it applies only through one of its codes.
notAssignedNo assignment names this product and customer, or all of them.
notInGroupAn assignment would match, but the product or the customer is not in the group it targets.
outsideWindowAn assignment matches the product and customer, but not on this day.
maxRedemptionsReachedEvery use under maxRedemptions is taken.
maxPerCustomerReachedThis 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.

FieldMeaning
code1 to 64 characters with no space inside. Matched without regard to case or surrounding spaces; normalizedCode is the upper-cased text a lookup compares.
customerIdThe one customer who may use the code; it must be a customer the discount's assignments cover. Null: any customer the discount applies to.
maxRedemptionsUses allowed through this code, on top of the discount's own limits. Null: no limit of its own.
expiresAtThe last calendar day the code can be used, inclusive (YYYY-MM-DD). Null: it never expires.
activefalse 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.

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/discounts/<discountId>/codes' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "code": "SUMMER25", "maxRedemptions": 500, "expiresAt": "2026-08-31" }'

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.

TerminalCode
curl 'https://api.ledgerbee.com/api/v1/discounts/codes/summer25?customerId=<customerId>&asOf=2026-08-15' \ -H 'x-api-key: <your-key>'
Code
{ "code": { "id": "0199B6D2-3C1A-7A40-9F00-1D2E3F4A5B6C", "code": "SUMMER25", "normalizedCode": "SUMMER25", "active": true, "uses": 12 }, "discount": { "id": "0199B6D2-3C1A-7A40-9F00-1D2E3F4A5B6D", "name": "Summer 25%", "type": "percentage", "scope": "line", "percentagePct": "25.0000", "combinable": true, "amounts": [] }, "applicable": true, "reason": null, "remaining": null, "remainingForCustomer": null, "remainingForCode": 488 }

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:

reasonMeaning
codeUnknownYour company has no code with this text.
inactive … maxPerCustomerReachedThe discount's own reasons, as in Applicability, except codeRequired: a code is how a code-only discount applies.
codeInactiveThe code has active: false.
codeExpiredThe day is after the code's expiresAt.
codeCustomerMismatchThe code is for another customer, or for a customer and none was given.
codeMaxRedemptionsReachedEvery 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: true applies 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.
TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/invoices' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "customerId": "<customerId>", "date": "2026-08-15", "currency": "DKK", "lines": [{ "priceId": "<priceId>", "quantity": 2 }], "discountCodes": ["summer25"] }'

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:

ConversionA code switched off since the quoteA code that expired after the quote's dateA different customer on the payload
discountPricing: "asQuoted"Kept as quotedKept as quotedKept as quoted
discountPricing: "current", or none400 DISCOUNT_NOT_APPLICABLE, codeInactiveApplies400 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.

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/subscriptions' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "customerId": "<customerId>", "planId": "<planId>", "startDate": "2026-11-01", "discountCode": "WELCOME10" }'

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:

FieldMeaning
firstPeriodNetMinorThe 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.
firstPeriodAmountOffMinorWhat the code takes off that invoice, before VAT. Positive; 0 when it touches none of the card's lines.
endsAtYYYY-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, durationInPeriodsAs 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.

TerminalCode
curl 'https://api.ledgerbee.com/api/v1/portal/plans?discountCode=WELCOME10' \ -H 'x-api-key: <your-key>'

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.

FieldMeaning
customerIdThe 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.
currencyThe cart's currency (ISO 4217). A fixed or per-unit discount needs an amount in it.
dateThe calendar day the cart is priced on (YYYY-MM-DD).
lines1 to 200 lines of productId, quantity (negative for a returned item), unitPrice (a decimal string) and an optional vatRatePct.
codesUp to 20 codes the shopper typed, matched like the code lookup.
discountIdsUp 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.

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/discounts/check' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "customerId": "<customerId>", "currency": "DKK", "date": "2026-11-27", "lines": [ { "productId": "<widgetId>", "quantity": 1, "unitPrice": "1000.00", "vatRatePct": 25 }, { "productId": "<gadgetId>", "quantity": 1, "unitPrice": "500.00", "vatRatePct": 25 }, { "productId": "<widgetId>", "quantity": -1, "unitPrice": "200.00", "vatRatePct": 25 } ], "codes": ["order10", "nosuchcode"] }'
Code
{ "lines": [ { "lineIndex": 0, "discounts": [], "originalNet": "1000.00" }, { "lineIndex": 1, "discounts": [], "originalNet": "500.00" }, { "lineIndex": 2, "discounts": [], "originalNet": "-200.00" } ], "documentDiscounts": [ { "discountId": "0199B6D2-3C1A-7A40-9F00-1D2E3F4A5B6E", "code": "ORDER10", "originalReductionAmount": "130.00", "vatSplitAvailable": true, "vatSplit": [{ "vatRatePct": "25.00", "originalReductionAmount": "130.00", "originalVatReductionAmount": "32.50" }] } ], "refusals": [{ "code": "NOSUCHCODE", "discountId": null, "reason": "codeUnknown", "lineIndex": null }], "limits": [ { "discountId": "0199B6D2-3C1A-7A40-9F00-1D2E3F4A5B6E", "code": "ORDER10", "remaining": null, "remainingForCustomer": null, "remainingForCode": null } ] }

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:

reasonMeaning
codeUnknown … codeMaxRedemptionsReachedThe code's own reasons, as for the code lookup.
unknownYour company has no discount with an id in discountIds.
inactive … maxPerCustomerReachedThe 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_CURRENCYA fixed or per-unit discount has no amount in the cart's currency.
DISCOUNT_NOT_COMBINABLEA 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_DOCUMENTThe 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:

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/discounts' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Loyalty 10%", "type": "percentage", "scope": "line", "percentagePct": "10" }'

Assign it to the customer, leaving the product and the dates out:

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/discounts/<discountId>/assignments' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "customerId": "<customerId>" }'

A Black Friday discount of 50 DKK or 8 EUR off each document, for every customer and every product, from 27 to 30 November:

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/discounts' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Black Friday", "type": "fixed", "scope": "document", "amounts": [ { "currency": "DKK", "amount": "50.00" }, { "currency": "EUR", "amount": "8.00" } ] }' curl -X POST 'https://api.ledgerbee.com/api/v1/discounts/<discountId>/assignments' \ -H 'x-api-key: <your-key>' \ -H 'Content-Type: application/json' \ -d '{ "startsAt": "2026-11-27", "endsAt": "2026-11-30" }'

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
{ "discounts": [ { "discountId": "0199A2B4-6C1E-7F00-8D2A-3B4C5D6E7F80", "name": "Welcome 10%", "type": "percentage", "scope": "document", "percentagePct": "10", "amountInCurrency": null, "active": true, "endsAt": "2027-01-01", "targetsNoProduct": false } ] }

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
{ "id": "0199A2B4-6C1E-7F00-8D2A-3B4C5D6E7F80", "percentagePct": "20", "warnings": [], "subscriptionChanges": [ { "customerSubscriptionId": "0199A2B4-9F11-7A20-8C3D-4E5F60718293", "chainHeadId": "0199A2B4-7A10-7B30-9E4F-5A6B7C8D9E0F", "name": "Support plan", "customerName": "Nordlys ApS", "effectiveDate": "2026-11-01", "outcome": "scheduled", "errorCode": null } ] }

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.

ConditionWhat you observeFix
No discount with this id in your company404 DISCOUNT_NOT_FOUNDCheck the id; discounts of other companies are not visible.
The assignment does not exist on this discount404 DISCOUNT_ASSIGNMENT_NOT_FOUNDRead 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 company404 DISCOUNT_PRODUCT_NOT_FOUND / 404 DISCOUNT_CUSTOMER_NOT_FOUND / 404 DISCOUNT_PRODUCT_GROUP_NOT_FOUND / 404 DISCOUNT_CUSTOMER_GROUP_NOT_FOUNDUse 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 remove404 DISCOUNT_AMOUNT_NOT_FOUNDRead amounts first.
percentagePct is 0, negative, above 100 or has more than 4 decimals400 DISCOUNT_INVALID_PERCENTAGESend a value above 0 and at most 100, with at most 4 decimals.
percentagePct sent for a fixed or per-unit discount400 DISCOUNT_PERCENTAGE_NOT_ALLOWEDState amounts per currency instead.
Amounts sent for a percentage discount400 DISCOUNT_AMOUNT_NOT_ALLOWEDLeave amounts out.
An amount of 0 or below, or with more than 12 digits before the decimal point400 DISCOUNT_INVALID_AMOUNTSend a positive amount below 1,000,000,000,000.
The same currency twice in amounts400 DISCOUNT_DUPLICATE_CURRENCYSend each currency once.
A currency LedgerBee does not support400 DISCOUNT_UNSUPPORTED_CURRENCYUse a supported ISO 4217 code.
A different type on update400 DISCOUNT_TYPE_IMMUTABLECreate a new discount of the other type.
A per_unit discount with document scope, on create or update400 DISCOUNT_PER_UNIT_NEEDS_LINE_SCOPEUse line scope, or a fixed discount for an amount off the document.
maxRedemptions or maxPerCustomer below 1400 DISCOUNT_INVALID_LIMITSend a whole number of at least 1, or null.
startsAt after endsAt400 DISCOUNT_INVALID_WINDOWSwap or correct the dates.
An assignment holding both productId and productGroupId, or both customerId and customerGroupId, after the write400 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 maxRedemptions400 DISCOUNT_REDEMPTION_LIMIT_REACHEDRemove the discount from the invoice or raise the limit.
Booking an invoice that would pass the customer's maxPerCustomer400 DISCOUNT_CUSTOMER_REDEMPTION_LIMIT_REACHEDRemove the discount from the invoice or raise the limit.
Deleting a discount a document or redemption records409 DISCOUNT_IN_USEDeactivate it with "active": false.
Adding a code, or switching one back on, while another active code holds the same text409 DISCOUNT_CODE_TAKENSwitch the other code off first, or choose another text.
A code that is empty, longer than 64 characters or has a space inside400 DISCOUNT_CODE_INVALIDdetails.reason is empty, tooLong or whitespace.
A code for a customer the discount's assignments do not cover400 DISCOUNT_CODE_CUSTOMER_NOT_ASSIGNEDAssign the discount to the customer first, or leave customerId out.
The code does not exist on this discount404 DISCOUNT_CODE_NOT_FOUNDList 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 one429 DISCOUNT_CHECK_RATE_LIMITEDWait the seconds in Retry-After.
A cart check names a customerId your company does not have404 DISCOUNT_CUSTOMER_NOT_FOUNDSend a customer of your company, or leave customerId out for a guest.
Deleting a code a document references or that has been used409 DISCOUNT_CODE_IN_USESwitch 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 customer400 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 through400 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 applyToSubscriptions409 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 them400 with details.validationErrorsCorrect the field named in the error.
A discountId or assignmentId in the path that is not a UUID400Send the id as a UUID.
The key lacks the scope403 INSUFFICIENT_PERMISSIONSAdd 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.
Last modified on November 6, 2026
Products & PricingProduct Entitlements
On this page
  • What a discount takes off
  • Redemption limits
  • Assignments
  • Applicability
  • Codes
  • Codes on an invoice
  • Codes on quotes and order confirmations
  • Codes on a subscription
  • Codes on a pricing page
  • Check a cart
  • Worked example
  • Deleting and deactivating
  • Discounts on subscriptions
  • Errors
JSON
JSON
JSON
JSON
JSON