Developer quickstart

This is the fastest reliable path from a new Hilt workspace to one working developer integration. By the end of this page, you should have:
  • one product created from code
  • one webhook endpoint with a signed test delivery
  • one sandbox session with fake payment, membership, and receipt objects
  • one tiny live payment plan for final validation
This page is intentionally practical. It optimizes for one working flow, not full API coverage.

30-second win

If you only want to prove the Hilt developer surface is alive before doing anything deeper, run these two reads first.

1. Prove your API key works

curl https://api.hilt.so/v1/products \
  -H "X-Hilt-Key: hk_live_..."
Success looks like:
  • a 200 response
  • a JSON array or product list payload from your workspace
Representative response:
[
  {
    "id": "ae9673c8-95db-4b39-bc2c-b5e6d5dfd9d3",
    "slug": "example-slug",
    "title": "Hilt Quickstart Access",
    "status": "ACTIVE"
  }
]

2. Prove sandbox is available

curl https://api.hilt.so/v1/testing/scenarios \
  -H "X-Hilt-Key: hk_sandbox_..."
Success looks like:
  • a 200 response
  • a scenarios list that includes confirmed_access, payment_failed, delivery_failed, and renewal_grace
Representative response:
{
  "count": 4,
  "scenarios": [
    { "id": "confirmed_access", "title": "Confirmed access" },
    { "id": "payment_failed", "title": "Payment failed" },
    { "id": "delivery_failed", "title": "Delivery failed" },
    { "id": "renewal_grace", "title": "Renewal grace" }
  ]
}
If both commands work, your auth, base URL, and testing surface are in place. From there, the rest of this page becomes a straight line.

Before you start

Have these ready:
  • a Hilt workspace with Hilt Pay enabled
  • one payout wallet you actually want to use
  • one post-payment destination such as a redirect, download, Telegram flow, or Discord flow
  • one API key from Dashboard -> Advanced
  • one sandbox API key if you want to call the testing routes over API
  • one dashboard session token if you want to manage webhook endpoints by API instead of the dashboard UI

1. Set your environment variables once

Use the live API base URL unless you are explicitly testing a non-production environment.
export HILT_API_URL="https://api.hilt.so"
export HILT_API_KEY="hk_live_..."
export HILT_SANDBOX_KEY="hk_sandbox_..."
export HILT_BEARER_TOKEN="JWT_TOKEN"
export HILT_MERCHANT_WALLET="So1anaMerchantWallet1111111111111111111111111"
What each value is for:
  • HILT_API_KEY: merchant backend routes
  • HILT_SANDBOX_KEY: sandbox testing routes
  • HILT_BEARER_TOKEN: webhook endpoint management only
  • HILT_MERCHANT_WALLET: the payout wallet used in the example product
Checkpoint:
  • you can now run merchant API calls with X-Hilt-Key
  • you can now create webhook endpoints with Authorization: Bearer ...

Node .env.local

HILT_API_URL=https://api.hilt.so
HILT_API_KEY=hk_live_...
HILT_SANDBOX_KEY=hk_sandbox_...
HILT_BEARER_TOKEN=JWT_TOKEN
HILT_MERCHANT_WALLET=So1anaMerchantWallet1111111111111111111111111
HILT_WEBHOOK_SECRET=whsec_...

Python .env

HILT_API_URL=https://api.hilt.so
HILT_API_KEY=hk_live_...
HILT_SANDBOX_KEY=hk_sandbox_...
HILT_BEARER_TOKEN=JWT_TOKEN
HILT_MERCHANT_WALLET=So1anaMerchantWallet1111111111111111111111111
HILT_WEBHOOK_SECRET=whsec_...

2. Install one client surface

Use whichever surface matches how you work:

TypeScript

npm install @hiltpay/sdk

Python

pip install hilt-sdk

Postman

Use the public developer-assets repo:
  • https://github.com/Hiltpay/hilt-developer-assets
Import:
  • postman/hilt-postman-collection.json
  • postman/hilt-postman-environment.json
