Skip to content
front-webhook logo

Front Webhook Handler

front-webhook

Handle Front webhook events for the support platform. Use when implementing conversation ingestion, message handlers, Front API integrations, or webhook signature verification.

SKILL.md

Full skill instructions

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 _links for 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 EventAPI EventDescription
inbound_receivedinboundIncoming message
outbound_sentoutboundOutbound message sent
conversation_archivedarchiveConversation archived
conversation_reopenedreopenConversation reopened
assignee_changedassign/unassignAssignee changed
tag_addedtagTag added
tag_removeduntagTag 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:

ProductInbox IDEmail
Total TypeScriptinb_3srbb[EMAIL]
Pro Tailwindinb_3pqh3[EMAIL]
Epic Webinb_jqs2t[EMAIL]
Testing Accessibilityinb_3bkef[EMAIL]
Just JavaScriptinb_2odqf[EMAIL]
Pro NextJSinb_43olj[EMAIL]
AI Heroinb_4bj7r[EMAIL]
ScriptKitinb_41c3r[EMAIL]
Badass Coursesinb_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 structure
  • docs/​support-app-prd/​76-front-integration.md - Full integration spec