Back to arkyStore.io

Webhooks

Receive real-time event notifications

Webhooks allow you to receive real-time notifications when events occur in your Arky store.

Configuration

Enable Webhooks

Configure your webhook endpoint in store settings:

await sdk.store.updateStore({
  id: 'store_abc123',
  settings: {
    webhook_url: 'https://yourapp.com/api/webhooks/arky',
    webhook_secret: 'whsec_your_secret_key',
    webhook_events: [
      'order.created',
      'order.payment_received',
      'order.shipment_delivered',
      'order.reminder'
    ]
  }
});

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.reminderScheduled order item reminder due
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

Webhook Payload

All webhooks include:

{
  "id": "evt_abc123",
  "event": "order.paid",
  "timestamp": 1704067200,
  "store_id": "store_xyz789",
  "data": {
    // Event-specific data
  }
}

Example Payloads

order.payment_received

{
  "id": "evt_abc123",
  "event": "order.payment_received",
  "timestamp": 1704067200,
  "store_id": "store_xyz789",
  "data": {
    "order": {
      "id": "ord_123",
      "status": "active",
      "workflow_status": "confirmed",
      "total": 5999,
      "currency": "USD",
      "items": [
        {
          "product_id": "prod_456",
          "name": "Widget",
          "quantity": 2,
          "price": 2999
        }
      ],
      "profile_info": {
        "id": "cus_789",
        "email": "[email protected]"
      }
    },
    "payment": {
      "id": "pay_abc",
      "method": "stripe",
      "amount": 5999
    }
  }
}

order.created

{
  "id": "evt_def456",
  "event": "order.created",
  "timestamp": 1704067200,
  "store_id": "store_xyz789",
  "data": {
    "order": {
      "id": "ord_123",
      "number": "O-0001",
      "status": "active",
      "workflow_status": "confirmed",
      "items": [
        {
          "id": "itm_1",
          "type": "service",
          "service_id": "svc_456",
          "provider_id": "prv_789",
          "from": 1704110400,
          "to": 1704112200
        }
      ],
      "profile_info": {
        "id": "cus_789",
        "email": "[email protected]"
      }
    }
  }
}

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 'order.created':
        handleOrderCreated(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 'order.created':
      await handleOrderCreated(event.data.order);
      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.profile_info.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.product_id },
      data: { inventory: { decrement: item.quantity } }
    });
  }

  // 3. Create shipping label
  if (order.shipping_address) {
    await createShippingLabel(order);
  }

  // 4. Notify team
  await slack.send({
    channel: '#orders',
    text: `New order #${order.id} - ${formatPrice(order.total)}`
  });
}

Appointment Reminders

async function handleServiceScheduled(data: ServiceData) {
  const { order } = data;
  const item = order.items.find((orderItem) => orderItem.type === 'service');

  // Schedule reminder 24h before
  const reminderTime = item.from - 86400;

  await scheduleJob({
    type: 'order-service-reminder',
    runAt: reminderTime,
    data: { orderId: order.id, itemId: item.id }
  });

  // Add to provider's calendar
  await addToCalendar({
    providerId: item.provider_id,
    title: `Order ${order.number}`,
    start: item.from,
    end: item.to
  });
}

Testing Webhooks

Test Endpoint

await sdk.store.testWebhook({
  storeId: 'store_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);
}