Webhooks
Receive real-time event notifications
Webhooks allow you to receive real-time notifications when events occur in your Arky business.
Configuration
Enable Webhooks
Configure your webhook endpoint in business settings:
await sdk.business.updateBusiness({
id: 'biz_abc123',
settings: {
webhookUrl: 'https://yourapp.com/api/webhooks/arky',
webhookSecret: 'whsec_your_secret_key',
webhookEvents: [
'order.created',
'order.paid',
'order.shipped',
'reservation.created',
'reservation.confirmed',
'user.registered'
]
}
});
Available Events
Orders
| Event | Description |
|---|---|
order.created | New order placed |
order.paid | Payment successful |
order.shipped | Order marked as shipped |
order.delivered | Order marked as delivered |
order.cancelled | Order cancelled |
order.refunded | Order refunded |
Reservations
| Event | Description |
|---|---|
reservation.created | New booking created |
reservation.confirmed | Booking confirmed/paid |
reservation.cancelled | Booking cancelled |
reservation.completed | Appointment completed |
reservation.noshow | Customer no-show |
Users
| Event | Description |
|---|---|
user.registered | New user signup |
user.confirmed | Email confirmed |
user.updated | Profile updated |
Content
| Event | Description |
|---|---|
node.created | Content created |
node.updated | Content updated |
node.published | Content published |
node.deleted | Content deleted |
| Event | Description |
|---|---|
email.sent | Email sent |
email.delivered | Email delivered |
email.opened | Email opened |
email.clicked | Link clicked |
email.bounced | Delivery failed |
email.unsubscribed | User unsubscribed |
Webhook Payload
All webhooks include:
{
"id": "evt_abc123",
"event": "order.paid",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
// Event-specific data
}
}
Example Payloads
order.paid
{
"id": "evt_abc123",
"event": "order.paid",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
"order": {
"id": "ord_123",
"status": "PAID",
"total": 5999,
"currency": "USD",
"items": [
{
"productId": "prod_456",
"name": "Widget",
"quantity": 2,
"price": 2999
}
],
"customer": {
"id": "usr_789",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
}
},
"payment": {
"id": "pay_abc",
"method": "stripe",
"amount": 5999
}
}
}
reservation.confirmed
{
"id": "evt_def456",
"event": "reservation.confirmed",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
"reservation": {
"id": "res_123",
"status": "CONFIRMED",
"startTime": 1704110400,
"endTime": 1704112200,
"service": {
"id": "svc_456",
"name": "Haircut"
},
"provider": {
"id": "prv_789",
"name": "Sarah"
},
"customer": {
"email": "[email protected]",
"firstName": "John"
}
}
}
}
Handling Webhooks
Node.js / Express
import express from 'express';
import crypto from 'crypto';
const app = express();
// Parse raw body for signature verification
app.post('/api/webhooks/arky',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-arky-signature'];
const timestamp = req.headers['x-arky-timestamp'];
// Verify signature
if (!verifySignature(req.body, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// Handle event
switch (event.event) {
case 'order.paid':
handleOrderPaid(event.data);
break;
case 'reservation.confirmed':
handleReservationConfirmed(event.data);
break;
case 'user.registered':
handleUserRegistered(event.data);
break;
}
res.status(200).send('OK');
}
);
function verifySignature(
payload: Buffer,
signature: string,
timestamp: string
): boolean {
const webhookSecret = process.env.ARKY_WEBHOOK_SECRET!;
const signedPayload = `${timestamp}.${payload.toString()}`;
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Next.js API Route
// app/api/webhooks/arky/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('x-arky-signature')!;
const timestamp = req.headers.get('x-arky-timestamp')!;
if (!verifySignature(body, signature, timestamp)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON.parse(body);
try {
await handleWebhook(event);
return NextResponse.json({ received: true });
} catch (err) {
console.error('Webhook error:', err);
return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
}
}
async function handleWebhook(event: WebhookEvent) {
switch (event.event) {
case 'order.paid':
await sendOrderConfirmation(event.data.order);
await updateInventory(event.data.order.items);
break;
case 'reservation.confirmed':
await sendCalendarInvite(event.data.reservation);
break;
case 'email.bounced':
await markEmailAsBounced(event.data.email);
break;
}
}
Event Handlers
Order Fulfillment
async function handleOrderPaid(data: OrderPaidData) {
const { order, payment } = data;
// 1. Send confirmation email
await sendEmail({
to: order.customer.email,
template: 'order-confirmation',
data: {
orderNumber: order.id,
items: order.items,
total: formatPrice(order.total)
}
});
// 2. Update inventory
for (const item of order.items) {
await db.product.update({
where: { id: item.productId },
data: { inventory: { decrement: item.quantity } }
});
}
// 3. Create shipping label
if (order.shippingAddress) {
await createShippingLabel(order);
}
// 4. Notify team
await slack.send({
channel: '#orders',
text: `New order #${order.id} - ${formatPrice(order.total)}`
});
}
Appointment Reminders
async function handleReservationConfirmed(data: ReservationData) {
const { reservation } = data;
// Schedule reminder 24h before
const reminderTime = reservation.startTime - 86400;
await scheduleJob({
type: 'reservation-reminder',
runAt: reminderTime,
data: { reservationId: reservation.id }
});
// Add to provider's calendar
await addToCalendar({
providerId: reservation.provider.id,
title: `${reservation.service.name} - ${reservation.customer.firstName}`,
start: reservation.startTime,
end: reservation.endTime
});
}
Testing Webhooks
Test Endpoint
await sdk.business.testWebhook({
businessId: 'biz_abc123',
event: 'order.paid'
});
Local Development
Use a tunnel service for local testing:
# Using ngrok
ngrok http 3000
# Update webhook URL temporarily
# https://abc123.ngrok.io/api/webhooks/arky
Best Practices
Tip
Idempotency: Webhooks may be delivered multiple times. Use the event id to deduplicate.
- Verify signatures - Always validate webhook signatures
- Respond quickly - Return 200 within 5 seconds, process async
- Handle retries - Store event IDs to prevent duplicate processing
- Log everything - Log webhook payloads for debugging
- Use queues - Queue heavy processing for reliability
async function handleWebhook(event: WebhookEvent) {
// Check if already processed
const processed = await db.webhookEvent.findUnique({
where: { id: event.id }
});
if (processed) {
console.log('Duplicate webhook, skipping:', event.id);
return;
}
// Mark as processing
await db.webhookEvent.create({
data: { id: event.id, event: event.event, status: 'PROCESSING' }
});
// Queue for async processing
await queue.add('webhook', event);
}