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

EventDescription
order.createdNew order placed
order.paidPayment successful
order.shippedOrder marked as shipped
order.deliveredOrder marked as delivered
order.cancelledOrder cancelled
order.refundedOrder refunded

Reservations

EventDescription
reservation.createdNew booking created
reservation.confirmedBooking confirmed/paid
reservation.cancelledBooking cancelled
reservation.completedAppointment completed
reservation.noshowCustomer no-show

Users

EventDescription
user.registeredNew user signup
user.confirmedEmail confirmed
user.updatedProfile updated

Content

EventDescription
node.createdContent created
node.updatedContent updated
node.publishedContent published
node.deletedContent deleted

Email

EventDescription
email.sentEmail sent
email.deliveredEmail delivered
email.openedEmail opened
email.clickedLink clicked
email.bouncedDelivery failed
email.unsubscribedUser 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.

  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);
}