cURL
curl --request POST 'https://api.integrateledger.com/v1/plans' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"id":"starter","name":"Starter","monthlyCredits":100,"monthlyPriceCents":1900}'import os
from ledger_sdk import Ledger
ledger = Ledger(api_key=os.environ["LEDGER_API_KEY"])
ledger.create_plan(id="starter", name="Starter", monthly_credits=100, monthly_price_cents=1900)import { Ledger } from "@ledger/sdk";
const ledger = new Ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await ledger.createPlan({ id: "starter", name: "Starter", monthlyCredits: 100, monthlyPriceCents: 1900 });const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
id: '<string>',
name: '<string>',
monthlyCredits: 123,
monthlyPriceCents: 1073741823
})
};
fetch('https://api.integrateledger.com/v1/plans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.integrateledger.com/v1/plans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'id' => '<string>',
'name' => '<string>',
'monthlyCredits' => 123,
'monthlyPriceCents' => 1073741823
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.integrateledger.com/v1/plans"
payload := strings.NewReader("{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.integrateledger.com/v1/plans")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.integrateledger.com/v1/plans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}"
response = http.request(request)
puts response.read_body{
"item": {
"id": "starter",
"name": "Starter",
"monthlyCredits": 100,
"monthlyPriceCents": 1900,
"stripeSyncStatus": "synced"
},
"changed": true,
"stripeSync": "synced"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Plans
Create a plan
Create one plan without replacing other catalog items. Paid plans sync to the selected environment’s Stripe account.
POST
/
v1
/
plans
cURL
curl --request POST 'https://api.integrateledger.com/v1/plans' \
-H "Authorization: Bearer $LEDGER_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"id":"starter","name":"Starter","monthlyCredits":100,"monthlyPriceCents":1900}'import os
from ledger_sdk import Ledger
ledger = Ledger(api_key=os.environ["LEDGER_API_KEY"])
ledger.create_plan(id="starter", name="Starter", monthly_credits=100, monthly_price_cents=1900)import { Ledger } from "@ledger/sdk";
const ledger = new Ledger({ apiKey: process.env.LEDGER_API_KEY ?? "" });
await ledger.createPlan({ id: "starter", name: "Starter", monthlyCredits: 100, monthlyPriceCents: 1900 });const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
id: '<string>',
name: '<string>',
monthlyCredits: 123,
monthlyPriceCents: 1073741823
})
};
fetch('https://api.integrateledger.com/v1/plans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.integrateledger.com/v1/plans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'id' => '<string>',
'name' => '<string>',
'monthlyCredits' => 123,
'monthlyPriceCents' => 1073741823
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.integrateledger.com/v1/plans"
payload := strings.NewReader("{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.integrateledger.com/v1/plans")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.integrateledger.com/v1/plans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"id\": \"<string>\",\n \"name\": \"<string>\",\n \"monthlyCredits\": 123,\n \"monthlyPriceCents\": 1073741823\n}"
response = http.request(request)
puts response.read_body{
"item": {
"id": "starter",
"name": "Starter",
"monthlyCredits": 100,
"monthlyPriceCents": 1900,
"stripeSyncStatus": "synced"
},
"changed": true,
"stripeSync": "synced"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create one plan without replacing other catalog items. A plan grants a monthly credit allowance to each customer who buys it. The request uses integer cents for the monthly price and an integer credit count for the allowance.
Choose a free or paid plan
- Set
monthlyPriceCentsto0for a free plan. Attach it withPOST /v1/billing/attach; Ledger activates it and grants its first monthly allowance immediately, without Stripe or a Checkout URL. - Set
monthlyPriceCentsabove0for a recurring plan. Ledger syncs a monthly Stripe Price to the Stripe account connected in the key’s environment. For example,1900means $19.00 per month in a USD catalog. monthlyCreditsmust be positive. Paid plans grant the allowance after Ledger receives Stripe’s paid invoice event. Free plans grant it on assignment; each new monthly period creates an allowance when the customer’s balance is next read or usage is recorded.
409 already_exists.
After creating a paid plan
The response contains the createditem, changed: true, and a stripeSync state. awaiting_connection means Stripe has not been connected yet; connect it under Developer → Stripe in the dashboard, then confirm the plan reports synced. If the state is pending or failed, check the sync result before sending customers to Checkout. Ledger can retry synchronization when an attach request arrives. The dashboard shows the state in Catalog → Plans.
Create a customer with Create or get a customer, then use Attach a plan or add-on to get a hosted Checkout URL. A successful browser redirect is not proof of payment. Ledger activates the subscription and grants credits after the signed Stripe invoice webhook arrives.
For a free plan, the attach response contains subscriptionId and status: "active" rather than checkoutUrl. No Stripe connection or payment redirect is needed.
Changing a plan later
Update a plan requires its full editable body. A changed plan creates a new version for future Checkouts; existing subscriptions retain their referenced version. Archive a plan removes it from active lists and new purchases while retaining historical subscriptions. See Catalog for the full lifecycle.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
application/json