Examples

Use this page when you want end-to-end starting points rather than route-by-route explanation.

Base URLs

Marketing site:  https://www.hilt.so
Merchant app:    https://app.hilt.so
API:             https://api.hilt.so
Docs:            https://docs.hilt.so

Auth patterns

Server-side merchant automation normally uses:
X-Hilt-Key: hk_live_...
Session-assisted merchant tools can use:
Authorization: Bearer JWT_TOKEN

Example 1: Create a hosted access product

cURL

curl -X POST https://api.hilt.so/v1/products \
  -H "X-Hilt-Key: hk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "product_type": "PAYMENT_LINK",
    "title": "Members lounge",
    "description": "Monthly access",
    "amount_minor_units": 200000,
    "token_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "merchant_wallet": "So1anaMerchantWallet1111111111111111111111111",
    "delivery_type": "REDIRECT",
    "delivery_value": "https://example.com/welcome",
    "membership_config": {
      "enabled": true,
      "platform": "CUSTOM",
      "identity_type": "WALLET",
      "identity_required": false,
      "renewal_mode": "ONE_OFF",
      "billing_interval_days": 30,
      "grace_period_days": 3
    }
  }'

TypeScript

const response = await fetch("https://api.hilt.so/v1/products", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Hilt-Key": process.env.HILT_API_KEY!,
  },
  body: JSON.stringify({
    product_type: "PAYMENT_LINK",
    title: "Discord membership",
    description: "Monthly paid access",
    amount_minor_units: 29000000,
    token_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    merchant_wallet: "So1anaMerchantWallet1111111111111111111111111",
    delivery_type: "DISCORD_INVITE",
    delivery_value: "https://discord.gg/example",
    membership_config: {
      enabled: true,
      platform: "DISCORD",
      identity_type: "DISCORD_USER_ID",
      renewal_mode: "ONE_OFF",
      billing_interval_days: 30,
      grace_period_days: 3,
    },
  }),
});

if (!response.ok) {
  const error = await response.json().catch(() => ({}));
  throw new Error(error.detail ?? `Hilt request failed: ${response.status}`);
}

const product = await response.json();
console.log(product.id, product.slug);
Set renewal_mode to ONE_OFF for single-payment products. Use native subscription setup for recurring products when the workspace has been enabled for it.

Official SDKs and Postman

If you want a faster start than hand-written fetch or requests, use the official developer assets:
  • TypeScript SDK: npm install @hiltpay/sdk
  • TypeScript SDK source: https://github.com/Hiltpay/hilt-sdk-js
  • Python SDK: pip install hilt-sdk
  • Python SDK source: https://github.com/Hiltpay/hilt-sdk-python
  • Developer assets repo: https://github.com/Hiltpay/hilt-developer-assets
The SDKs wrap the same examples shown below. The developer-assets repo carries public OpenAPI snapshots, Postman imports, and example webhook payloads for products, checkout, payments, memberships, receipts, support, and webhooks.

Example 2: Protect an AI API endpoint with Hilt Pay API

This is the hero proof for Hilt Pay API. Your server checks Hilt before serving paid work. If the user or agent has not paid, your server returns HTTP 402 Payment Required with the Hilt checkout handoff or x402-shaped payment requirement that the session response provides. Solana USDC is the live settlement rail.

FastAPI

import os
import httpx
from fastapi import FastAPI, Header, HTTPException

app = FastAPI()
HILT_API_URL = os.getenv("HILT_API_URL", "https://api.hilt.so")
HILT_API_KEY = os.environ["HILT_API_KEY"]
PRODUCT = "pro-ai-api"


async def hilt_post(path: str, payload: dict):
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.post(
            f"{HILT_API_URL}{path}",
            headers={"X-Hilt-Key": HILT_API_KEY, "Content-Type": "application/json"},
            json=payload,
        )
        response.raise_for_status()
        return response.json()


