Webhooks are available on the Enterprise plan only
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 let us know!
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.
Here's an example with a Next.js API route:
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()
}
Or with a Next.js route handler (building off types defined above):
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 })
}