Back to arky.io

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

EventDescription
order.createdNew order placed
order.updatedOrder updated
order.status_changedOrder status changed
order.payment_receivedPayment successful
order.payment_failedPayment failed
order.refundedOrder refunded
order.completedOrder completed
order.cancelledOrder cancelled
order.shipment_createdShipment created
order.shipment_in_transitShipment in transit
order.shipment_out_for_deliveryOut for delivery
order.shipment_deliveredShipment delivered
order.shipment_failedShipment failed
order.shipment_returnedShipment returned
order.shipment_status_changedShipment status changed

Bookings

EventDescription
booking.createdNew booking created
booking.updatedBooking updated
booking.status_changedBooking status changed
booking.payment_receivedBooking payment successful
booking.payment_failedBooking payment failed
booking.refundedBooking refunded
booking.completedAppointment completed
booking.cancelledBooking 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.

  1. Verify signatures - Always validate webhook signatures
  2. Respond quickly - Return 200 within 5 seconds, process async
  3. Handle retries - Store event IDs to prevent duplicate processing
  4. Log everything - Log webhook payloads for debugging
  5. 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);
}