Developer Documentation

HashPrism is a product hosting platform that uses the Solana Pay protocol as its payment rail. The API lets you manage your catalog, observe transactions, and automate fulfillment — all secured with scoped Bearer tokens. Transactions are always initiated by the buyer or agent holding the wallet. Your integrations react to what happens.

Overview

HashPrism exposes two integration surfaces. They serve different purposes and are designed to be used together.

Webhooks

Push notifications sent to your server when events occur on-chain. Use webhooks for real-time fulfillment, CRM updates, buyer receipts, and downstream automation. This is the primary integration path for most use cases.

REST API

Pull interface for reading payments, managing your product catalog, and initiating refunds. Authenticated with Bearer tokens. Designed for scripts, AI agents, and server-side workflows that need on-demand access to your store data.

What you can do with the API

List and read your payments and refunds
Create, update, and delete products
Initiate refunds on confirmed payments
Sync payment history to external systems
Automate fulfillment triggered by webhooks
Build AI agents that manage your store

What you cannot do

Create payments. Payments are exclusively buyer-initiated through the Solana Pay checkout flow. When a buyer visits your product page and clicks Pay, the checkout client calls POST /api/v1/payments/create on their behalf. The resulting QR code is signed and broadcast by their wallet. There is no way to create a payment on behalf of a buyer via the API — this is intentional and a property of the trustless Solana Pay protocol.

Quickstart

Get from zero to a working integration in five minutes.

1

Create an API key

Go to Dashboard → Settings → API Keys. Give it a name, select the scopes you need, and optionally set an expiry. Copy the token immediately — it is shown once and HashPrism stores only a SHA-256 hash.

2

Make your first request

curl https://hashprism.com/api/v1/products \
  -H "Authorization: Bearer hp_your_key_here"

# Response
{
  "data": [ ... ],
  "meta": {
    "request_id": "7abbd72e-e361-4871-ab30-26518e4fbdcb",
    "count": 3,
    "limit": 50,
    "has_more": false,
    "next_cursor": null
  }
}
3

Register a webhook endpoint

Go to Dashboard → Settings → Webhooks. Add your HTTPS endpoint and copy the webhook secret. Use the Test button to send a sample payload and confirm delivery. Your endpoint must respond with 2xx within 10 seconds.

4

Verify signatures and handle events

import crypto from 'crypto'

export async function POST(req: Request) {
  const rawBody = await req.text()
  const sig = req.headers.get('x-hashprism-signature') ?? ''
  const timestamp = sig.match(/t=(\d+)/)?.[1]
  const receivedHex = sig.match(/v1=([a-f0-9]+)/)?.[1]

  if (!timestamp || !receivedHex) return new Response('Bad signature', { status: 400 })
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return new Response('Replay', { status: 400 })

  const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(`${timestamp}.${rawBody}`).digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedHex))) {
    return new Response('Unauthorized', { status: 401 })
  }

  const { event, data } = JSON.parse(rawBody)

  // Respond immediately, process async
  if (event === 'payment.confirmed') {
    queueFulfillment(data.payment_id, data.buyer_email)
  }

  return new Response('OK', { status: 200 })
}
5

Go live

HashPrism runs on Solana mainnet. All payments are real USDC or SOL transactions settled atomically on-chain, including the 0.99% platform fee. There is no sandbox mode — use small amounts for integration testing.

Payment Lifecycle

Every payment moves through a defined set of states. Understanding the lifecycle is critical for building correct integrations — especially for fulfillment logic and refund eligibility.

pendingconfirmedrefunded
pendingexpired
pendingfailed
StatusMeaning
pendingPayment record created. Awaiting buyer to sign and broadcast the Solana transaction.
confirmedTransaction verified on-chain. Correct amounts sent to correct wallets. payment.confirmed webhook dispatched.
expiredBuyer did not complete payment within 10 minutes. Checkout timed out.
failedTransaction landed on-chain but was reverted. Funds were not transferred.
refundedRefund transaction verified on-chain. Buyer received funds. refund.confirmed webhook dispatched.

Refund Lifecycle

Refunds have their own status independent of the payment. A refund can only be initiated on a confirmed payment. The refund record moves through pendingconfirmed or failed. The parent payment moves to refunded only after the refund is confirmed on-chain.

