ML-DSA-65 · NIST FIPS 204 · Quantum Resistant

Developer Guide  ·  SDK Reference  ·  API Reference  ·  v2.0.0
Base URL: https://pqauth-core.gdbok.workers.dev

Contents
00 — Setup & environment SDK 01 — Installation SDK 02 — sign() SDK 03 — verify() remote SDK 04 — verify() local / offline SDK 05 — revoke() SDK 06 — middleware() Express SDK 07 — usage() SDK 08 — webhooks SDK 09 — Error handling SDK 10 — Constructor options REST 01 — Health check REST 02 — Public key REST 03 — Sign REST 04 — Verify REST 05 — Revoke REST 06 — Usage REST 07 — Complete script REST 08 — Token costs REST 09 — Common errors
00 Environment variables

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"
Where do I get my API key? Dashboard → your project → expand the card → + New key. The key is shown only once at creation time. If you lose it, create a new one and revoke the old one.
── SDK reference ─────────────────────────────────────────────────────────
SDK 01 Installation

Works in Node.js, Deno, Cloudflare Workers, and the browser. TypeScript types included — no separate @types package needed.

Install
npm install pqauth-sdk
Get your API key
1. Create a free account at pqauth-dashboard.pages.dev — enter your email and verify the OTP sent to your inbox.

2. In the dashboard, create a project, then create an API key inside that project.

3. Save the key immediately — it is shown only once. Store it in an environment variable, never in source code.
Instantiate the client
// 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
})
SDK 02 sign() — Sign anything

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.

User session
const { token, usage } = await pq.sign({
  sub:              'user_123',
  email:            '[email protected]',
  role:             'admin',
  expiresInSeconds: 3600,    // optional — default: 1 hour
})
Payment order
const { token } = await pq.sign({
  sub:      'order_456',
  amount:   299.99,
  currency: 'USD',
  expiresInSeconds: 300,     // 5 minutes — short-lived payment intent
})
Document certification
const { token } = await pq.sign({
  sub:      'doc_789',
  hash:     'sha256:abc...',
  signedBy: 'alice',
  // No expiresInSeconds — document signatures don't expire
})
IoT device / firmware
const { token } = await pq.sign({
  sub:      'device_iot_001',
  firmware: '2.1.4',
  location: 'plant-A',
})
Response — token object
{
  token: {
    payload: "eyJzdWIi...", // base64 encoded payload
    signature: "oi5UKsTn...", // ML-DSA-65 signature
    algorithm: "ML-DSA-65",
    issuedAt: 1778947233
  },
  usage: {
    freeRemaining: 9999,
    packRemaining: 0,
    totalRemaining: 9999
  }
}
Monitor quota inline
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`)
SDK 03 verify() — Remote verification

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
Invalid / revoked / tampered token
const { valid, error } = await pq.verify(token)
// valid: false
// error: "Token has been revoked" | "Token expirado..." | "Firma inválida..."
verify() never throws. On any failure it returns { valid: false, error: "..." }. Your code should always check valid before using payload.
SDK 04 verify() local — Offline, ~1ms

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
Important: Local verification does not check the revocation list. A revoked token will pass local verification if its signature is valid and it has not expired. Use remote verification (localVerify: false) for payments, admin actions, and any security-sensitive operation.
Key rotation: When the server rotates keys, the SDK automatically detects the mismatch, refreshes the cached public key, and retries — no action needed on your end.
SDK 05 revoke() — Revoke a token

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
SDK 06 middleware() — Express / Fastify

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.

Full Express example — login, logout, protected routes
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)
Token format for the Authorization header: The token object returned by 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.
SDK 07 usage() — Token balance

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}`)
})
Free tokens reset on the 1st of each month (UTC). Unused free tokens do not carry over.
Pack tokens never expire and accumulate across purchases. They are consumed after the monthly free tokens are exhausted.
SDK 08 webhooks — Real-time notifications

Receive HTTP POST events when tokens are signed, rejected, or revoked, and when your quota is running low or exhausted.

Available events
token.signed  ·  token.rejected  ·  token.revoked
limit.warning  ·  limit.reached
Register, test, inspect, and delete
// 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()
Verifying incoming webhook requests (Express)
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')
})
Store the webhook secret securely. It is shown only once at registration time. If you lose it, delete the webhook and register a new one.
SDK 09 Error handling

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)
  }
}
SDK 10 Constructor options — full reference

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)
})
OptionTypeDefaultDescription
apiKeystringRequired. API key from the dashboard (starts with pqa_)
baseUrlstringProduction URLOverride for local development or self-hosted instances
timeoutnumber10000Request timeout in milliseconds. Throws TIMEOUT on exceeded.
localVerifybooleanfalseWhen true, verify() runs in memory using the cached public key. No API call, no token cost. Does not check revocation.
── REST API reference (curl) ──────────────────────────────────────────────
01 Check service status

