BillKit/DocsConsole
Core concepts

Products and prices

A product is what you sell, a price is how much and how often, and a checkout always names a price.

Updated Sep 26, 2026·OpenAPI v1.0.0

Product or price

A product carries the name and description a customer sees on the payment page, the invoice and the portal. It has no amount of its own.

A price pins an amount, a currency and an interval to one product, along with the trial, the payment methods, the refund policy and the tax treatment. It is what a customer buys: checkout sessions, subscriptions and plan changes all name a price_…, never a product.

Use one product per offering and several prices under it: monthly and yearly, or one per currency.

Create a product

shell
curl https://api.billkit.eu/v1/products \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "name": "Pro plan",
  "description": "For teams that ship every week.",
  "marketing_features": ["Unlimited projects", "Priority support"]
}'
Response · 200 OK
{
"id": "prod_kQ2v8Zr4nT1yLpXaBc",
"object": "product",
"created": 1789344000,
"livemode": false,
"name": "Pro plan",
"description": "For teams that ship every week.",
"prices": null,
"stats": null,
"marketing_features": ["Unlimited projects", "Priority support"],
"metadata": {},
"active": true,
"allow_promotion_codes": false,
"default_price_id": null,
"default_price": null,
"archived_at": null
}
FieldNotes
namerequiredUp to 255 characters. Shown to the customer.
descriptionUp to 2,048 characters.
marketing_featuresUp to 24 strings, listed on the portal’s plan switcher.
metadataUp to 50 keys of 40 characters, values up to 500. Never read by BillKit.
allow_promotion_codesLets a buyer type a coupon code in the embedded checkout, and a customer type one in the billing portal when its coupons capability is on. Defaults to false. See Discounts with coupons.

prices, stats and default_price are null unless you ask for them with ?expand=prices,stats,default_price. See Expanding responses. default_price_id stays null until the product has a price.

Create a price

shell
curl https://api.billkit.eu/v1/prices \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "product_id": "prod_kQ2v8Zr4nT1yLpXaBc",
  "amount_cents": 29000,
  "currency": "EUR",
  "interval": "year",
  "trial_days": 14,
  "tax_behavior": "exclusive",
  "payment_methods": ["creditcard", "ideal"],
  "metadata": {"plan": "pro-yearly"}
}'
Response · 200 OK
{
"id": "price_7Hd3Wm9pQx2vRt5Kna",
"object": "price",
"created": 1789344060,
"livemode": false,
"product_id": "prod_kQ2v8Zr4nT1yLpXaBc",
"product_name": "Pro plan",
"amount_cents": 29000,
"unit_amount_decimal": "29000",
"currency": "EUR",
"interval": "year",
"active": true,
"metadata": {"plan": "pro-yearly"},
"trial_days": 14,
"trial_verification_cents": 100,
"payment_methods": ["creditcard", "ideal"],
"refund_on_cancel": "none",
"refund_window_initial_days": null,
"refund_window_renewal_days": null,
"tax_behavior": "exclusive",
"usage_type": "licensed",
"billing_scheme": "per_unit",
"tiers_mode": null,
"tiers": null
}

The product must exist and be active. Every field below is optional unless marked.

FieldNotes
product_idrequiredThe prod_… this price sells.
amount_centsWhat is charged each period, as a positive integer in the currency’s minor unit: 29000 is €290.00. Required on a flat price.
currencyISO 4217, defaults to EUR. It must be enabled on your account; GET /v1/tenant/capabilities lists yours in allowed_currencies, and anything else is a 400.
intervalrequiredmonth or year. There is no interval count, so quarterly or weekly billing cannot be expressed.
trial_days0 to 90, default 0. See Free trials.
trial_verification_centsWhat a trial or metered checkout charges to create the mandate. Default 100.
payment_methodsOffered at checkout. Any of creditcard, ideal, eps, applepay, paypal, directdebit; default ["creditcard"]. At least one must be able to take a first payment, so directdebit alone is a 400.
tax_behaviorinclusive (VAT is inside amount_cents), exclusive (VAT is added on top) or unspecified, the default, which defers to the tax rate for the buyer’s country. See Charge and record EU VAT.
refund_on_cancelnone (default), full or prorated. See Refunds.
refund_window_initial_days, refund_window_renewal_days0 to 365. null uses the default window, 0 disables refunds for that kind of charge.
usage_typelicensed (default) charges amount_cents every period. metered makes it a per-unit rate billed in arrears from usage records.
unit_amount_decimal, billing_scheme, tiers_mode, tiersSub-cent and tiered rates, metered prices only. See Metered billing.
metadataSame limits as on a product.

There are no nicknames or lookup keys. Keep your own mapping from plan names to price_… ids, or put a key in metadata and filter on it after listing.

unit_amount_decimal is returned on every price as a string, even when you only sent amount_cents, so a client can read that one field for the rate.

The default price