Authentication

Bearer Token

Pass your API key as a Bearer token on every authenticated request. Keys are prefixed hp_ followed by 64 hex characters (256 bits of entropy).

Authorization: Bearer hp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

If both a Bearer token and a session cookie are present in the same request, the Bearer token takes precedence. Session cookies are used by the HashPrism dashboard — external integrations should always use Bearer tokens.

Scopes

Each key is granted one or more scopes at creation time. Scopes cannot be modified after issuance — revoke and reissue to change permissions. Grant only the minimum scopes your integration requires.

ScopeGrants access to
payments:readList and retrieve payments and refunds
products:readList and retrieve products
products:writeCreate, update, and delete products
refunds:writeInitiate refunds on confirmed payments

Key management, webhook management, and account deletion are session-only — no Bearer access. These operations require a logged-in dashboard session to prevent unrecoverable actions via leaked keys.

Response Envelope

All authenticated REST endpoints return a consistent JSON envelope. Every response includes a request_id UUID for tracing specific requests in logs and support tickets.

Single resource — 200

{
  "data": { ... },
  "meta": {
    "request_id": "uuid"
  }
}

Collection — 200

{
  "data": [ ... ],
  "meta": {
    "request_id": "uuid",
    "count": 50,
    "limit": 50,
    "has_more": true,
    "next_cursor": "uuid"
  }
}

Error — 4xx / 5xx

{
  "error": {
    "code": "insufficient_scope",
    "message": "Requires products:write scope",
    "request_id": "uuid"
  }
}

Pagination

All list endpoints use cursor-based pagination. Default limit is 50, maximum is 100. Pass after=<next_cursor> to fetch the next page. When has_more is false, you have reached the end.

# First page
GET /api/v1/payments?limit=100

# Next page
GET /api/v1/payments?limit=100&after=<next_cursor from meta>

# With filters
GET /api/v1/payments?status=confirmed&currency=USDC&limit=50

Webhooks

HashPrism sends signed HTTP POST requests to your endpoint when payment and refund events occur on-chain. Webhooks are the recommended way to trigger fulfillment — they are lower latency and more reliable than polling the REST API.

Setup

  1. Go to Dashboard → Settings → Webhooks
  2. Add your HTTPS endpoint URL (up to 5 endpoints per account)
  3. Copy the webhook secret shown at creation — it is only displayed once
  4. Click Test to send a sample payload and confirm your endpoint is reachable
  5. Toggle endpoints active or inactive without deleting them

Request Format

Every webhook is an HTTP POST with JSON body and the following headers:

Content-Type: application/json
X-HashPrism-Event: payment.confirmed
X-HashPrism-Signature: t=1743516000,v1=a3f2e1...

Retry Behavior

HashPrism retries failed deliveries 3 times with exponential backoff (immediate, 1s, 2s). Your endpoint must return a 2xx status within 10 seconds. Delivery history and HTTP response codes are logged in Settings → Webhooks. Events are not re-queued after all attempts fail.

Best Practices

Respond immediately, process asynchronously

Return 200 as fast as possible, then queue the work. Doing database writes or sending emails inline risks missing the 10-second window and triggering a retry. Use a job queue or background function.

Handle duplicates idempotently

Network failures can cause the same event to be delivered more than once. Use payment_id or refund_id as an idempotency key — check whether you have already processed the event before acting on it.

Always verify the signature

Treat any request to your webhook URL as untrusted until you have validated the HMAC. Reject requests missing the signature header, with malformed signatures, or with timestamps older than 5 minutes.

Use the raw request body

Compute the HMAC over the raw body bytes, not a parsed or re-serialized version. Parsing and re-serializing JSON can change whitespace and key ordering, causing verification to fail even on legitimate payloads.

Events