If you prefer raw HTTP first, stay with the cURL examples below and move to the SDK or Postman pages afterwards.

3. Create one manual recurring product

Start with a simple product that gives you the full Hilt trail:
  • price
  • checkout payload
  • payment
  • membership
  • receipt
  • renewal intelligence later
curl -X POST "$HILT_API_URL/v1/products" \
  -H "X-Hilt-Key: $HILT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product_type": "PAYMENT_LINK",
    "title": "Hilt Quickstart Access",
    "description": "Manual recurring access for integration testing",
    "amount_minor_units": 29000000,
    "token_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "merchant_wallet": "'"$HILT_MERCHANT_WALLET"'",
    "delivery_type": "REDIRECT",
    "delivery_value": "https://example.com/hilt/thanks",
    "membership_config": {
      "enabled": true,
      "platform": "CUSTOM",
      "identity_type": "WALLET",
      "identity_required": false,
      "renewal_mode": "MANUAL",
      "billing_interval_days": 30,
      "grace_period_days": 3
    }
  }'
Representative response:
{
  "id": "ae9673c8-95db-4b39-bc2c-b5e6d5dfd9d3",
  "slug": "example-slug",
  "status": "ACTIVE",
  "product_type": "PAYMENT_LINK",
  "title": "Hilt Quickstart Access",
  "amount_minor_units": 29000000
}
Save these immediately:
  • product.id
  • product.slug
Checkpoint:
  • the product appears in the dashboard
  • GET /v1/products/{product_id} returns the same commercial object

4. Create one webhook endpoint and send a signed test event

Webhook endpoint management uses a workspace session token, not an API key. If you do not want to create the endpoint over API, create it once in Dashboard -> Advanced and then continue with the test-event and sandbox steps.

Create the endpoint

curl -X POST "$HILT_API_URL/v1/webhooks/endpoints" \
  -H "Authorization: Bearer $HILT_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Quickstart receiver",
    "url": "https://example.com/hilt/webhooks",
    "subscribed_events": [
      "payment.confirmed",
      "receipt.created",
      "membership.activated",
      "delivery.failed"
    ],
    "product_ids": []
  }'
Representative response:
{
  "endpoint": {
    "id": "wh_123",
    "label": "Quickstart receiver",
    "status": "active"
  },
  "signing_secret": "whsec_..."
}
Store the signing secret immediately. Hilt only returns the full secret on creation or rotation.

Send a signed test event

curl -X POST "$HILT_API_URL/v1/webhooks/endpoints/wh_123/test" \
  -H "Authorization: Bearer $HILT_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "payment.confirmed"
  }'
Checkpoint:
  • the endpoint exists in Hilt
  • your receiver gets a signed request
  • the delivery log shows success or a concrete failure reason

Smallest possible webhook receiver: Node

If you want the shortest possible Node receiver that can go green on a signed test event, use this:
import crypto from "node:crypto";
import express from "express";

const app = express();
const signingSecret = process.env.HILT_WEBHOOK_SECRET!;

