Kaiten

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:

CodeNameKindWhat it does
WELCOME20Welcome offerDiscount (PRICE)20 % off the base price of 3 invoices, on Standard; 200 redemptions at most
PEAK-SEASONPeak seasonBoost (ENTITLEMENT_BOOST)200 more monthly-orders for one billing period, on Premium; 25 redemptions at most
TRACKING-PILOTDelivery tracking pilotFeature grant (FLAG_GRANT)delivery-tracking-enabled answers on for 2 billing periods, on Starter
NEW-LOCATIONNew location bundleBundle (COMPOSITE)10 % off 3 invoices and 2 more delivery-drivers; 100 redemptions at most
SUMMER-FESTIVALSummer festivalDiscount (PRICE)10.00 EUR off one invoice — expired
The console's Vouchers catalog: Welcome offer, Peak season, Delivery tracking pilot, New location bundle and the expired Summer festival, with their kind, status and redemptionsThe console's Vouchers catalog: Welcome offer, Peak season, Delivery tracking pilot, New location bundle and the expired Summer festival, with their kind, status and redemptions

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 typeModifiers
NUMBER, NUMBER_AI_CREDITSET (a value of 0 or more), ADD and MULTIPLY (above 0), UNLIMITED — with modifierValue
BOOLEAN, CONFIGSET 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.

The first step of the New voucher wizard, Kind: Discount, Boost, Feature grant and Bundle, each with a line on what it doesThe first step of the New voucher wizard, Kind: Discount, Boost, Feature grant and Bundle, each with a line on what it does

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-20 redeems WELCOME20. 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 maxRedemptions or expiresAt (422 CreateVoucher.WeakCodeUnbounded). WELCOME20 is nine characters, and carries its maximum of 200; PEAK-SEASON and NEW-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 carry codeHint, its last four characters — the console then shows Code ending in …. To find a voucher by its code, POST /vouchers/lookup takes the code in the body, never in a URL.

Who can redeem it

A voucher restricts its redemptions with:

MemberAn Instance can redeem the voucher when
applicableLicenseIdsit runs one of these License versions — any when empty
applicableAddonIdsit holds one of these add-on versions — any when empty
restrictedCustomerSlugit belongs to this customer
redemptionRules.firstTimeOnlynone of its customer's instances has paid an invoice yet
redemptionRules.annualOnlyits live subscription is billed yearly
redemptionRules.minimumSubscriptionAmountits live subscription's base price is at least this amount, in this currency
startsAt, expiresAtthe redemption falls between the two
maxRedemptionsfewer 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

StatusMeaningWhat can change
DRAFTCreated, not redeemable yetEverything but its kind, with PUT /vouchers/{voucherId}
ACTIVEPublished and redeemableIts name, description, expiresAt, and maxRedemptions — never below the redemptions already made. Any other change answers 409 UpdateVoucher.NotEditable
EXPIREDIts expiresAt has passed — refused from that instant, and marked so by the next lifecycle pass (VOUCHER_EXPIRED)Nothing
EXHAUSTEDThe redemption that reached maxRedemptions made it so (VOUCHER_EXHAUSTED)Nothing
ARCHIVEDWithdrawn by POST /vouchers/{voucherId}/archive: no more redemptions, while those already made keep applying until revokedNothing

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.

The New location bundle voucher, NEW-LOCATION, active and redeemed 0 of 100 times, behind its confirmation dialog: Delete New location bundle?, which says no instance has redeemed it, that it is deleted with its offer and its code can be given to another voucher, and that a voucher redeemed at least once cannot be deleted and should be archived instead; Cancel and Delete.The New location bundle voucher, NEW-LOCATION, active and redeemed 0 of 100 times, behind its confirmation dialog: Delete New location bundle?, which says no instance has redeemed it, that it is deleted with its offer and its code can be given to another voucher, and that a voucher redeemed at least once cannot be deleted and should be archived instead; Cancel and Delete.

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.

The Welcome offer voucher, WELCOME20: what it does in plain sentences, its details, and its redemptions, with Demo Restaurant's this morningThe Welcome offer voucher, WELCOME20: what it does in plain sentences, its details, and its redemptions, with Demo Restaurant's this morning

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

OperationRouteScope
List, create vouchersGET / POST /vouchersread:vouchers / write:vouchers
Read, replace a voucherGET / PUT /vouchers/{voucherId}read:vouchers / write:vouchers
Publish, archivePOST /vouchers/{voucherId}/publish, /archivewrite:vouchers
Delete a voucher never redeemedDELETE /vouchers/{voucherId}write:vouchers
Find a voucher by its codePOST /vouchers/lookupread:vouchers
A voucher's redemptionsGET /vouchers/{voucherId}/redemptionsread:vouchers
Validate a codePOST /vouchers/validateread:voucher_redemptions
Redeem a code for an instancePOST /instances/{instanceSlug}/vouchers/redeemwrite:voucher_redemptions
An instance's redemptionsGET /instances/{instanceSlug}/vouchersread:voucher_redemptions
Revoke a redemptionPOST /instances/{instanceSlug}/vouchers/{instanceVoucherId}/revokewrite:vouchers
Validate, redeem from the portalPOST /public/session/vouchers/validate, /redeema 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.

On this page