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.payment_received',
'order.shipment_delivered',
'booking.created',
'booking.payment_received'
]
}
});
Available Events
Orders
| Event | Description |
|---|---|
order.created | New order placed |
order.updated | Order updated |
order.status_changed | Order status changed |
order.payment_received | Payment successful |
order.payment_failed | Payment failed |
order.refunded | Order refunded |
order.completed | Order completed |
order.cancelled | Order cancelled |
order.shipment_created | Shipment created |
order.shipment_in_transit | Shipment in transit |
order.shipment_out_for_delivery | Out for delivery |
order.shipment_delivered | Shipment delivered |
order.shipment_failed | Shipment failed |
order.shipment_returned | Shipment returned |
order.shipment_status_changed | Shipment status changed |
Bookings
| Event | Description |
|---|---|
booking.created | New booking created |
booking.updated | Booking updated |
booking.status_changed | Booking status changed |
booking.payment_received | Booking payment successful |
booking.payment_failed | Booking payment failed |
booking.refunded | Booking refunded |
booking.completed | Appointment completed |
booking.cancelled | Booking cancelled |
Webhook Payload
All webhooks include:
{
"id": "evt_abc123",
"event": "order.paid",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
// Event-specific data
}
}
Example Payloads
order.payment_received
{
"id": "evt_abc123",
"event": "order.payment_received",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
"order": {
"id": "ord_123",
"status": "active",
"workflowStatus": "confirmed",
"total": 5999,
"currency": "USD",
"items": [
{
"productId": "prod_456",
"name": "Widget",
"quantity": 2,
"price": 2999
}
],
"customerInfo": {
"id": "cus_789",
"email": "customer@example.com"
}
},
"payment": {
"id": "pay_abc",
"method": "stripe",
"amount": 5999
}
}
}
booking.payment_received
{
"id": "evt_def456",
"event": "booking.payment_received",
"timestamp": 1704067200,
"businessId": "biz_xyz789",
"data": {
"booking": {
"id": "bkg_123",
"number": "B-0001",
"status": "active",
"workflowStatus": "confirmed",
"items": [
{
"id": "bki_1",
"serviceId": "svc_456",
"providerId": "prv_789",
"from": 1704110400,
"to": 1704112200
}
],
"customerInfo": {
"id": "cus_789",
"email": "customer@example.com"
}
}
}
}
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.payment_received':
handleOrderPaid(event.data);
break;
case 'booking.payment_received':
handleBookingConfirmed(event.data);
break;
case 'order.shipment_delivered':
handleOrderDelivered(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.payment_received':
await sendOrderConfirmation(event.data.order);
await updateInventory(event.data.order.items);
break;
case 'booking.payment_received':
await sendCalendarInvite(event.data.booking);
break;
case 'order.shipment_delivered':
await markOrderDelivered(event.data.order);
break;
}
}
Event Handlers
Order Fulfillment
async function handleOrderPaid(data: OrderPaidData) {
const { order, payment } = data;
// 1. Send confirmation email
await sendEmail({
to: order.customerInfo.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 handleBookingConfirmed(data: BookingData) {
const { booking } = data;
const item = booking.items[0];
// Schedule reminder 24h before
const reminderTime = item.from - 86400;
await scheduleJob({
type: 'booking-reminder',
runAt: reminderTime,
data: { bookingId: booking.id }
});
// Add to provider's calendar
await addToCalendar({
providerId: item.providerId,
title: `Booking ${booking.number}`,
start: item.from,
end: item.to
});
}
Testing Webhooks
Test Endpoint
await sdk.business.testWebhook({
businessId: 'biz_abc123',
event: 'order.payment_received'
});
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);
}