@app.post("/ai/pro")
async def pro_endpoint(x_customer_id: str = Header(...)):
    entitlement = await hilt_post(
        "/v1/access/entitlements/check",
        {
            "external_product_id": PRODUCT,
            "external_customer_id": x_customer_id,
            "rail": "solana_usdc",
        },
    )

    if entitlement.get("has_access"):
        return {"result": "paid work can run"}

    session = await hilt_post(
        "/v1/access/payment-sessions",
        {
            "external_product_id": PRODUCT,
            "external_customer_id": x_customer_id,
            "rail": "solana_usdc",
            "metadata": {"resource": "/ai/pro"},
        },
    )

    payment_session = session.get("payment_session", {})
    detail = {
        "error": "payment_required",
        "rail": session.get("rail", "solana_usdc"),
        "payment_session": payment_session,
        "checkout_url": payment_session.get("checkout_url"),
    }

    if session.get("payment_requirement"):
        detail.update(
            {
                "payment_protocol": "x402",
                "settlement_rail": "solana_usdc",
                "payment_requirement": session["payment_requirement"],
            }
        )

    raise HTTPException(status_code=402, detail=detail)

Node and Express

import express from "express";

const app = express();
const HILT_API_URL = process.env.HILT_API_URL ?? "https://api.hilt.so";
const HILT_API_KEY = process.env.HILT_API_KEY!;
const PRODUCT = "pro-ai-api";

app.use(express.json());

async function hiltPost(path: string, body: unknown) {
  const response = await fetch(`${HILT_API_URL}${path}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Hilt-Key": HILT_API_KEY,
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    throw new Error(`Hilt request failed: ${response.status}`);
  }

  return response.json();
}

app.post("/ai/pro", async (req, res, next) => {
  try {
    const customerId = req.header("x-customer-id");

    if (!customerId) {
      return res.status(400).json({ error: "x-customer-id required" });
    }

    const entitlement = await hiltPost("/v1/access/entitlements/check", {
      external_product_id: PRODUCT,
      external_customer_id: customerId,
      rail: "solana_usdc",
    });

    if (entitlement.has_access) {
      return res.json({ result: "paid work can run" });
    }

    const session = await hiltPost("/v1/access/payment-sessions", {
      external_product_id: PRODUCT,
      external_customer_id: customerId,
      rail: "solana_usdc",
      metadata: { resource: "/ai/pro" },
    });

    const paymentSession = session.payment_session ?? {};
    const body: Record<string, unknown> = {
      error: "payment_required",
      rail: session.rail ?? "solana_usdc",
      payment_session: paymentSession,
      checkout_url: paymentSession.checkout_url,
    };

    if (session.payment_requirement) {
      body.payment_protocol = "x402";
      body.settlement_rail = "solana_usdc";
      body.payment_requirement = session.payment_requirement;
    }

    return res.status(402).json(body);
  } catch (error) {
    return next(error);
  }
});
Use the same rule in every framework:
  1. check Hilt entitlement before doing paid work
  2. return 402 when Hilt says access is missing
  3. include the Hilt session handoff from the session response
  4. retry the entitlement check after payment succeeds
The access rule is simple: do not serve the protected work until Hilt returns has_access: true. Use this when your own bot or backend already knows who the buyer is before checkout.
curl -X POST https://api.hilt.so/v1/products/PRODUCT_ID/handoff-link \
  -H "X-Hilt-Key: hk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "identity_type": "TELEGRAM_USER_ID",
    "identity_value": "728255790",
    "identity_display": "@buyer",
    "expires_in_minutes": 30
  }'
Representative response:
{
  "handoff_token": "hilt_handoff_...",
  "expires_at": "2026-04-16T18:30:00Z",
  "checkout_url": "https://www.hilt.so/p/example-slug?handoff=hilt_handoff_..."
}

Example 4: Start a buyer session and keep the payment id

cURL

curl -X POST https://api.hilt.so/v1/products/p/example-slug/connect \
  -H "Content-Type: application/json" \
  -d '{
    "payer_wallet": "BuyerWallet1111111111111111111111111111111111"
  }'

What to keep from the response

Persist:
  • payment_id
  • amount_minor_units
  • merchant_amount_minor_units
  • asset_symbol
  • expires_at

Example 5: Poll payment status during an active buyer session

cURL

curl https://api.hilt.so/v1/payments/PAYMENT_ID

TypeScript

type PaymentStatus = {
  id: string;
  product_id: string;
  status: string;
  tx_signature?: string | null;
  delivery_status?: string | null;
};

async function getPayment(paymentId: string): Promise<PaymentStatus> {
  const response = await fetch(`https://api.hilt.so/v1/payments/${paymentId}`);
  if (!response.ok) {
    throw new Error(`Unable to read payment ${paymentId}: ${response.status}`);
  }
  return response.json();
}

const payment = await getPayment("f0f4e620-1ca3-4fc8-b0ba-2d04342fe467");

switch (payment.status) {
  case "CONFIRMED":
    console.log("ready for member lookup");
    break;
  case "PENDING_CONFIRMATION":
    console.log("keep waiting with the same payment_id");
    break;
  default:
    console.log("current status:", payment.status);
}
Use this when the buyer is still waiting on the checkout screen. For longer-running automation after checkout, prefer Hilt webhooks.

Python

import requests

def get_payment(payment_id: str):
    response = requests.get(
        f"https://api.hilt.so/v1/payments/{payment_id}",
        timeout=20,
    )
    response.raise_for_status()
    return response.json()

payment = get_payment("f0f4e620-1ca3-4fc8-b0ba-2d04342fe467")
print(payment["status"], payment.get("tx_signature"))

Rust

use reqwest::Client;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Payment {
    id: String,
    status: String,
    tx_signature: Option<String>,
}

async fn get_payment(payment_id: &str) -> Result<Payment, reqwest::Error> {
    let client = Client::new();
    client
        .get(format!("https://api.hilt.so/v1/payments/{payment_id}"))
        .send()
        .await?
        .error_for_status()?
        .json::<Payment>()
        .await
}

Example 6: Read the post-payment trail

Look up a membership

curl "https://api.hilt.so/v1/memberships/lookup?identity=@buyer" \
  -H "X-Hilt-Key: hk_live_..."

Read receipts in Python

import requests

response = requests.get(
    "https://api.hilt.so/v1/receipts",
    headers={"X-Hilt-Key": "hk_live_..."},
    params={"page": 1, "per_page": 20},
    timeout=20,
)
response.raise_for_status()
print(response.json())

Example 7: Open a support ticket

curl -X POST https://api.hilt.so/v1/support/tickets \
  -H "X-Hilt-Key: hk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Buyer needs help",
    "category": "OTHER",
    "body": "Payment confirmed, but the buyer still needs a manual handoff."
  }'