payment.confirmedFired when a payment transaction is verified on-chain
{
  "event": "payment.confirmed",
  "data": {
    "payment_id":   "3f2a1c8e-...",       // UUID — use as idempotency key
    "product_id":   "7b8d3e2f-...",       // UUID of the purchased product
    "product_name": "My eBook",
    "product_slug": "my-ebook",
    "product_type": "digital",           // digital | physical | service | tip

    "currency":       "USDC",            // USDC | SOL
    "amount_crypto":  9.99,              // total amount sent by buyer
    "price_usd":      9.99,              // USD value at time of payment
    "platform_fee":   0.098901,          // 0.99% deducted by HashPrism
    "creator_amount": 9.891099,          // amount_crypto - platform_fee

    "tx_signature": "5yYZ1km...",        // Solana transaction signature
    "buyer_wallet": "BuyerPubkey...",    // buyer's Solana wallet address

    "buyer_email": "buyer@example.com",  // null if not collected at checkout
    "buyer_name":  "Jane Doe"            // null if not collected at checkout
  },
  "timestamp": "2026-04-01T14:30:00.000Z"
}

buyer_email and buyer_name are null for tip products or products with buyer info collection disabled.

The Solana transaction always contains exactly two instructions: creator transfer (creator_amount) and platform fee transfer (platform_fee). Both succeed or neither does — atomic by design.

refund.confirmedFired when a refund transaction is verified on-chain
{
  "event": "refund.confirmed",
  "data": {
    "refund_id":    "9c4b2d1a-...",       // UUID of the refund record
    "payment_id":   "3f2a1c8e-...",       // UUID of the original payment
    "product_id":   "7b8d3e2f-...",
    "product_name": "My eBook",
    "product_slug": "my-ebook",
    "product_type": "digital",

    "currency":       "USDC",
    "amount_crypto":  9.891099,           // amount the creator received originally
    "refund_fee":     0.097922,           // 0.99% of creator_amount
    "buyer_receives": 9.793177,           // amount_crypto - refund_fee

    "tx_signature": "8aBC2zk...",
    "buyer_wallet": "BuyerPubkey...",
    "buyer_email":  "buyer@example.com"   // null if not available
  },
  "timestamp": "2026-04-01T15:00:00.000Z"
}

buyer_receives = amount_crypto - refund_fee. The original platform fee from the purchase is not refunded. A separate 0.99% refund fee is charged on the creator's received amount, paid from the creator's wallet at signing time.

testSent when you click Test in Settings — use to verify endpoint connectivity
{
  "event": "test",
  "data": { "message": "HashPrism webhook test — your endpoint is working." },
  "timestamp": "2026-04-01T15:00:00.000Z"
}

Signature Verification

Every webhook includes an X-HashPrism-Signature header in the format t=<unix_timestamp>,v1=<hex_digest>. The HMAC-SHA256 digest is computed over {timestamp}.{raw_body} using your webhook secret. The timestamp lets you reject replayed requests.

Node.js — Production-ready handler

import crypto from 'crypto'

// Next.js App Router example — works with any framework
export async function POST(req: Request) {
  // 1. Read raw body before any parsing
  const rawBody = await req.text()
  const sigHeader = req.headers.get('x-hashprism-signature') ?? ''

  // 2. Parse the signature header
  const timestamp = sigHeader.match(/t=(\d+)/)?.[1]
  const receivedHex = sigHeader.match(/v1=([a-f0-9]+)/)?.[1]

  if (!timestamp || !receivedHex) {
    return new Response('Bad signature format', { status: 400 })
  }

  // 3. Reject replays older than 5 minutes
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10))
  if (age > 300) {
    return new Response('Timestamp expired', { status: 400 })
  }

  // 4. Compute expected HMAC
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(`${timestamp}.${rawBody}`)  // signed payload = timestamp + "." + body
    .digest('hex')

  // 5. Constant-time comparison — prevents timing attacks
  const valid = crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(receivedHex)
  )
  if (!valid) return new Response('Unauthorized', { status: 401 })

  // 6. Parse and dispatch — respond immediately, process async
  const payload = JSON.parse(rawBody)

  if (payload.event === 'payment.confirmed') {
    const { payment_id, product_id, product_type, buyer_email, buyer_name,
            amount_crypto, currency, buyer_wallet } = payload.data

    // Check idempotency before acting
    const alreadyProcessed = await db.payments.exists({ hashprismId: payment_id })
    if (!alreadyProcessed) {
      await queue.push('fulfill', { payment_id, product_id, product_type,
                                    buyer_email, buyer_name, currency,
                                    amount_crypto, buyer_wallet })
    }
  }

  if (payload.event === 'refund.confirmed') {
    const { payment_id, buyer_email, buyer_receives, currency } = payload.data
    await queue.push('revoke-access', { payment_id, buyer_email, buyer_receives, currency })
  }

  return new Response('OK', { status: 200 })
}

