front-webhook
Front 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
_linksfor resources - 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 |
|---|---|---|
inbound_received |
inbound |
Incoming message |
outbound_sent |
outbound |
Outbound message sent |
conversation_archived |
archive |
Conversation archived |
conversation_reopened |
reopen |
Conversation reopened |
assignee_changed |
assign/unassign |
Assignee changed |
tag_added |
tag |
Tag added |
tag_removed |
untag |
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 | inb_3srbb |
[EMAIL] |
| Pro Tailwind | inb_3pqh3 |
[EMAIL] |
| Epic Web | inb_jqs2t |
[EMAIL] |
| Testing Accessibility | inb_3bkef |
[EMAIL] |
| Just JavaScript | inb_2odqf |
[EMAIL] |
| Pro NextJS | inb_43olj |
[EMAIL] |
| AI Hero | inb_4bj7r |
[EMAIL] |
| ScriptKit | inb_41c3r |
[EMAIL] |
| Badass Courses | inb_3mn7r |
[EMAIL] |
Environment Variables
FRONT_WEBHOOK_SECRET= # App signing key (32-char hex)
FRONT_API_TOKEN= # API token for fetching data
Reference Docs
docs/FRONT-EVENTS.md- Event types and payload structuredocs/support-app-prd/76-front-integration.md- Full integration spec