Developer Guide · SDK Reference · API Reference · v2.0.0
Base URL: https://pqauth-core.gdbok.workers.dev
Set these in your terminal before running any REST/curl command in this guide. If you are using the SDK you only need your API key.
# Replace with your API key from the dashboard (starts with pqa_) export API_KEY="pqa_YOUR_API_KEY_HERE" export BASE_URL="https://pqauth-core.gdbok.workers.dev"
Works in Node.js, Deno, Cloudflare Workers, and the browser. TypeScript types included — no separate @types package needed.
npm install pqauth-sdk
// Simple form — just the API key import { PQAuth } from 'pqauth-sdk' const pq = new PQAuth('pqa_your_api_key') // Or with all options (see SDK 10 for full reference) const pq = new PQAuth({ apiKey: 'pqa_your_api_key', localVerify: false, // set to true for offline verification (SDK 04) timeout: 10_000, // request timeout in ms, default 10000 })
Signs any payload with ML-DSA-65. The only required field is sub — any string identifying the entity. All other fields are stored in the payload and returned on verify. Cost: 1 token.
const { token, usage } = await pq.sign({ sub: 'user_123', email: '[email protected]', role: 'admin', expiresInSeconds: 3600, // optional — default: 1 hour })
const { token } = await pq.sign({ sub: 'order_456', amount: 299.99, currency: 'USD', expiresInSeconds: 300, // 5 minutes — short-lived payment intent })
const { token } = await pq.sign({ sub: 'doc_789', hash: 'sha256:abc...', signedBy: 'alice', // No expiresInSeconds — document signatures don't expire })
const { token } = await pq.sign({ sub: 'device_iot_001', firmware: '2.1.4', location: 'plant-A', })
const { token, usage } = await pq.sign({ sub: 'user_123' }) console.log(`${usage.freeRemaining} free tokens remaining this month`) console.log(`${usage.packRemaining} pack tokens remaining`) console.log(`${usage.totalRemaining} total remaining`)
Verifies the ML-DSA-65 signature, token expiry, and the revocation list. Never throws — always returns an object. Cost: 1 token. Use this for sensitive operations (payments, admin actions).
const { valid, payload } = await pq.verify(token) if (!valid) { return res.status(401).json({ error: 'Unauthorized' }) } // payload contains sub + all custom fields you passed to sign() console.log(payload.sub) // 'user_123' console.log(payload.role) // 'admin' console.log(payload.exp) // expiry Unix timestamp console.log(payload.iat) // issued at Unix timestamp
const { valid, error } = await pq.verify(token) // valid: false // error: "Token has been revoked" | "Token expirado..." | "Firma inválida..."
{ valid: false, error: "..." }. Your code should always check valid before using payload.
Enable localVerify: true to verify tokens entirely in memory using the cached public key — no API call, no network latency, no token cost. Ideal for high-throughput read endpoints.
const pq = new PQAuth({ apiKey: 'pqa_your_api_key', localVerify: true, }) // Optional: preload the public key at startup to avoid first-request latency await pq.preloadPublicKey() const { valid, payload, local } = await pq.verify(token) console.log(local) // true — verified without an API call
localVerify: false) for payments, admin actions, and any security-sensitive operation.
Immediately and permanently invalidates a token. Future verify() calls will reject it even if the signature is still valid and the token has not expired. Cost: 1 token.
// Revoke with a reason (optional string) await pq.revoke(token, 'user logged out') await pq.revoke(token, 'order cancelled') await pq.revoke(token, 'suspicious activity detected') // After revoke, any verify() returns { valid: false, error: 'Token has been revoked' } const { valid } = await pq.verify(token) console.log(valid) // false
Reads the Authorization: Bearer <token> header, verifies the token, and attaches the decoded payload to req.user. Returns 401 automatically on invalid tokens. Node.js only.
import express from 'express' import { PQAuth } from 'pqauth-sdk' const app = express() const pq = new PQAuth('pqa_your_api_key') app.use(express.json()) // Login — sign a token and return it to the client app.post('/login', async (req, res) => { const user = await db.users.findByEmail(req.body.email) if (!user || !checkPassword(req.body.password, user.passwordHash)) { return res.status(401).json({ error: 'Invalid credentials' }) } const { token } = await pq.sign({ sub: user.id, email: user.email, role: user.role, expiresInSeconds: 3600, }) res.json({ token }) }) // Logout — revoke the token immediately app.post('/logout', async (req, res) => { const token = getTokenFromRequest(req) // your helper to extract token from header if (token) await pq.revoke(token, 'user logged out') res.json({ success: true }) }) // Protect all routes under /api with the PQSign middleware app.use('/api', pq.middleware()) // req.user is the verified payload — sub, email, role, exp, iat, etc. app.get('/api/profile', (req, res) => { res.json({ user: req.user }) }) app.listen(3000)
sign() is a JSON object {"payload":"...","signature":"...",...}. Encode it to base64 before putting it in the Bearer header, and decode on the server before passing to verify(). The middleware handles this automatically.
Returns the current token balance, 6-month usage history, and purchased packs. No token cost.
const { current, monthlyHistory, packs } = await pq.usage() // Current balance console.log(`Free: ${current.freeRemaining} / ${current.freeLimit}`) console.log(`Pack: ${current.packRemaining}`) console.log(`Total: ${current.totalRemaining}`) // 6-month history monthlyHistory.forEach(({ month, tokensUsed, fromFree, fromPack }) => { console.log(`${month}: ${tokensUsed} used (${fromFree} free + ${fromPack} pack)`) }) // Purchased packs packs.forEach(({ packType, tokensPurchased, purchasedAt }) => { const date = new Date(purchasedAt * 1000).toLocaleDateString() console.log(`${packType}: ${tokensPurchased} tokens — ${date}`) })
Receive HTTP POST events when tokens are signed, rejected, or revoked, and when your quota is running low or exhausted.
// Register a webhook const { webhook } = await pq.webhooks.register({ url: 'https://yourapp.com/webhooks/pqsign', events: ['limit.warning', 'limit.reached', 'token.revoked'], }) // Store webhook.secret securely — shown only once, used to verify incoming requests console.log(webhook.secret) // Send a test event to confirm your endpoint is reachable await pq.webhooks.test() // Get current webhook config (does not return the secret) const { webhook: config } = await pq.webhooks.get() // Delete the webhook await pq.webhooks.delete()
import crypto from 'crypto' app.post('/webhooks/pqsign', express.json(), (req, res) => { const sig = req.headers['x-pqauth-signature'] const expected = 'sha256=' + crypto .createHmac('sha256', process.env.PQSIGN_WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest('hex') if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).send('Invalid signature') } const { event, data } = req.body switch (event) { case 'limit.warning': console.warn(`Only ${data.freeRemaining} free tokens left this month`) break case 'limit.reached': console.error(`Limit reached — pack remaining: ${data.packRemaining}`) break case 'token.revoked': console.log(`Token revoked — sub: ${data.sub}`) break } res.status(200).send('ok') })
verify() never throws — it always returns { valid, payload } or { valid: false, error }. All other methods (sign, revoke, usage, webhooks.*) throw PQAuthError on failure.
import { PQAuth, PQAuthError } from 'pqauth-sdk' try { await pq.sign({ sub: 'user_123' }) } catch (err) { if (err instanceof PQAuthError) { switch (err.code) { case 'INVALID_API_KEY': // bad or missing API key break case 'API_ERROR': // server returned an error (check err.status) break case 'TIMEOUT': // request exceeded timeout (default: 10s) break case 'NETWORK_ERROR': // connection failed break case 'MISSING_SUB': // sign() called without sub field break case 'INVALID_SIGNATURE': // local verify: token tampered break case 'TOKEN_EXPIRED': // local verify: token expired break case 'UNSUPPORTED_ALGORITHM': // local verify: unknown algorithm break } console.error(err.code, err.message, err.status) } }
All available options when instantiating PQAuth.
const pq = new PQAuth({ apiKey: 'pqa_...', // required — from the dashboard baseUrl: 'https://pqauth-core.gdbok.workers.dev', // optional — override for self-hosting timeout: 10_000, // optional — request timeout in ms (default: 10000) localVerify: false, // optional — enable offline verification (default: false) })
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | — | Required. API key from the dashboard (starts with pqa_) |
| baseUrl | string | Production URL | Override for local development or self-hosted instances |
| timeout | number | 10000 | Request timeout in milliseconds. Throws TIMEOUT on exceeded. |
| localVerify | boolean | false | When true, verify() runs in memory using the cached public key. No API call, no token cost. Does not check revocation. |
Public endpoint. No authentication. No token cost. Confirm the service is operational before testing.
curl -s $BASE_URL/health | jq
Public endpoint. No token cost. Returns the ML-DSA-65 public key for offline token verification on your own server.
curl -s $BASE_URL/public-key | jq
localVerify: true option.
Requires X-API-Key. Cost: 1 token. Signs any payload with ML-DSA-65. The sub field is the only required field — you can add any additional fields.
curl -s -X POST $BASE_URL/sign \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{"sub":"user_123","email":"[email protected]","role":"admin","expiresInSeconds":3600}' | jq
curl -s -X POST $BASE_URL/sign \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{"sub":"order_456","amount":1500.00,"currency":"USD","expiresInSeconds":300}' | jq
curl -s -X POST $BASE_URL/sign \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{"sub":"doc_789","documentHash":"sha256:abc123...","signedBy":"alice"}' | jq
# Save the complete token object to an environment variable TOKEN=$(curl -s -X POST $BASE_URL/sign \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{"sub":"user_test","email":"[email protected]"}' \ | jq -c '.token') echo $TOKEN
"free", "pack", or "free+pack" — indicating which token pool was consumed.
Requires X-API-Key. Cost: 1 token. Verifies the ML-DSA-65 signature, token expiry, and revocation status.
curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN}" | jq
# Modify the payload — the signature must fail FAKE_TOKEN=$(echo $TOKEN | jq -c '.payload = "TAMPERED_PAYLOAD"') curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $FAKE_TOKEN}" | jq
Requires X-API-Key. Cost: 1 token. Permanently and immediately invalidates the token. Any future /verify call will reject it even if the signature is valid and it has not yet expired.
curl -s -X POST $BASE_URL/revoke \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN, \"reason\": \"user logged out\"}" | jq
# This verify must return valid: false with error "Token has been revoked" curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN}" | jq
Requires X-API-Key. No token cost. Returns current free and pack token balance, 6-month history, and purchased packs.
curl -s $BASE_URL/usage \
-H "X-API-Key: $API_KEY" | jq
Copy, replace your API key, and run to test the full token lifecycle in a single command.
#!/bin/bash # PQSign — Full token lifecycle test # Usage: chmod +x test.sh && ./test.sh API_KEY="pqa_YOUR_API_KEY" BASE_URL="https://pqauth-core.gdbok.workers.dev" echo "[1/6] Health check..." curl -s $BASE_URL/health | jq .status echo "[2/6] Signing token..." TOKEN=$(curl -s -X POST $BASE_URL/sign \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d '{"sub":"user_test","email":"[email protected]","role":"user"}' \ | jq -c '.token') echo "Token signed OK" echo "[3/6] Verifying token (must be valid: true)..." curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN}" | jq .valid echo "[4/6] Tampered token (must be valid: false)..." FAKE=$(echo $TOKEN | jq -c '.payload = "FAKE"') curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $FAKE}" | jq .valid echo "[5/6] Revoking token..." curl -s -X POST $BASE_URL/revoke \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN, \"reason\": \"test\"}" | jq .message echo "[6/6] Verify revoked token (must be valid: false)..." curl -s -X POST $BASE_URL/verify \ -H "Content-Type: application/json" \ -H "X-API-Key: $API_KEY" \ -d "{\"token\": $TOKEN}" | jq .valid echo "Final token balance:" curl -s $BASE_URL/usage \ -H "X-API-Key: $API_KEY" | jq .current
Quick reference of token consumption per endpoint.
| Endpoint | Cost | Auth | Description |
|---|---|---|---|
| POST /sign | 1 token | X-API-Key | Sign any payload |
| POST /verify | 1 token | X-API-Key | Verify signature and expiry |
| POST /revoke | 1 token | X-API-Key | Permanently revoke a token |
| GET /usage | Free | X-API-Key | Balance and 6-month history |
| GET /public-key | Free | — | Public key for offline verification |
| GET /health | Free | — | Service status |
"free" = from free tier · "pack" = from a purchased pack · "free+pack" = consumed from both in a single operation.
Error reference for testing and debugging.
| HTTP | Error | Cause |
|---|---|---|
| 401 | API key required or invalid | Missing or incorrect X-API-Key header |
| 401 | Token has been revoked | Token was previously revoked |
| 401 | Firma inválida... | Token was tampered with or not issued by this server |
| 401 | Token expirado hace N segundos | Token exceeded the expiresInSeconds defined at signing |
| 429 | Token limit reached | Free and pack tokens exhausted |
| 400 | "sub" is required | Missing sub field in the /sign request body |