You can receive webhooks for some Arcade events.
Currently, only form submissions are supported. If there are more events you would like to receive via webhooks, please !
In your advanced settings, you can find your webhook secret and configure a URL to receive webhooks.
The webhook secret verifies the payload signature (see below).
Webhook URL
This is the URL where you want to receive webhooks.
Method: POST
Headers:
content-type: application/json
x-arcade-signature
HMAC-SHA256-Base64 signature
x-arcade-signature-2
(optional)
Body:
{
"type": "formSubmission.created",
// "type": "formSubmission.updated",
"timestamp": "2024-02-15T15:51:12.013Z",
"flowId": "khM1A286JbRPxN9fxYsE",
"sessionId": "b8103665-e6ce-4db8-894e-f123ec49ed9c",
"viewerIp": "127.0.0.1",
"stepTitle": "Step title (optional)",
"stepSubtitle": "Step subtitle (optional)",
"answers": {
"User Email": {
"variant": "email",
"value": "foo@bar.com"
},
"User Name": {
"variant": "text",
"value": "Foo Bar"
}
}
}
Verifying signatures
The signature is a HMAC-SHA256 of the raw HTTP body, encoded with Base64.
import crypto from 'node:crypto'
import type { NextApiRequest, NextApiResponse } from 'next'
import getRawBody from 'raw-body'
type FormSubmissionAnswer = {
variant: 'email' | 'text' | 'date' | 'hidden' | 'single-select'
value: string | number
}
type FormSubmissionEvent = {
type: 'formSubmission.created' | 'formSubmission.updated'
timestamp: string
flowId: string
sessionId: string
viewerIp: string
stepTitle?: string
stepSubtitle?: string
answers: Record<string, FormSubmissionAnswer>
}
type ArcadeEvent = FormSubmissionEvent
export const config = {
api: {
bodyParser: false,
},
}
function getHeader(headers: NextApiRequest['headers'], name: string) {
const value = headers[name]
return Array.isArray(value) ? value[0] : value
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).end()
}
const signature = getHeader(req.headers, 'x-arcade-signature')
// Optional, second signature using during signing secret rotation period
const signature2 = getHeader(req.headers, 'x-arcade-signature-2')
if (!signature && !signature2) {
return res.status(401).end()
}
const rawBody = await getRawBody(req)
const validSignature = crypto
.createHmac('sha256', process.env.ARCADE_SIGNING_SECRET!)
.update(rawBody)
.digest('base64')
if (signature !== validSignature && signature2 !== validSignature) {
return res.status(403).end()
}
const event = JSON.parse(rawBody) as ArcadeEvent
// Do something with the event
res.status(200).end()
}
import { type NextRequest, NextResponse } from 'next/server'
export async function POST (request: NextRequest): NextResponse {
const signature = req.headers.get('x-arcade-signature')
// Optional, second signature using during signing secret rotation period
const signature2 = req.headers.get('x-arcade-signature-2')
if (!signature && !signature2) {
return NextResponse.json({ error: 'Missing signature' }, { status: 401 })
}
const rawBody = await request.text()
const validSignature = crypto
.createHmac('sha256', process.env.ARCADE_SIGNING_SECRET!)
.update(rawBody)
.digest('base64')
if (signature !== validSignature && signature2 !== validSignature) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 403 })
}
const event = JSON.parse(rawBody) as ArcadeEvent
// Do something with the event
return NextResponse.json({ success: true })
}