Public plan catalog
curl --request GET \
--url https://api.nozle.app/engine/api/v1/plans \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/engine/api/v1/plans"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/engine/api/v1/plans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/engine/api/v1/plans"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/engine/api/v1/plans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/engine/api/v1/plans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.get("https://api.nozle.app/engine/api/v1/plans")
.header("Authorization", "Bearer <token>")
.asString();Public plan catalog
What a publishable key can read from the plan catalog, and which fields a browser key does not receive.
GET
/
engine
/
api
/
v1
/
plans
Public plan catalog
curl --request GET \
--url https://api.nozle.app/engine/api/v1/plans \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/engine/api/v1/plans"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/engine/api/v1/plans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/engine/api/v1/plans"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/engine/api/v1/plans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/engine/api/v1/plans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.get("https://api.nozle.app/engine/api/v1/plans")
.header("Authorization", "Bearer <token>")
.asString();Base URL:
See Authentication for the full credential model.
The Engine catalog answers CORS from any origin, so a browser on your own site can call it with the publishable key.
Only a secret key receives: Charge (
Fixed charge (
Only in Minimum commitment (
Fields: Usage threshold (
Fields: Tax (
Fields: Entitlement (
Only a secret key receives: Credit grant rule (
Only a secret key receives: Credit pool policy (
Fields:
In TypeScript, read an undeclared field by widening the type yourself. See
https://api.nozle.app/engine/api/v1.
The plan catalog is the one part of the API a publishable pk_ key can call. Use it to build pricing pages and plan selectors.
Auth: pk_ (publishable key) or sk_ (secret key). A pk_ key receives a trimmed plan; an sk_ key receives the whole plan.
A publishable key ships in browsers, so everything it receives is readable by anyone who has the key: prices, usage charges and their rates, taxes, minimum commitments, entitlements, credit grants and
metadata. Keep negotiated or internal pricing out of the organization whose publishable key you ship. See Keep sensitive pricing out of the catalog.What a publishable key can call
| Host | Request | With pk_ |
|---|---|---|
Engine https://api.nozle.app/engine/api/v1 | GET /plans | Allowed. Same body as Core. |
Core https://api.nozle.app/core/api/v1 | GET /plans | Allowed. |
| Either host | GET /plans/{code} | Allowed. One plan, wrapped as { "plan": { ... } }. |
| Either host | Any other path or method | 403. Core returns {"status": 403, "error": "Publishable keys may only read the public plan catalog", "code": "publishable_key_not_allowed"}. Engine returns 403 with an error message. |
GET /plans, GET /plans/{code} and their preflight return Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET, OPTIONS, never Access-Control-Allow-Credentials, so do not send cookies. Every other Engine route keeps its origin allow-list.
Engine forwards the request, with its query string, to Core and returns Core’s status code and body unchanged, including Core’s errors. If Engine cannot reach Core within 10 seconds, it returns 502 with {"code": "billing_unavailable", "error": "failed to reach billing service"}.
Which plans are returned
All top-level plans of the organization, newest first. There is no listed, public or draft flag, so trial plans, pay-in-arrears plans and free plans are all included. Per-customer override plans (plans with aparent_id) and deleted plans are not.
- A
pk_key does not receive plans that are pending deletion, andfamilies[].plan_codesleaves their codes out.GET /plans/{code}returns404with"code": "plan_not_found"for one. - An
sk_key receives them, withpending_deletion: true. - A
pk_key, or ansk_key withinclude_hidden=false, does not receive plans whose plan family is hidden from the pricing table in the list. Hiding a family is a listing setting only:GET /plans/{code}still returns a plan of a hidden family, or of a deleted family, to apk_key, so the SDK can describe a subscriber’s own plan. include_hidden(secret keys only): sendinclude_hidden=falseto get the same pricing table view as apk_key. Any other value, or none, returns every plan.- Every plan, listed or read by code, carries all its entitlements and charges, including those of features hidden from the pricing table. Only the pricing table rows (
families[].features) leave hidden features out.
POST /plans, the dashboard onboarding or the Agent Kit starts in a new plan family that is shown on the pricing table, so it is listed at once. Send show_on_pricing: false in plan (GraphQL showOnPricing: false) to create it hidden. A plan that joins an existing family with plan_family_code keeps that family’s visibility.
Plan families
A plan family groups the billing intervals of one offer, for example “Pro” monthly and “Pro” yearly. Every plan hasplan_family_code, and the list returns a families array with the display settings (show_on_pricing, pricing_position, pricing_highlighted, badge_text, cta_text, plan_codes and the feature rows) for the families of the returned plans.
POST /plansandPUT /plans/{code}acceptplan_family_code. A code that does not exist yet creates the family; without a code the plan gets its own family.- A family holds at most one plan per
interval, all in one currency. Otherwise the call returns422withinterval: plan_family_interval_already_existoramount_currency: plan_family_currency_mismatch. - A plan that joins a family with other plans takes the family’s features and privileges.
POST,PATCHandDELETE /plans/{code}/entitlements(and.../privileges/{code}) change the features and privileges of every plan in the family.usage_limitandhard_limitapply only to the plan in the path, because usage is counted per billing period.
Pagination
Core acceptspage and per_page (default and page size 100) and returns meta. Engine forwards page and per_page to Core, so both hosts page the same way.
meta field | Type | Description |
|---|---|---|
current_page | integer | Page returned. |
next_page | integer or null | Next page, null on the last page. |
prev_page | integer or null | Previous page, null on the first page. |
total_pages | integer | Number of pages. |
total_count | integer | Number of plans in the catalog. |
sdk_appearance | object | The appearance saved on the dashboard page Developers > Customize SDK, which the React components apply automatically. {} when none is saved. Returned on this list endpoint only, not on GET /plans/{code}. |
nozle_branding | boolean | Whether the SDK components show “Powered by Nozle”. Publishable keys only. |
Example
What apk_ key receives. The example shows one entry of each array. Optional fields that this plan does not use are left out, and the charge shows only its main fields; every field is listed under Fields.
{
"plans": [
{
"nozle_id": "7d6f0c52-3f1e-4c1a-9a53-1c2d9a0e1001",
"name": "Pro",
"code": "pro",
"interval": "monthly",
"description": "For growing teams",
"amount_cents": 1499,
"amount_currency": "USD",
"trial_period": null,
"pay_in_advance": true,
"charges": [
{
"nozle_id": "7d6f0c52-3f1e-4c1a-9a53-1c2d9a0e1002",
"code": "api_calls",
"feature_code": "api_calls",
"charge_model": "graduated",
"properties": {
"graduated_ranges": [
{ "from_value": 0, "to_value": 1000, "per_unit_amount": "0", "flat_amount": "0" },
{ "from_value": 1001, "to_value": null, "per_unit_amount": "0.01", "flat_amount": "0" }
]
}
}
],
"entitlements": [
{
"feature_type": "metered",
"code": "api_calls",
"name": "API calls",
"description": "Calls per month",
"usage_limit": 100000,
"privileges": []
}
],
"taxes": [],
"credit_grant_rules": [
{
"nozle_id": "7d6f0c52-3f1e-4c1a-9a53-1c2d9a0e1003",
"plan_code": "pro",
"credit_system_code": "ai_credits",
"amount": "500.0",
"interval": "month",
"interval_count": 1,
"status": "active"
}
],
"metadata": { "product": "acme" }
}
],
"meta": { "current_page": 1, "next_page": null, "prev_page": null, "total_pages": 1, "total_count": 1 }
}
Fields
Identifiers are namednozle_id. Amounts named *_cents are integers in cents of the plan currency. Amounts inside a charge’s properties are decimal strings in currency units (for example "14.99" is 14.99 USD), not cents.
Plan
| Field | Type | Description |
|---|---|---|
nozle_id | string (UUID) | Plan identifier. |
code | string | Unique plan code. Use it to start checkout. |
name | string | Plan name. |
description | string or null | Plan description. |
plan_family_code | string or null | Code of the plan family. See Plan families. |
interval | string | weekly, monthly, quarterly, semiannual or yearly. |
amount_cents | integer | Base price per interval, before tax, in cents of amount_currency. 0 for a free or pure usage-based plan. Usage prices are in charges. |
amount_currency | string | ISO 4217 currency code, such as USD. |
trial_period | number or null | Free trial length in days. |
pay_in_advance | boolean | true bills the base price at the start of each period, false at the end. |
bill_charges_monthly | boolean or null | true invoices usage charges monthly on a yearly or semiannual plan. |
bill_fixed_charges_monthly | boolean or null | Same, for fixed charges. |
charges | array | Usage charges. Empty when there are none. |
fixed_charges | array | Fixed charges. Only in GET /plans/{code}. |
minimum_commitment | object | Present only when the plan has one. |
usage_thresholds | array | Thresholds for progressive billing. |
taxes | array | Taxes applied to the plan. |
entitlements | array | Feature entitlements. |
credit_grant_rules | array | Active credit grant rules. |
credit_pool_policies | array | Active credit pool policies. |
metadata | object | Key-value pairs set on the plan. Absent when there is none. |
invoice_display_name, parent_id, pending_deletion, created_at and applicable_usage_thresholds.
Charge (charges[])
| Field | Type | Description |
|---|---|---|
nozle_id | string (UUID) | Charge identifier. |
code | string or null | Charge code, unique within the plan. |
feature_id, feature_code | string | The metered feature the charge prices. |
invoice_display_name | string or null | Name shown on invoices. |
created_at | string | Creation time. |
charge_model | string | standard, graduated, package, percentage, volume, graduated_percentage, custom or dynamic. |
properties | object | The prices. Its keys depend on charge_model: amount (standard); graduated_ranges (graduated); amount, package_size, free_units (package); volume_ranges (volume); rate, fixed_amount, free_units_per_events, free_units_per_total_aggregation (percentage); graduated_percentage_ranges (graduated percentage). |
filters | array | Prices that differ by event property. Each item has nozle_id, charge_code, invoice_display_name, properties and values, an object that maps each filter key to the array of values it matches, for example { "region": ["eu", "us"] }. |
taxes | array | Taxes applied to the charge. |
pay_in_advance | boolean | true invoices the charge immediately, false at the end of the period. |
invoiceable | boolean | false means no invoice is issued for the charge itself. |
regroup_paid_fees | string or null | invoice or null. |
prorated | boolean | Whether the charge is prorated in the billing period. |
min_amount_cents | integer | Minimum spend for the charge per period, in cents. 0 for none. |
accepts_target_wallet | boolean | Whether the charge accepts a target prepaid wallet. |
applied_pricing_unit | object or null | { "code", "conversion_rate" } when the charge is priced in a pricing unit. |
parent_id | string (UUID) or null | Parent charge of an override. |
Fixed charge (fixed_charges[])
Only in GET /plans/{code}. Fields: nozle_id, add_on_id, add_on_code, code, invoice_display_name, created_at, charge_model (standard, graduated or volume), pay_in_advance, prorated, properties, units, parent_id and taxes.
Minimum commitment (minimum_commitment)
Fields: nozle_id, plan_code, commitment_type (always minimum_commitment), invoice_display_name, amount_cents (per interval), interval, created_at, updated_at and taxes.
Usage threshold (usage_thresholds[])
Fields: nozle_id, threshold_display_name, amount_cents (usage amount in cents that triggers a progressive billing invoice), recurring (true repeats the invoice each time usage grows by amount_cents), created_at and updated_at.
Tax (taxes[])
Fields: nozle_id, name, code, description, rate (percentage, for example 20), applied_to_organization (deprecated), created_at. The count fields add_ons_count, customers_count, plans_count, charges_count and commitments_count are always 0.
Entitlement (entitlements[])
| Field | Type | Description |
|---|---|---|
feature_type | string | boolean, config or metered. |
code, name, description | string, string, string or null | Feature code and labels. |
usage_limit | integer or null | Usage allowed per period. null when no limit is set. |
privileges | array | For config features, objects with code, name, value_type (integer, string, boolean or select), value and config. Empty for boolean and metered features. |
feature_id, hard_limit (for metered features, true blocks usage over the limit), min_margin_percent, margin_enforcement_mode and enforce_plan_margin.
Credit grant rule (credit_grant_rules[])
| Field | Type | Description |
|---|---|---|
nozle_id, plan_code | string | Rule identifier and plan code. |
credit_system_code | string | The credit system that is granted. |
amount | string | Credits granted per run, as an exact decimal such as "500.0". Credits, not cents. |
interval | string | month, year or lifetime. |
interval_count | integer | Periods between grants. |
status | string | active. Archived rules are not returned. |
plan_id, credit_system_id, scope (customer or entity), entity_join_policy, entity_leave_policy, upgrade_policy, downgrade_policy, created_at and updated_at.
Credit pool policy (credit_pool_policies[])
Fields: nozle_id, plan_id, plan_code, credit_system_id, credit_system_code, policy (entity_only, entity_then_customer or customer_only; see pool policies), status (active), created_at and updated_at.
React, Node, Python and Go
The SDKs declare only five fields for each plan (code, name, amount_cents, amount_currency, interval), but the API sends the fields above.
| Client | At runtime |
|---|---|
React usePlans / CatalogPlan | The plan objects from this response. PricingTable renders only the fields it knows. |
Node plans() | The plan objects. |
Python plans() | The plan dictionaries. |
Go Plans() | Only the five declared fields; the Go Plan struct drops the rest. |
usePlans.
Filter the catalog on the client
Every plan is returned, so decide in your code which ones to show:- Decide what to do with plans where
trial_periodis notnull: show them as trial plans or hide them. - Decide what to do with pay-in-arrears plans (
pay_in_advance: false). Their base price is billed at the end of the period. - Skip the plan codes you never sell on a pricing page, such as internal or legacy plans. Keep that list in your application; the catalog has no flag for it.
Keep sensitive pricing out of the catalog
Hiding a plan in your UI does not hide it from the API. Anyone can copy the publishable key from your page and call the endpoint directly.- Do not put negotiated, customer-specific or internal pricing in plans of the organization whose
pk_key you ship. This covers charges, taxes, minimum commitments, credit grants andmetadata. - Do not store secrets, internal notes or account details in a plan
descriptionormetadata. - If you need internal plans, keep them in a separate organization, so that its publishable key is never shipped.
- If you want a pricing page that shows a smaller set of fields, call the catalog from your backend with an
sk_key, keep only the fields you want, and pass that list toPricingTablethrough itsplansprop. The browser then never calls the catalog. The publishable key still works for anyone who has it.