Python (Flask / FastAPI)

import hmac, hashlib, re, time, os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']

def verify_hashprism_signature(raw_body: bytes, sig_header: str) -> bool:
    ts_match = re.search(r't=(\d+)', sig_header)
    hex_match = re.search(r'v1=([a-f0-9]+)', sig_header)
    if not ts_match or not hex_match:
        return False

    timestamp = int(ts_match.group(1))
    received_hex = hex_match.group(1)

    # Reject replays older than 5 minutes
    if abs(time.time() - timestamp) > 300:
        return False

    signed_payload = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(WEBHOOK_SECRET.encode(), signed_payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received_hex)

@app.route('/webhook/hashprism', methods=['POST'])
def handle_webhook():
    raw_body = request.get_data()
    sig_header = request.headers.get('X-HashPrism-Signature', '')

    if not verify_hashprism_signature(raw_body, sig_header):
        abort(401)

    payload = request.get_json()
    event = payload['event']
    data = payload['data']

    if event == 'payment.confirmed':
        # Queue fulfillment asynchronously
        queue.enqueue('fulfill', payment_id=data['payment_id'],
                      buyer_email=data['buyer_email'])

    return 'OK', 200

Use the raw body bytes

Always compute the HMAC over the raw request body — not a parsed or re-serialized version. Most frameworks buffer the body before routing. Read it once as bytes/text before calling your JSON parser.

REST API Reference

Base URL: https://hashprism.com/api/v1. The /api/v1/ prefix is a permanent public contract — it will never change or be removed. New fields may be added to responses (additive changes). Removing fields or changing status codes requires a new major version.

Authenticated Endpoints

Accept Authorization: Bearer hp_xxx or a session cookie. Responses use the standard envelope.

MethodEndpoint
Products
GET/products
POST/products
GET/products/[id]
PATCH/products/[id]
DELETE/products/[id]
Payments
GET/payments
GET/payments/[id]
DELETE/payments/[id]
Refunds
POST/refunds
GET/refunds/[id]
API Key Management — session only
GET/keys
POST/keys
DELETE/keys/[id]

Public Endpoints

No authentication required. These serve buyer-facing checkout flows and public storefronts.

MethodEndpoint
Catalog
GET/catalog/[username]
GET/catalog/[username]/[slug]
Checkout Protocol (buyer-initiated)
POST/payments/create
GET/payments/status
GET/payments/solana
POST/payments/solana
POST/payments/expire
Refund Signing Protocol
POST/refunds/solana
GET/refunds/solana
GET/refunds/status

Agent Guide

For AI Agents

This section describes how AI agents should interact with HashPrism. It covers what you can and cannot do, recommended patterns, and important constraints to be aware of.

Mental model

HashPrism is a product hosting platform that uses the Solana Pay protocol as its settlement rail. A transaction happens when a buyer or agent holding a funded Solana wallet signs and broadcasts a transaction directly to a creator or agent's wallet. HashPrism does not custody funds, process payments, or act as an intermediary — it hosts the product catalog and verifies on-chain settlement. You, as an agent acting on behalf of a seller, observe what has settled and react to it. You read data, manage the catalog, and initiate refunds. You do not create transactions on behalf of another party — the signing wallet must belong to you.

Recommended Patterns

Pattern 1: Payment monitoring and sync

Poll or react to webhooks and sync confirmed payments to an external system (database, spreadsheet, CRM, accounting software).

