From persona-pack
Implements webhook handlers for Persona identity verification events with HMAC signature verification, event processing for status changes, and database updates in Node.js/Express.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin persona-packThis skill is limited to using the following tools:
HMAC signature verification, inquiry.completed/approved/declined events, idempotent processing.
Secures Persona API keys via rotation and env vars, verifies webhook HMAC, encrypts PII at rest, and sets up audit logging for identity verification.
Sets up Clerk webhook endpoints in Next.js to verify signatures and handle auth events for user sync using @clerk/backend or Svix.
Generates complete, verified Next.js webhook handlers for Clerk events including user create/update/delete and organization membership. Enables database sync, notifications, integrations.
Share bugs, ideas, or general feedback.
HMAC signature verification, inquiry.completed/approved/declined events, idempotent processing.
persona-install-auth setup1. Dashboard > Settings > Webhooks > Add Webhook
2. URL: https://your-app.com/webhooks/persona
3. Events: inquiry.completed, inquiry.approved, inquiry.declined,
verification.passed, verification.failed
4. Copy the webhook secret for signature verification
import express from 'express';
import crypto from 'crypto';
const app = express();
app.post('/webhooks/persona',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['persona-signature'] as string;
const secret = process.env.PERSONA_WEBHOOK_SECRET!;
// Verify HMAC-SHA256 signature
const expectedSig = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature || ''), Buffer.from(expectedSig))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
await handlePersonaEvent(event);
res.status(200).json({ received: true });
}
);
async function handlePersonaEvent(event: any) {
const { type, data } = event;
switch (type) {
case 'inquiry.completed':
const inquiryId = data.attributes.payload.data.id;
const referenceId = data.attributes.payload.data.attributes['reference-id'];
console.log(`Inquiry completed: ${inquiryId} for user ${referenceId}`);
// Update user KYC status in your database
await updateUserKycStatus(referenceId, 'completed');
break;
case 'inquiry.approved':
await updateUserKycStatus(data.attributes.payload.data.attributes['reference-id'], 'approved');
break;
case 'inquiry.declined':
await updateUserKycStatus(data.attributes.payload.data.attributes['reference-id'], 'declined');
break;
case 'verification.passed':
console.log(`Verification passed: ${data.attributes.payload.data.id}`);
break;
case 'verification.failed':
console.log(`Verification failed: ${data.attributes.payload.data.id}`);
break;
default:
console.log(`Unhandled event: ${type}`);
}
}
const processedEvents = new Set<string>();
async function idempotentHandle(event: any) {
const eventId = event.data.id;
if (processedEvents.has(eventId)) {
console.log(`Skipping duplicate: ${eventId}`);
return;
}
await handlePersonaEvent(event);
processedEvents.add(eventId);
}
| Issue | Cause | Solution |
|---|---|---|
| Invalid signature | Wrong webhook secret | Re-copy secret from Dashboard |
| Missing events | Events not selected | Check webhook configuration |
| Duplicate processing | Retry delivery | Use event ID deduplication |
For common errors, see persona-common-errors.