Pryv API Platform
Keys, credits, and private AI docs in one dashboard.
Use wallet-signed management endpoints to create keys and add credits, then call the public API with Bearer keys. Everything below matches current backend contracts.
Quickstart
Purchase credits, mint a key, ship a request.
- 1Set NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_PLATFORM_URL.
- 2Create an API key (shown once).
- 3Buy credits with usdAmount and wait for balance to update.
- 4Call /v1/chat/completions with Bearer auth.
- 5Handle 402 insufficient_credits and 429 rate limits.
Hello world request
const apiKey = process.env.PRYV_API_KEY
const res = await fetch('https://api.pryv.ai/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-5.2',
messages: [{ role: 'user', content: 'Hello Pryv!' }],
stream: false
})
})
const data = await res.json()
console.log(data.choices[0].message.content)Topology
Keep the dashboard and API on separate hosts for security and CORS control.
Dashboard
<your-platform-url>
Public developer API
https://api.pryv.ai
Env config
NEXT_PUBLIC_PLATFORM_URL=<your-platform-url>
NEXT_PUBLIC_API_BASE_URL=https://api.pryv.aiEnvironments
Sandbox
Stripe test mode, separate webhook secret, sandbox base URL.
Production
Live Stripe, production base URL, separate WAF and rate limits.
Personas
Developer
Create, label, rotate keys; purchase credits; view usage; run quickstarts.
Admin (support)
View account state, assist failed payments, revoke compromised keys, monitor abuse.
Jobs to be done
- Purchase credits -> create key -> call /v1/chat/completions -> ship.
- Rotate keys, restrict usage, respond to insufficient credits.
- Inspect usage, balances, and request_id for debugging.
Platform IA
Roles are single-owner today. The wallet signing management requests is treated as the owner.
Auth model
Public API
Authorization: Bearer pryv_...
Management
walletAddress + message + signature (anti-replay)
Wallet auth fields can be passed in query params (GET) or JSON body (POST or DELETE).
Wallet signature payload
const message = buildWalletAuthMessage({ action: 'create' })
const { walletAddress, signature } = await signWalletMessage(message)
const body = {
walletAddress,
message,
signature,
name: 'Production key'
}Key concepts
userId
Salted hash of a wallet address.
Credits unit
Balance stored in microusd (1e-6 USD). $10.00 = 10_000_000.
Preview cost
PII preview charges $0.05 (50_000 microusd).
Pricing
Debits follow model prices per 1M tokens with a margin multiplier (default 1.25).
Typed fetch helper
export type ApiError = {
error: { message: string; type?: string; code?: string; details?: unknown }
}
export async function fetchJson<T>(
baseUrl: string,
path: string,
init?: RequestInit
): Promise<T> {
const res = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...(init?.headers || {})
}
})
const text = await res.text()
const body = text ? (JSON.parse(text) as unknown) : null
if (!res.ok) {
const apiErr = body as ApiError | null
const msg = apiErr?.error?.message || `Request failed: ${res.status}`
const err = new Error(msg)
;(err as any).status = res.status
;(err as any).code = apiErr?.error?.code
;(err as any).details = apiErr?.error?.details
throw err
}
return body as T
}Wallet signing helper
import { BrowserProvider } from 'ethers'
export async function signWalletMessage(message: string) {
const provider = new BrowserProvider((window as any).ethereum)
const signer = await provider.getSigner()
const walletAddress = await signer.getAddress()
const signature = await signer.signMessage(message)
return { walletAddress, signature }
}
export function buildWalletAuthMessage(params: {
action: 'create' | 'list' | 'revoke' | 'checkout' | 'balance' | 'ledger'
keyId?: string
}) {
const nonce = crypto.randomUUID()
const ts = new Date().toISOString()
const keyId = params.keyId ?? ''
return `Pryv API key mgmt | action=${params.action} | keyId=${keyId} | nonce=${nonce} | ts=${ts}`
}Endpoint catalog
Public and wallet-auth routes
/v1/modelsBearerList available models/v1/models/{id}BearerModel details/v1/chat/completionsBearerChat completions (SSE when stream=true)/v1/pii/previewBearerPaid preview ($0.05)/api/v1/billing/credit-packsPublicList credit packs/api/v1/billing/balanceWalletCredits balance/api/v1/billing/ledgerWalletUsage ledger/api/v1/billing/successBrowserCheckout success redirect/api/v1/billing/cancelBrowserCheckout cancel redirectChat completions
POST /v1/chat/completions
OpenAI-compatible chat completions with optional tools, URLs, PDFs, and PII controls.
- messages[].content can be a string or an array of parts (type: "text" or "image_url").
- Aliases accepted: pii_entities, pii_config, skipPiiRedaction, no_pii_redaction, noPiiRedaction.
- Optional header X-Request-Id is echoed into ledger entries for correlation.
Streaming SSE
Set stream: true to receive text/event-stream chunks. Streaming ends with [DONE].
export async function streamChatCompletion(
apiBaseUrl: string,
apiKey: string,
req: any,
onDelta: (text: string) => void
) {
const res = await fetch(`${apiBaseUrl}/v1/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...req, stream: true })
})
if (!res.ok || !res.body) {
const text = await res.text()
throw new Error(`stream failed: ${res.status} ${text}`)
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split('\n\n')
buffer = frames.pop() || ''
for (const frame of frames) {
const line = frame.trim()
if (!line.startsWith('data:')) continue
const data = line.slice('data:'.length).trim()
if (data === '[DONE]') return
const evt = JSON.parse(data)
const delta = evt?.choices?.[0]?.delta?.content
if (typeof delta === 'string' && delta) onDelta(delta)
}
}
}PII controls
Privacy tuning
Redact emails only
piiEntities: ["EMAIL_ADDRESS"]
Disable image redaction
skip_pii_redaction: { image: true }
Blur faces strongly
piiConfig.image.faces = { blur: "strong" }
PII config fields
- image.faces.* (face blur controls)
- image.scoreThreshold (OCR and PII strictness)
- pdf.scoreThreshold, pdf.maxPages
Aliases accepted: pii_entities, pii_config, skipPiiRedaction, no_pii_redaction, noPiiRedaction.
POST /v1/pii/preview
export async function piiPreview(
apiBaseUrl: string,
apiKey: string,
req: any
) {
return fetchJson(apiBaseUrl, '/v1/pii/preview', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(req)
})
}Models
GET /v1/models returns the models available to your API key. Cache for hours.
Open model catalogUse GET /v1/models/{id} for a single model details response.
List models
curl https://api.pryv.ai/v1/models \
-H "Authorization: Bearer pryv_..."Pricing and credits
Debits are based on model prices (USD per 1M tokens) with a margin multiplier. Preview charges $0.05 per call.
Error handling
Handle auth, credits, and limits
Error envelope
{
"error": {
"message": "Missing or invalid API key",
"type": "authentication_error",
"code": "invalid_api_key",
"details": { "optional": "context" }
}
}Wallet error codes: ACTION_MISMATCH, KEY_ID_MISMATCH, MISSING_NONCE_OR_TS, INVALID_TS, TS_EXPIRED, NONCE_REPLAY, SIGNATURE_INVALID, SIGNATURE_VERIFY_ERROR.
Rate limits
Public API responses include rate limit headers and Retry-After on 429.
- X-RateLimit-Limit-RPM
- X-RateLimit-Remaining-RPM
- X-RateLimit-Reset-RPM
- X-RateLimit-Limit-TPM
- X-RateLimit-Remaining-TPM
- X-RateLimit-Reset-TPM
- Retry-After
export function parseRetryAfterSeconds(res: Response): number | null {
const ra = res.headers.get('Retry-After')
if (!ra) return null
const n = Number(ra)
return Number.isFinite(n) ? n : null
}Security
- API key is shown once and cannot be recovered.
- Never commit keys; avoid localStorage by default.
- Frontend never asserts entitlement; backend is source of truth.
- Prefer POST for wallet-auth to reduce URL leakage in logs.
Operations
Caching
- Cache GET /v1/models for hours.
- Cache keys and balance for 5 to 15 seconds with manual refresh.
Retries
- Retry idempotent GETs with exponential backoff.
- Do not auto-retry key creation or checkout creation without user action.
Telemetry events
- api_keys_viewed
- api_key_created
- api_key_revoked
- billing_checkout_started
- billing_checkout_completed
- credits_balance_viewed
- insufficient_credits_error_shown
- rate_limit_error_shown
Launch checklist
Minimum QA cases
- Create key -> show once -> copy -> close -> key not retrievable.
- Revoke key -> bearer calls fail with 401.
- Checkout with usdAmount min and max boundaries.
- Credits applied after webhook; polling balance updates.
- Chat completions streaming works.
- PII preview charges $0.05 and returns redacted content.
- Insufficient credits returns 402 and UI reacts correctly.
- Rate limit 429 handling respects Retry-After.