Example 8: Continue a support thread

curl -X POST https://api.hilt.so/v1/support/tickets/hilt_ticket_R64eNsqH_nntOg/message \
  -H "X-Hilt-Key: hk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "body": "We retried delivery and confirmed the membership is active."
  }'

Example 9: Webhook consumer after checkout

If your application already has queue workers or background automation, native webhooks are now the cleanest Hilt integration.

TypeScript

async function handleHiltWebhook(event: {
  id: string;
  type: string;
  data: {
    payment?: { id: string; status: string };
    membership?: { id: string; status: string };
    receipt?: { id: string };
  };
}) {
  if (await alreadyProcessed(event.id)) {
    return;
  }

  if (event.type === "payment.confirmed") {
    await unlockAccessFromPayment(event.data.payment?.id);
    await markProcessed(event.id);
    return;
  }

  if (event.type === "payment.failed") {
    await markCheckoutFailed(event.data.payment?.id);
    await markProcessed(event.id);
  }
}

Python

def handle_hilt_webhook(event: dict) -> None:
    if already_processed(event["id"]):
        return

    if event["type"] == "payment.confirmed":
        unlock_access_from_payment(event["data"]["payment"]["id"])
        mark_processed(event["id"])
        return

    if event["type"] == "payment.failed":
        mark_checkout_failed(event["data"]["payment"]["id"])
        mark_processed(event["id"])

Example rule of thumb

The simplest reliable pattern is:
  1. let Hilt run checkout and settlement
  2. keep the payment_id
  3. let your backend react to webhook events afterwards
  4. read GET /v1/payments/{payment_id} only when the buyer is actively waiting or you need a fallback check
That is usually cleaner than rebuilding the payment, membership, receipt, and support state machine yourself.

Common questions

Which Hilt example should I copy first?

Start with product creation, then signed handoff or checkout session start, then payment status, then webhook handling.

Do the examples replace the SDKs?

No. The examples show the underlying API patterns. Use the SDKs when you want typed helpers and less hand-written request code.

What is the safest example pattern?

Let Hilt run hosted checkout and settlement, store payment_id, and react to signed webhook events after checkout.