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.
HashPrism exposes two integration surfaces. They serve different purposes and are designed to be used together.
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.
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.
Get from zero to a working integration in five minutes.
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.
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
}
}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.
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 })
}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.
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.
| Status | Meaning |
|---|---|
| pending | Payment record created. Awaiting buyer to sign and broadcast the Solana transaction. |
| confirmed | Transaction verified on-chain. Correct amounts sent to correct wallets. payment.confirmed webhook dispatched. |
| expired | Buyer did not complete payment within 10 minutes. Checkout timed out. |
| failed | Transaction landed on-chain but was reverted. Funds were not transferred. |
| refunded | Refund transaction verified on-chain. Buyer received funds. refund.confirmed webhook dispatched. |
Refunds have their own status independent of the payment. A refund can only be initiated on a confirmed payment. The refund record moves through pending → confirmed or failed. The parent payment moves to refunded only after the refund is confirmed on-chain.
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).
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.
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.
| Scope | Grants access to |
|---|---|
| payments:read | List and retrieve payments and refunds |
| products:read | List and retrieve products |
| products:write | Create, update, and delete products |
| refunds:write | Initiate 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.
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"
}
}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¤cy=USDC&limit=50
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.
Every webhook is an HTTP POST with JSON body and the following headers:
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.
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.
{
"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.
{
"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.
{
"event": "test",
"data": { "message": "HashPrism webhook test — your endpoint is working." },
"timestamp": "2026-04-01T15:00:00.000Z"
}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.
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 })
}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', 200Use 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.
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.
Accept Authorization: Bearer hp_xxx or a session cookie. Responses use the standard envelope.
| Method | Endpoint | ||
|---|---|---|---|
| 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] | ||
No authentication required. These serve buyer-facing checkout flows and public storefronts.
| Method | Endpoint | |
|---|---|---|
| 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 | |
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.
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.
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
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
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
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.
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 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.
Solana Blinks enable shareable, embeddable payment actions directly in social feeds, emails, and any web surface — no checkout page required. HashPrism's existing /api/v1/payments/solana endpoints are already ~90% compatible with the Blinks specification. Full Blinks support is on the roadmap.