Monetization
Vouchers
Codes that act on a price, a grant, or a feature flag — the four kinds, how codes and eligibility work, a voucher's statuses, redemption by your backend or from the portal, revocation, and how a discount reaches Stripe.
A voucher is a code an Instance redeems. Depending on its kind, it takes something off the invoices of the Instance's subscription, raises what the Instance is entitled to for a while, turns a feature flag variant on for it for a while, or does several of these at once. Each redemption is recorded against the Instance, applies for its own duration, and can be revoked.
Once Kaiten Sushi Shop sells its Licenses through Kaiten, it runs five vouchers. The demo seed writes all five; their redemptions come from subscriptions, which it does not create:
| Code | Name | Kind | What it does |
|---|---|---|---|
WELCOME20 | Welcome offer | Discount (PRICE) | 20 % off the base price of 3 invoices, on Standard; 200 redemptions at most |
PEAK-SEASON | Peak season | Boost (ENTITLEMENT_BOOST) | 200 more monthly-orders for one billing period, on Premium; 25 redemptions at most |
TRACKING-PILOT | Delivery tracking pilot | Feature grant (FLAG_GRANT) | delivery-tracking-enabled answers on for 2 billing periods, on Starter |
NEW-LOCATION | New location bundle | Bundle (COMPOSITE) | 10 % off 3 invoices and 2 more delivery-drivers; 100 redemptions at most |
SUMMER-FESTIVAL | Summer festival | Discount (PRICE) | 10.00 EUR off one invoice — expired |
Vouchers are part of native billing: their routes answer where billing is enabled for your organization (billing error families). Examples use $KAITEN_API and $KAITEN_TOKEN as set up in the quickstart; responses are illustrative.
The four kinds
The console names the kinds Discount, Boost, Feature grant, and Bundle; the API, voucherType. Every voucher has a duration — ONE_TIME, REPEATING with durationInPeriods, or FOREVER — which a discount counts in invoices and the other kinds in billing periods.
Discount — PRICE
A percentage in (0, 100], or a fixed amount in minor units with its currency (priceDiscountType PERCENTAGE or FIXED_AMOUNT, priceDiscountValue). priceAppliesTo says what it takes off: the License's base price (LICENSE_BASE), the add-ons (ADDONS), both (BOTH), or chosen prices (SELECTED_PRICES, with applicableLicensePriceIds and applicableAddonPriceIds). It discounts one invoice (ONE_TIME), the next durationInPeriods invoices (REPEATING), or every invoice (FOREVER), and appears on each as a discount line allocated to the lines it reduces. A fixed amount is refused on a subscription billed in another currency (CURRENCY_MISMATCH).
Boost — ENTITLEMENT_BOOST
grants change entitlements of the redeeming Instance, one grant per entitlement:
| Entitlement type | Modifiers |
|---|---|
NUMBER, NUMBER_AI_CREDIT | SET (a value of 0 or more), ADD and MULTIPLY (above 0), UNLIMITED — with modifierValue |
BOOLEAN, CONFIG | SET only, with value: true or false, or the configuration object |
Boosts apply after the License and the add-ons: on a number, the most recently redeemed SET replaces the value, ADDs are summed onto it, and MULTIPLYs multiply it. A SET can therefore lower a value; the console warns when one sets less than a License grants. A boost changes only an entitlement the Instance already holds — it never grants one — so a boost whose entitlements the Instance holds none of is refused as NOT_ELIGIBLE (NOTHING_TO_BOOST). It applies from the next read or report, for its duration in the subscription's billing periods counted from the redemption — a calendar month per period when the Instance has no live subscription — and without end for FOREVER.
Feature grant — FLAG_GRANT
flagGrants names a variant of one or more feature flags, one grant per flag. For the duration, counted like a boost's, each flag answers that variant for the redeeming Instance, whatever its targeting rules say: an OFREP evaluation whose subject is that Instance returns the variant with the reason VOUCHER_GRANT, and the flag's metadata carries voucherId and redemptionId. The subject is the instance the evaluation context names with kaiten.instanceSlug, else the oldest instance of the customer the targeting key names. A disabled flag stays disabled: the kill switch wins over a grant. A grant whose variant the flag no longer defines is skipped, and the rules apply.
TRACKING-PILOT, redeemed on Ninja Osaka's Starter instance, turns delivery tracking on although Starter grants delivery-tracking: false:
curl -X POST "$KAITEN_API/ofrep/v1/evaluate/flags/delivery-tracking-enabled" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "context": { "targetingKey": "ninja-osaka", "kaiten": { "instanceSlug": "ninja-osaka-prod" } } }'{
"key": "delivery-tracking-enabled",
"variant": "on",
"value": true,
"reason": "VOUCHER_GRANT",
"metadata": { "voucherId": "3f2a…", "redemptionId": "9b1c…" }
}Bundle — COMPOSITE
At least two of a discount, boosts, and feature grants, each checked as its own kind is. Its discount counts invoices from the next invoice; its boosts and feature grants count billing periods from the redemption. NEW-LOCATION takes 10 % off three invoices and adds two delivery drivers for three billing periods.
Create and publish a voucher
A voucher is created as a draft, then published. In the console, Catalog → Vouchers → New voucher is a wizard — kind, offer, eligibility, review — whose last step reads the voucher in plain sentences and publishes it. With the API, WELCOME20:
curl -X POST "$KAITEN_API/vouchers" \
-H "Authorization: Bearer $KAITEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "WELCOME20",
"name": "Welcome offer",
"description": "20 % off the first three months of Standard.",
"voucherType": "PRICE",
"priceDiscountType": "PERCENTAGE",
"priceDiscountValue": "20",
"priceAppliesTo": "LICENSE_BASE",
"duration": "REPEATING",
"durationInPeriods": 3,
"maxRedemptions": 200,
"applicableLicenseIds": ["<id of standard>", "<id of standard-v2>"]
}'{
"id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
"code": "WELCOME20",
"codeHint": "ME20",
"name": "Welcome offer",
"voucherType": "PRICE",
"status": "DRAFT",
"duration": "REPEATING",
"durationInPeriods": 3,
"maxRedemptions": 200,
"redemptionsCount": 0,
"priceDiscountType": "PERCENTAGE",
"priceDiscountValue": "20",
"priceAppliesTo": "LICENSE_BASE",
"…": "…"
}201 Created and VOUCHER_CREATED, whose payload carries the hint, never the code. The License version ids come from GET /license-families/standard?include=versions. POST /vouchers/{voucherId}/publish makes it redeemable (VOUCHER_PUBLISHED).
Codes
- Yours or generated. A code you choose is 8 to 64 characters among letters, digits,
_and-. Without one, Kaiten generates 16 characters from an alphabet without look-alike letters. - Matched without case or separators.
welcome-20redeemsWELCOME20. Two vouchers of an organization cannot share a code in that form (409 CreateVoucher.CodeConflict). - A short code needs a bound. A code under 12 characters, separators left out, can be guessed: it must come with
maxRedemptionsorexpiresAt(422 CreateVoucher.WeakCodeUnbounded).WELCOME20is nine characters, and carries its maximum of 200;PEAK-SEASONandNEW-LOCATION, ten and eleven once their hyphen is left out, carry 25 and 100. - The code is a secret. The voucher reads, which take
read:vouchers, return it. A validation answers the voucher without it, and redemptions, events, and the portal carrycodeHint, its last four characters — the console then shows Code ending in …. To find a voucher by its code,POST /vouchers/lookuptakes the code in the body, never in a URL.
Who can redeem it
A voucher restricts its redemptions with:
| Member | An Instance can redeem the voucher when |
|---|---|
applicableLicenseIds | it runs one of these License versions — any when empty |
applicableAddonIds | it holds one of these add-on versions — any when empty |
restrictedCustomerSlug | it belongs to this customer |
redemptionRules.firstTimeOnly | none of its customer's instances has paid an invoice yet |
redemptionRules.annualOnly | its live subscription is billed yearly |
redemptionRules.minimumSubscriptionAmount | its live subscription's base price is at least this amount, in this currency |
startsAt, expiresAt | the redemption falls between the two |
maxRedemptions | fewer redemptions than this have been made |
An Instance redeems a voucher once: a second redemption answers 409 .AlreadyRedeemed. POST /vouchers/validate runs every check without redeeming — the voucher's own, and with instanceSlug those of the Instance, or with licensePriceId those of the subscription that price would start — and answers valid with the first failing reason (NOT_FOUND, NOT_ACTIVE, NOT_YET_VALID, EXPIRED, EXHAUSTED, ALREADY_REDEEMED, NOT_ELIGIBLE with its rule, CURRENCY_MISMATCH, DISCOUNT_LIMIT_EXCEEDED). It allows 60 checks a minute per caller, then answers 429 with Retry-After.
Statuses
| Status | Meaning | What can change |
|---|---|---|
DRAFT | Created, not redeemable yet | Everything but its kind, with PUT /vouchers/{voucherId} |
ACTIVE | Published and redeemable | Its name, description, expiresAt, and maxRedemptions — never below the redemptions already made. Any other change answers 409 UpdateVoucher.NotEditable |
EXPIRED | Its expiresAt has passed — refused from that instant, and marked so by the next lifecycle pass (VOUCHER_EXPIRED) | Nothing |
EXHAUSTED | The redemption that reached maxRedemptions made it so (VOUCHER_EXHAUSTED) | Nothing |
ARCHIVED | Withdrawn by POST /vouchers/{voucherId}/archive: no more redemptions, while those already made keep applying until revoked | Nothing |
Delete a voucher
A voucher no Instance ever redeemed can be deleted, whatever its status: DELETE /vouchers/{voucherId} answers 204 No Content and emits VOUCHER_DELETED, whose payload is the voucher's last state, without its code. Its grants and feature grants go with it, so the entitlements and flags it named can be deleted again, and its code can be given to another voucher. In the console, the voucher's page offers Delete while no Instance has redeemed it.
A voucher redeemed once stays, since the invoices it discounted name it: the deletion answers 409 DeleteVoucher.Redeemed, even after the redemption is revoked or the Instance is deleted. Archive it instead. The same 409 answers while a checkout that holds the voucher waits for its payment; once that checkout expires unpaid, the voucher can be deleted.
Redeem a voucher
From your backend. POST /instances/{instanceSlug}/vouchers/redeem with { "code": "…" } redeems a code for an Instance and answers 201 with the redemption. It takes write:voucher_redemptions, a scope apart from the one that creates vouchers, so a backend can redeem codes for its customers without being able to issue them. A discount applies to the subscription's next invoices; a boost or a feature grant, from the next read or evaluation. A subscription started with voucherCode (POST /instances/{instanceSlug}/billing) redeems it once its add-ons are attached, and a discount then applies from the activation invoice: Demo Restaurant subscribed to Standard monthly with WELCOME20, and its activation invoice reads 49.00 − 9.80 = 39.20 EUR.
From the portal. Your customer enters a code through a customer session bound to an Instance whose subscription is in trial, active, or past due — POST /public/session/vouchers/redeem, or POST /public/session/vouchers/validate to check it first. The voucher applies to the existing subscription: a discount from the next invoice composed (appliesFromInvoice: "next"), never to one already issued; a boost at once. Every refusal of the code answers the same 422 RedeemSessionVoucher.VoucherInvalid, so the portal never says why a code is refused, and attempts are limited per session and per customer.
A redemption reads back with GET /instances/{instanceSlug}/vouchers or GET /vouchers/{voucherId}/redemptions: its window (effectiveStartsAt, effectiveExpiresAt), the invoices a discount has used (applicationsCount of applicationsMax), and its status — ACTIVE, EXPIRED once a discount has used its last invoice or a boost's window has ended, or REVOKED.
Revoke a redemption
POST /instances/{instanceSlug}/vouchers/{instanceVoucherId}/revoke with a reason of 1 to 500 characters stops an active redemption: a boost or a feature grant ends at once, a discount applies to no further invoice, and invoices already issued are untouched. The redemption stays readable with its reason, and INSTANCE_VOUCHER_REVOKED is emitted. Revoking takes write:vouchers. Revoking a redemption that is no longer active answers 409 RevokeInstanceVoucher.NotActive.
Discounts on Stripe
On a subscription that Stripe collects, each discount reaches Stripe as a one-off coupon of a fixed amount, attached to the invoice item it reduces, so that Stripe Tax — when it is on — taxes the discounted amount. These coupons belong to one invoice: Kaiten deletes them once the invoice is reconciled, and Stripe never holds a copy of your vouchers. A coupon added to a Kaiten invoice from Stripe's dashboard is reported as a mismatch.
A Stripe invoice item takes at most 20 discounts. Kaiten applies that limit whatever the payment provider: a redemption that would put a 21st discount on a line of the Instance's invoices is refused with 422 RedeemVoucher.DiscountLimitExceeded, and validation answers DISCOUNT_LIMIT_EXCEEDED.
Reference
| Operation | Route | Scope |
|---|---|---|
| List, create vouchers | GET / POST /vouchers | read:vouchers / write:vouchers |
| Read, replace a voucher | GET / PUT /vouchers/{voucherId} | read:vouchers / write:vouchers |
| Publish, archive | POST /vouchers/{voucherId}/publish, /archive | write:vouchers |
| Delete a voucher never redeemed | DELETE /vouchers/{voucherId} | write:vouchers |
| Find a voucher by its code | POST /vouchers/lookup | read:vouchers |
| A voucher's redemptions | GET /vouchers/{voucherId}/redemptions | read:vouchers |
| Validate a code | POST /vouchers/validate | read:voucher_redemptions |
| Redeem a code for an instance | POST /instances/{instanceSlug}/vouchers/redeem | write:voucher_redemptions |
| An instance's redemptions | GET /instances/{instanceSlug}/vouchers | read:voucher_redemptions |
| Revoke a redemption | POST /instances/{instanceSlug}/vouchers/{instanceVoucherId}/revoke | write:vouchers |
| Validate, redeem from the portal | POST /public/session/vouchers/validate, /redeem | a customer session |
Events: VOUCHER_CREATED, VOUCHER_UPDATED, VOUCHER_PUBLISHED, VOUCHER_ARCHIVED, VOUCHER_DELETED, VOUCHER_EXHAUSTED, VOUCHER_EXPIRED for the voucher, and INSTANCE_VOUCHER_REDEEMED, INSTANCE_VOUCHER_REVOKED, INSTANCE_VOUCHER_EXPIRED for each redemption — no payload carries a code. Payloads are in the event catalog; request and response shapes in the API reference.
Where to go next
The layers a boost sits between: add-ons, before it, and entitlement exceptions — your own dated decision on one Instance, without a code — after it. How a discount line is composed: Invoices. Exact reference: the data model.
Add-ons
Extensions sold on top of a License and held by an Instance in a quantity — families and versions, what one unit grants and how it composes with the License, prices and compatibility, and how a change of quantity is billed.
Self-Serve Checkout
Selling from your own pages — the three public credentials (publishable key, checkout session, customer session), the anonymous checkout end to end from the Turnstile check to the completion, and what you see and receive once a visitor has bought.