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