Public endpoint. No authentication. No token cost. Confirm the service is operational before testing.

curl -s $BASE_URL/health | jq
Expected response
{
  "success": true,
  "status": "ok",
  "algorithm": "ML-DSA-65",
  "quantumResistant": true,
  "version": "2.0.0"
}
02 Get the public key (GET /public-key)

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
Expected response
{
  "success": true,
  "publicKey": "base64encodedkey...",
  "algorithm": "ML-DSA-65"
}
Usage: Store this public key on your server to verify tokens locally without calling the API. See the SDK's localVerify: true option.
03 Sign a payload (POST /sign)

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.

Case 1 — User session
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
Case 2 — Payment order
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
Case 3 — Document certification
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 token for the next steps
# 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
Expected response
{
  "success": true,
  "token": {
    "payload": "eyJzdWIi...",
    "signature": "oi5UKsTn...",
    "algorithm": "ML-DSA-65",
    "issuedAt": 1778947233
  },
  "meta": { "tokenCost": 1, "source": "free" },
  "usage": {
    "freeRemaining": 9999,
    "packRemaining": 0,
    "totalRemaining": 9999
  }
}
meta.source can be "free", "pack", or "free+pack" — indicating which token pool was consumed.
04 Verify a token (POST /verify)

Requires X-API-Key. Cost: 1 token. Verifies the ML-DSA-65 signature, token expiry, and revocation status.

Verify a valid token (use $TOKEN from the previous step)
curl -s -X POST $BASE_URL/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d "{\"token\": $TOKEN}" | jq
Expected response — valid token
{
  "success": true,
  "valid": true,
  "payload": {
    "sub": "user_test",
    "email": "[email protected]",
    "iat": 1778947233,
    "exp": 1778950833
  }
}
Test with a tampered token
# 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
Expected response — invalid token
{
  "success": false,
  "valid": false,
  "error": "Firma inválida — el token fue alterado o no fue emitido por este servidor"
}
Note: The error message is currently returned in Spanish by the backend. This will be internationalized in a future release.
05 Revoke a token (POST /revoke)

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.

Revoke
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
Expected response
{
  "success": true,
  "message": "Token revoked successfully",
  "sub": "user_test"
}
Confirm the revoked token is rejected
# 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
{
  "success": false,
  "valid": false,
  "error": "Token has been revoked"
}
06 Check token usage (GET /usage)

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
Expected response
{
  "current": {
    "month": "2026-05",
    "freeUsed": 6,
    "freeRemaining": 9994,
    "freeLimit": 10000,
    "packRemaining": 0,
    "totalRemaining": 9994
  },
  "monthlyHistory": [...],
  "packs": [...]
}
07 Complete test script

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
Expected output: health "ok" → valid true → valid false (tampered) → revoke OK → valid false (revoked) → balance with 5 fewer tokens.
08 Token costs and limits

Quick reference of token consumption per endpoint.

EndpointCostAuthDescription
POST /sign1 tokenX-API-KeySign any payload
POST /verify1 tokenX-API-KeyVerify signature and expiry
POST /revoke1 tokenX-API-KeyPermanently revoke a token
GET /usageFreeX-API-KeyBalance and 6-month history
GET /public-keyFreePublic key for offline verification
GET /healthFreeService status
Free tier: 10,000 tokens/month. Reset on the 1st of each month (UTC). Unused tokens do not carry over.
Pack tokens: Never expire. Consumed after the monthly free tokens are exhausted.
meta.source in the response: "free" = from free tier · "pack" = from a purchased pack · "free+pack" = consumed from both in a single operation.
09 Common errors

Error reference for testing and debugging.

HTTPErrorCause
401API key required or invalidMissing or incorrect X-API-Key header
401Token has been revokedToken was previously revoked
401Firma inválida...Token was tampered with or not issued by this server
401Token expirado hace N segundosToken exceeded the expiresInSeconds defined at signing
429Token limit reachedFree and pack tokens exhausted
400"sub" is requiredMissing sub field in the /sign request body
Important: Store your API key securely. It cannot be recovered after the creation window closes. If lost, create a new key and revoke the old one from the dashboard.