Claude-skill-registry front-webhook
Handle Front webhook events for the support platform. Use when implementing conversation ingestion, message handlers, Front API integrations, or webhook signature verification.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/front-webhook" ~/.claude/skills/majiayu000-claude-skill-registry-front-webhook && rm -rf "$T"
skills/data/front-webhook/SKILL.mdFront Webhook Handler
Front is the source of truth for all conversations. Every support interaction starts with a Front webhook.
⚠️ CRITICAL: Webhooks Send PREVIEWS, Not Full Data
This is the #1 gotcha with Front integration. Webhooks do NOT contain the full message body or sender email. You MUST fetch via API.
Key Insight: Webhooks Send PREVIEWS
Front webhooks send event previews, NOT full data. You get:
- IDs and
for resources_links - Basic metadata
You do NOT get:
- Full message body
- Author email address
- Conversation history
Must fetch full data via Front API using the
_links in the preview.
Front Signature Verification
Front uses a different format than Stripe-style:
// Headers from Front: // x-front-signature: base64-encoded HMAC // x-front-request-timestamp: milliseconds // x-front-challenge: present during setup only function verifyFrontSignature(timestamp: string, body: string, secret: string): string { // Format: HMAC-SHA256(timestamp:body), base64 encoded const baseString = Buffer.concat([ Buffer.from(`${timestamp}:`, 'utf8'), Buffer.from(body, 'utf8'), ]).toString() return crypto.createHmac('sha256', secret).update(baseString).digest('base64') }
Challenge-Response for Setup
When creating/updating a webhook, Front sends a validation request:
- Header:
x-front-challenge: <random-string> - Must respond with:
{"challenge": "<value>"}
if (result.challenge) { return NextResponse.json({ challenge: result.challenge }) }
Webhook Handler Pattern
export async function POST(request: NextRequest) { const payload = await request.text() const secret = process.env.FRONT_WEBHOOK_SECRET // Build headers object const headers: Record<string, string> = {} request.headers.forEach((value, key) => { headers[key] = value }) // Verify signature const result = verifyFrontWebhook(payload, headers, { secret }) if (!result.valid) { return NextResponse.json({ error: result.error }, { status: 401 }) } // Handle challenge during setup if (result.challenge) { return NextResponse.json({ challenge: result.challenge }) } // Parse and dispatch to Inngest const event = JSON.parse(payload) if (event.type === 'inbound_received') { await inngest.send({ name: SUPPORT_INBOUND_RECEIVED, data: { conversationId: event.payload.conversation.id, messageId: event.payload.target.data.id, _links: { message: event.payload.target.data._links?.self, conversation: event.payload.conversation._links?.self, }, }, }) } return NextResponse.json({ received: true }) }
Local Dev (ngrok)
For local testing, use a tunnel (ngrok) and point the Front webhook to the exact tunnel URL:
ngrok http 3000 # Use the https URL printed by ngrok as the webhook target
Do not use wildcard domains for webhook URLs.
Front Event Types (Application Webhooks)
| Webhook Event | API Event | Description |
|---|---|---|
| | Incoming message |
| | Outbound message sent |
| | Conversation archived |
| | Conversation reopened |
| / | Assignee changed |
| | Tag added |
| | Tag removed |
Fetching Full Data
const front = createFrontClient(process.env.FRONT_API_TOKEN) // Fetch full message const message = await front.getMessage(messageId) // message.body, message.author.email now available // Fetch conversation history const history = await front.getConversationMessages(conversationId)
File Locations
- Webhook handler:
apps/front/app/api/webhooks/front/route.ts - Front API client:
packages/core/src/front/client.ts - Signature verification:
packages/core/src/webhooks/verify.ts - Event types reference:
docs/FRONT-EVENTS.md
Inbox ID Conversion
Front UI URLs use decimal IDs, but the API uses base-36 with a prefix.
Formula:
inb_ + base36(decimal_id)
Example:
- UI URL:
https://app.frontapp.com/settings/tim:571743/inboxes/edit/[PHONE]/settings - Decimal ID:
[PHONE] - Base-36:
3srbb - API ID:
inb_3srbb
JavaScript conversion:
function frontInboxId(decimalId: number): string { return `inb_${decimalId.toString(36)}` } // frontInboxId([PHONE]) → "inb_3srbb"
List all inboxes via API:
curl -H "Authorization: Bearer $FRONT_API_TOKEN" "https://api2.frontapp.com/inboxes" | jq '._results[] | {id, name}'
Known Skill Recordings inboxes:
| Product | Inbox ID | |
|---|---|---|
| Total TypeScript | | [EMAIL] |
| Pro Tailwind | | [EMAIL] |
| Epic Web | | [EMAIL] |
| Testing Accessibility | | [EMAIL] |
| Just JavaScript | | [EMAIL] |
| Pro NextJS | | [EMAIL] |
| AI Hero | | [EMAIL] |
| ScriptKit | | [EMAIL] |
| Badass Courses | | [EMAIL] |
Environment Variables
FRONT_WEBHOOK_SECRET= # App signing key (32-char hex) FRONT_API_TOKEN= # API token for fetching data
Reference Docs
- Event types and payload structuredocs/FRONT-EVENTS.md
- Full integration specdocs/support-app-prd/76-front-integration.md