// Fetch all confirmed payments, paginate until done
async function syncAllPayments(apiKey: string) {
  const headers = { Authorization: `Bearer ${apiKey}` }
  let cursor: string | null = null

  do {
    const url = new URL('https://hashprism.com/api/v1/payments')
    url.searchParams.set('status', 'confirmed')
    url.searchParams.set('limit', '100')
    if (cursor) url.searchParams.set('after', cursor)

    const res = await fetch(url, { headers })
    const { data, meta } = await res.json()

    for (const payment of data) {
      await upsert('payments', {
        id: payment.id,
        product: payment.products?.name,
        amount: payment.amount_crypto,
        currency: payment.currency,
        buyer: payment.buyer_info?.email,
        confirmed_at: payment.created_at,
      })
    }

    cursor = meta.has_more ? meta.next_cursor : null
  } while (cursor)
}

Required scope: payments:read

Pattern 2: Automated refund after condition

Detect a condition (complaint, chargeback request, policy violation) and initiate a refund programmatically. The creator still signs the Solana transaction — this call only creates the pending refund record.

async function initiateRefund(apiKey: string, paymentId: string) {
  const res = await fetch('https://hashprism.com/api/v1/refunds', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ paymentId }),
  })

  const { data, error } = await res.json()

  if (error?.code === 'conflict') {
    // Refund already pending for this payment — idempotent, safe to ignore
    return
  }

  if (!res.ok) throw new Error(error?.message ?? 'Refund failed')

  // Refund is now pending. Creator must sign the transaction in the dashboard.
  // A refund.confirmed webhook fires when the on-chain tx is verified.
  console.log('Refund initiated:', data.id, 'status:', data.status)
}

Required scope: refunds:write

Pattern 3: Catalog management

Create, update, or remove products in response to external triggers — inventory changes, pricing updates, content schedules.

// Update price of an existing product
async function updatePrice(apiKey: string, productId: string, newPriceUsd: number) {
  const res = await fetch(`https://hashprism.com/api/v1/products/${productId}`, {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ price_usd: newPriceUsd }),
  })

  if (!res.ok) {
    const { error } = await res.json()
    throw new Error(`${error.code}: ${error.message} (request_id: ${error.request_id})`)
  }

  const { data } = await res.json()
  return data
}

// Create a new product
async function createProduct(apiKey: string, walletAddress: string) {
  const res = await fetch('https://hashprism.com/api/v1/products', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Premium Template Pack',
      description: 'Figma + Framer source files',
      price_usd: 49,
      accept_usdc: true,
      accept_sol: true,
      product_type: 'digital',
      creator_wallet: walletAddress,
    }),
  })

  const { data, error } = await res.json()
  if (error?.code === 'conflict') {
    // Slug already exists — adjust the name slightly
  }
  return data
}

Required scope: products:write

Important constraints for agents

Payments are read-only from the API perspective

You cannot create, modify, or force-confirm a payment via the API. Payments are created by the checkout page on behalf of a buyer and confirmed by on-chain verification. Your integration observes and reacts.

Refunds require creator wallet signature

POST /api/v1/refunds creates a pending refund record. The creator must then open the dashboard and sign the Solana refund transaction with their connected wallet. The agent cannot sign blockchain transactions.

Session-only endpoints cannot be automated

Key management (GET/POST/DELETE /keys), webhook management, and account deletion require a live dashboard session. These cannot be called via Bearer token by design.

Use request_id for debugging

Every response includes a meta.request_id UUID. Log this alongside your own trace IDs. When reporting issues, include the request_id to allow HashPrism to locate the exact request in server logs.

Handle errors by code, not message

The error.code field is a stable machine-readable string. The error.message is human-readable and may change. Branch on code values like resource_not_found, insufficient_scope, and validation_error.

Error Codes

Branch on the machine-readable error.code field. Messages are for humans and may change.

Code
authentication_required
invalid_api_key
revoked_api_key
expired_api_key
insufficient_scope
resource_not_found
validation_error
conflict
rate_limited
internal_error

Rate Limits

Rate limits are enforced per IP address for public endpoints and per API key (or session) for authenticated endpoints. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Exceeded limits return 429 with a Retry-After header.

Public endpoints (per IP)
POST /payments/create10 / min
GET /payments/status60 / min
POST /payments/expire1 / 24h per reference
GET /refunds/status60 / min
GET /refunds/solana20 / min
POST /refunds/solana10 / min
POST /contact5 / hour
POST /webhooks/test10 / min
Authenticated endpoints (per key)
GET (all read endpoints)100 / min
POST / PATCH / DELETE10 / min