API et exemples de webhooks par langue

URL de base de production : https://api.licensechain.app/v1 (11 mai 2026). Authentifiez-vous avec Authorization: Bearer … en utilisant un utilisateur JWT de /v1/auth/login ou une clé d'application API de Dashboard.

Pour les règles au niveau du champ (niveaux, identifiant de prix récurrent du partenaire de paiement tiers, indicateurs de livraison Enterprise uniquement), voir Produits API CRUD et le contrat OpenAPI dans le Référence API.

Bilan de santé (boucle)

curl -sS "https://api.licensechain.app/v1/health"

Liste des produits (boucle)

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"

Créer un produit (boucle)

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"}'

Mise à jour partielle — PATCH (curl)

PATCH /v1/products/:id accepte les mêmes champs JSON que PUT ; les clés omises restent inchangées.

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 (récupérer)

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 (demandes)

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 (boucle)

<?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)

Utiliser dans une méthode async ; ajoutez NuGet System.Text.Json si nécessaire.

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)

Ajoutez des dépendances : reqwest (avec la fonctionnalité json), 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 webhooks marchands (x-lc-pay-*)

Pay signe le corps JSON brut avec x-lc-pay-timestamp en utilisant le callbackSecret de votre produit (renvoyé lors de la création du produit/update pour les appelants autorisés). Message : chaîne UTF-8 timestamp + "." + rawBody, HMAC-SHA256, résumé hexadécimal minuscule, envoyé sous la forme x-lc-pay-signature. Comparez en temps constant. Voir aussi Rappels et 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 — vérifier la 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 — vérifier la 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 — vérifier la 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 — vérifier la 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 — vérifier la 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