A product’s default_price_id names the price the billing portal offers on that price’s interval. A price created while its product has no default becomes the default, and creating it also sends product.updated. That is the first price under a product, and also the next price you create after clearing the default or archiving the default price. A price created while a default is set does not change it.

To raise a price, create the new price and point the default at it with POST /v1/products/{id}:

shell
curl https://api.billkit.eu/v1/products/prod_kQ2v8Zr4nT1yLpXaBc \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"default_price_id": "price_Nt8Ql3wZ7bR2vMxKye"}'

The default must be an active price of the same product. Anything else, including another product’s price or an archived one, is a 400 on default_price_id. Send null to clear it; the portal then offers the newest active price on each interval, until the next price you create becomes the default. Customers already on the old price keep renewing at it, and the portal still shows it to them as their current plan.

What can change after creation

The fields that decide what a past charge was are fixed: amount_cents, unit_amount_decimal, currency, interval, usage_type, billing_scheme, the tier table, trial_days and trial_verification_cents. Subscriptions renew against a price by id, so editing any of these would re-price customers already on it. To charge something different, create a new price.

POST /v1/prices/{id} accepts the forward-looking fields only:

FieldNotes
activefalse archives the price, true puts it back on sale.
payment_methodsRead when a checkout opens.
metadataReplaced as a whole. Send {} to empty it.
refund_on_cancel, refund_window_initial_days, refund_window_renewal_daysRead when a cancellation or refund happens, so a change covers customers already on the price. Send a window as null to go back to the default.
tax_behaviorinclusive or exclusive, and only while the price is still unspecified. Once set it cannot change; trying is a 400.
shell
curl https://api.billkit.eu/v1/prices/price_7Hd3Wm9pQx2vRt5Kna \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"refund_on_cancel": "prorated", "refund_window_renewal_days": 14}'

Omitted fields are left alone. Only the two refund windows accept null; on any other field it is refused with a 400 naming the field, because none of them can be emptied. A field not listed here, amount_cents included, is refused with 400 parameter_unknown naming it:

shell
curl https://api.billkit.eu/v1/prices/price_7Hd3Wm9pQx2vRt5Kna \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 3900}'

POST /v1/products/{id} is less strict, because a product holds no money: name, description, marketing_features, metadata, allow_promotion_codes, default_price_id and active can all change. marketing_features and metadata are replaced, not merged. null clears description, marketing_features and default_price_id. null on name, metadata, allow_promotion_codes or active is a 400 naming the field; send metadata: {} to remove all keys.

Archive, never delete

There is no DELETE for products or prices. Subscriptions and invoices point at them by id, so they stay readable for good. To stop selling one, send active: false.

shell
curl https://api.billkit.eu/v1/prices/price_7Hd3Wm9pQx2vRt5Kna \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'

An archived price is refused for anything new. A checkout session against it returns 404 resource_missing, as if the price did not exist, and it is no longer offered as a plan change:

shell
curl https://api.billkit.eu/v1/checkout/sessions \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "customer_email": "grace@example.com",
  "price_id": "price_7Hd3Wm9pQx2vRt5Kna",
  "success_url": "https://example.com/welcome",
  "cancel_url": "https://example.com/pricing"
}'

Archiving a product’s default price also clears its default_price_id and sends product.updated. The portal then offers the newest active price on that interval, until you set a new default or create another price, which becomes it.

Customers already subscribed keep renewing at the archived price until they cancel or change plan. GET /v1/prices/{id} and listings still return it, with active: false. Send active: true to put it back on sale.

Archiving a product works the same way one level up. It stamps archived_at, and new checkouts against any of its prices fail with a 400 on price_id. No new prices can be created under it. Its prices keep their own active: true, so archive them too if your pricing page reads that field.

ChangeEvents
Createdproduct.created, price.created. A price created while the product has no default, such as the first one, also sends product.updated, for its new default_price_id.
Archivedproduct.archived, price.archived. Archiving a product’s default price also sends product.updated, for its cleared default_price_id.
Anything elseproduct.updated, price.updated. Un-archiving is an update. A price update that changes nothing emits no event.

Listing

Both lists are newest first and paginated with limit (1 to 100, default 10) and starting_after. Archived rows are included, so filter on active before rendering a pricing page.

shell
curl "https://api.billkit.eu/v1/prices?product_id=prod_kQ2v8Zr4nT1yLpXaBc&limit=100" \
-H "Authorization: Bearer $BILLKIT_API_KEY"

product_id is the only filter on prices. Use it rather than filtering one page yourself, which misses everything past the limit. Products have no filters, but GET /v1/products?expand=prices returns each product with all its prices, active ones first, in one call.

Using a price

Pass the price id as price_id when you create a checkout session. That session’s first charge, its trial and the payment methods on offer all come from the price. See the Quickstart for the full flow, and Subscriptions for moving a customer to a different price.

A plan change can only move between prices in the same currency, and never to or from a metered price. Keep a monthly and a yearly price per currency if you want customers to switch between them.