API and webhook examples by language
Production base URL: https://api.licensechain.app/v1 (May 11, 2026). Authenticate with
Authorization: Bearer โฆ using a user JWT from /v1/auth/login or an app API key from the Dashboard.
For field-level rules (tiers, recurring Stripe price id, Enterprise-only delivery flags), see Products API CRUD and the OpenAPI contract in the API repository.
Health check (curl)
curl -sS "https://api.licensechain.app/v1/health" List products (curl)
curl -sS "https://api.licensechain.app/v1/products?limit=20&offset=0&active=true" \
-H "Authorization: Bearer YOUR_JWT_OR_APP_API_KEY" \
-H "Accept: application/json" Create product (curl)
curl -sS -X POST "https://api.licensechain.app/v1/products" \
-H "Authorization: Bearer YOUR_JWT_OR_APP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Pro Monthly","price":49.99,"currency":"USD","active":true,"productType":"subscription","billingType":"recurring","interval":"month","stripePriceIdRecurring":"price_1234567890","successUrl":"https://merchant.example.com/ok","cancelUrl":"https://merchant.example.com/cancel"}' Partial update โ PATCH (curl)
PATCH /v1/products/:id accepts the same JSON fields as PUT; omitted keys are left unchanged.
curl -sS -X PATCH "https://api.licensechain.app/v1/products/PRODUCT_UUID" \
-H "Authorization: Bearer YOUR_JWT_OR_APP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Pro Monthly (API)","active":true,"stripeTaxEnabled":true}' JavaScript (fetch)
const res = await fetch("https://api.licensechain.app/v1/products", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_JWT_OR_APP_API_KEY",
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
name: "Pro Monthly",
price: 49.99,
currency: "USD",
active: true,
productType: "subscription",
billingType: "recurring",
interval: "month",
stripePriceIdRecurring: "price_1234567890",
successUrl: "https://merchant.example.com/ok",
cancelUrl: "https://merchant.example.com/cancel",
}),
});
if (!res.ok) throw new Error(await res.text());
const product = await res.json();
console.log(product); Python (requests)
import requests
url = "https://api.licensechain.app/v1/products"
headers = {
"Authorization": "Bearer YOUR_JWT_OR_APP_API_KEY",
"Content-Type": "application/json",
"Accept": "application/json",
}
body = {
"name": "Pro Monthly",
"price": 49.99,
"currency": "USD",
"active": True,
"productType": "subscription",
"billingType": "recurring",
"interval": "month",
"stripePriceIdRecurring": "price_1234567890",
"successUrl": "https://merchant.example.com/ok",
"cancelUrl": "https://merchant.example.com/cancel",
}
r = requests.post(url, json=body, headers=headers, timeout=30)
r.raise_for_status()
print(r.json()) Go (net/http)
package main
import (
"bytes"
"encoding/json"
"net/http"
"time"
)
func main() {
const base = "https://api.licensechain.app/v1"
body := map[string]any{
"name": "Pro Monthly", "price": 49.99, "currency": "USD", "active": true,
"productType": "subscription", "billingType": "recurring", "interval": "month",
"stripePriceIdRecurring": "price_1234567890",
"successUrl": "https://merchant.example.com/ok",
"cancelUrl": "https://merchant.example.com/cancel",
}
b, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPost, base+"/products", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer YOUR_JWT_OR_APP_API_KEY")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
} PHP (curl)
<?php
$ch = curl_init("https://api.licensechain.app/v1/products");
$payload = json_encode([
"name" => "Pro Monthly",
"price" => 49.99,
"currency" => "USD",
"active" => true,
"productType" => "subscription",
"billingType" => "recurring",
"interval" => "month",
"stripePriceIdRecurring" => "price_1234567890",
"successUrl" => "https://merchant.example.com/ok",
"cancelUrl" => "https://merchant.example.com/cancel",
]);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_JWT_OR_APP_API_KEY",
"Content-Type: application/json",
"Accept: application/json",
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch); Ruby (stdlib)
require "net/http"
require "json"
require "uri"
uri = URI("https://api.licensechain.app/v1/products")
body = {
"name" => "Pro Monthly",
"price" => 49.99,
"currency" => "USD",
"active" => true,
"productType" => "subscription",
"billingType" => "recurring",
"interval" => "month",
"stripePriceIdRecurring" => "price_1234567890",
"successUrl" => "https://merchant.example.com/ok",
"cancelUrl" => "https://merchant.example.com/cancel",
}
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_JWT_OR_APP_API_KEY"
req["Content-Type"] = "application/json"
req["Accept"] = "application/json"
req.body = JSON.generate(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts res.body Java (HttpClient, Java 11+)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
var json = "{\"name\":\"Pro Monthly\",\"price\":49.99,\"currency\":\"USD\",\"active\":true,\"productType\":\"subscription\",\"billingType\":\"recurring\",\"interval\":\"month\",\"stripePriceIdRecurring\":\"price_1234567890\",\"successUrl\":\"https://merchant.example.com/ok\",\"cancelUrl\":\"https://merchant.example.com/cancel\"}";
var req = HttpRequest.newBuilder()
.uri(URI.create("https://api.licensechain.app/v1/products"))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer YOUR_JWT_OR_APP_API_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body()); C# (.NET HttpClient)
Use inside an async method; add NuGet System.Text.Json if needed.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var json = JsonSerializer.Serialize(new {
name = "Pro Monthly",
price = 49.99m,
currency = "USD",
active = true,
productType = "subscription",
billingType = "recurring",
interval = "month",
stripePriceIdRecurring = "price_1234567890",
successUrl = "https://merchant.example.com/ok",
cancelUrl = "https://merchant.example.com/cancel"
});
using var client = new HttpClient { BaseAddress = new Uri("https://api.licensechain.app/v1/") };
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "YOUR_JWT_OR_APP_API_KEY");
var content = new StringContent(json, Encoding.UTF8, "application/json");
var res = await client.PostAsync("products", content);
res.EnsureSuccessStatusCode();
Console.WriteLine(await res.Content.ReadAsStringAsync()); Rust (reqwest + serde_json)
Add dependencies: reqwest (with json feature), serde_json, tokio.
use reqwest::header::{AUTHORIZATION, ACCEPT, CONTENT_TYPE};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::builder().build()?;
let body = json!({
"name": "Pro Monthly",
"price": 49.99,
"currency": "USD",
"active": true,
"productType": "subscription",
"billingType": "recurring",
"interval": "month",
"stripePriceIdRecurring": "price_1234567890",
"successUrl": "https://merchant.example.com/ok",
"cancelUrl": "https://merchant.example.com/cancel",
});
let res = client
.post("https://api.licensechain.app/v1/products")
.header(AUTHORIZATION, "Bearer YOUR_JWT_OR_APP_API_KEY")
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.json(&body)
.timeout(std::time::Duration::from_secs(30))
.send()
.await?;
res.error_for_status_ref()?;
println!("{}", res.text().await?);
Ok(())
} Payment merchant webhooks (x-lc-pay-*)
Pay signs the raw JSON body together with x-lc-pay-timestamp using your product's
callbackSecret (returned on product create/update for authorized callers). Message: UTF-8 string
timestamp + "." + rawBody, HMAC-SHA256, lowercase hex digest, sent as
x-lc-pay-signature. Compare in constant time. See also
Callbacks & Webhooks.
Core API registered webhooks (app-level, POST /v1/webhooks) are signed with HMAC-SHA256 over the exact JSON request body; the platform sends header X-Webhook-Signature (hex). Verify with the webhook secret you stored at registration. This differs from Pay merchant callbacks (x-lc-pay-signature), which sign the UTF-8 string (x-lc-pay-timestamp) + "." + (raw request body bytes as UTF-8).
Node.js โ verify signature
import crypto from "node:crypto";
/** rawBody must be the exact request bytes/string before JSON.parse */
export function verifyLcPaySignature(
rawBody: string,
secret: string,
timestampHeader: string | undefined,
signatureHex: string | undefined
): boolean {
if (!timestampHeader || !signatureHex || !secret) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(timestampHeader + "." + rawBody, "utf8")
.digest("hex");
try {
const a = Buffer.from(signatureHex, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
} catch {
return false;
}
} Python โ verify signature
import hmac
import hashlib
def verify_lc_pay_signature(raw_body: str, secret: str, ts: str, sig_hex: str) -> bool:
msg = f"{ts}.{raw_body}".encode("utf-8")
expected = hmac.new(secret.encode("utf-8"), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_hex) Go โ verify signature
package payhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"subtle"
)
func VerifyLcPaySignature(rawBody, secret, ts, sigHex string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts + "." + rawBody))
expected := hex.EncodeToString(mac.Sum(nil))
if len(expected) != len(sigHex) {
return false
}
return subtle.ConstantTimeCompare([]byte(expected), []byte(sigHex)) == 1
} PHP โ verify signature
<?php
function verify_lc_pay_signature(string $rawBody, string $secret, string $ts, string $sigHex): bool {
$expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals($expected, $sigHex);
} Ruby โ verify signature
require "openssl"
def verify_lc_pay_signature(raw_body, secret, ts, sig_hex)
digest = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{ts}.#{raw_body}")
return false if digest.bytesize != sig_hex.bytesize
OpenSSL.fixed_length_secure_compare(digest, sig_hex)
end