function verifySignature(rawBody: Buffer, signatureHeader: string) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, value] = part.split("=", 2);
      return [key, value];
    }),
  );

  const timestamp = parts.t;
  const received = parts.v1;
  if (!timestamp || !received) {
    throw new Error("Malformed Hilt signature header");
  }

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody.toString("utf8")}`)
    .digest("hex");

  const valid = crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(received, "utf8"),
  );

  if (!valid) {
    throw new Error("Invalid Hilt webhook signature");
  }
}

app.post("/hilt/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body as Buffer;
  const signatureHeader = req.header("X-Hilt-Signature") ?? "";

  verifySignature(rawBody, signatureHeader);

  const event = JSON.parse(rawBody.toString("utf8"));
  console.log("hilt event", event.id, event.type, event.livemode);

  res.status(200).json({ ok: true });
});

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000/hilt/webhooks");
});

Smallest possible webhook receiver: Python

import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
signing_secret = os.environ["HILT_WEBHOOK_SECRET"]


def verify_signature(raw_body: bytes, signature_header: str) -> None:
    parts = {}
    for chunk in signature_header.split(","):
        key, value = chunk.split("=", 1)
        parts[key] = value

    timestamp = parts.get("t")
    received = parts.get("v1")
    if not timestamp or not received:
        raise ValueError("Malformed Hilt signature header")

    expected = hmac.new(
        signing_secret.encode("utf-8"),
        f"{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, received):
        raise ValueError("Invalid Hilt webhook signature")


@app.post("/hilt/webhooks")
async def hilt_webhooks(request: Request):
    raw_body = await request.body()
    signature_header = request.headers.get("X-Hilt-Signature", "")

    try:
        verify_signature(raw_body, signature_header)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    event = json.loads(raw_body.decode("utf-8"))
    print("hilt event", event["id"], event["type"], event["livemode"])

    return {"ok": True}
Success looks like:
  • Hilt sends a signed test event
  • your receiver logs the event id and type
  • Hilt marks the delivery as successful

5. Run one sandbox session

Now validate your contract handling without using a real payment.
curl -X POST "$HILT_API_URL/v1/testing/sessions" \
  -H "X-Hilt-Key: $HILT_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scenario": "confirmed_access",
    "product_id": "ae9673c8-95db-4b39-bc2c-b5e6d5dfd9d3",
    "identity_display": "@sandboxbuyer"
  }'
Representative response:
{
  "session": {
    "id": "6f9418d1-cf8e-4fcb-9b0d-90b31f407111",
    "scenario": "confirmed_access",
    "status": "complete",
    "fake_payment_id": "sandbox_pay_0b0fbaf8d8b844b38b",
    "fake_membership_id": "sandbox_mem_76fbb6674f1e4e7d9d",
    "fake_receipt_id": "sandbox_rcpt_4c56f442f8274e7d8f",
    "event_count": 3,
    "delivery_count": 1
  },
  "scenario": {
    "id": "confirmed_access",
    "title": "Confirmed access"
  }
}
What you should verify:
  • your webhook consumer receives livemode=false events
  • your application can handle payment.confirmed, receipt.created, and membership.activated
  • you can inspect the fake ids later through the returned sandbox session

6. Finish with one tiny live payment

Sandbox proves your Hilt contract handling. It does not prove wallet UX, chain confirmation, or payout routing. Do one small live payment before real traffic:
  1. open https://www.hilt.so/p/YOUR_SLUG
  2. complete one tiny real payment
  3. keep the payment_id if your app started the session itself
  4. keep the transaction signature if you need to compare API, dashboard, and receipt views
If you want to start the session from your own app flow, this is the connect call:
curl -X POST "$HILT_API_URL/v1/products/p/example-slug/connect" \
  -H "Content-Type: application/json" \
  -d '{
    "payer_wallet": "BuyerWallet1111111111111111111111111111111111"
  }'
Checkpoint:
  • you now have a real payment_id
  • the buyer saw the real wallet flow
  • Hilt can now be verified against live settlement, receipt creation, and membership state

7. Verify the Hilt trail after the live payment

Use Hilt as the source of truth after payment.

Payment state

curl "$HILT_API_URL/v1/payments/PAYMENT_ID"

Membership state

curl "$HILT_API_URL/v1/memberships?product_id=ae9673c8-95db-4b39-bc2c-b5e6d5dfd9d3&limit=20" \
  -H "X-Hilt-Key: $HILT_API_KEY"

Receipt proof

curl "$HILT_API_URL/v1/receipts?page=1&per_page=20&product_id=ae9673c8-95db-4b39-bc2c-b5e6d5dfd9d3" \
  -H "X-Hilt-Key: $HILT_API_KEY"
Expected result:
  • payment is visible and reaches CONFIRMED
  • membership exists if the flow should create access
  • receipt exists and has a public proof URL
  • webhook delivery log shows the real outbound event trail

Common first-launch mistakes

  • using an API key for webhook endpoint management
  • skipping the signed test event and only discovering signature problems on the first real payment
  • treating sandbox as proof of wallet or payout correctness
  • launching multiple products before one path is proven

Where to go next