Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.arcuserp.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Webhooks let your application react to events in real time without polling. When something happens in Arcus — an order is confirmed, a payment is received, a shipment is created — Arcus sends an HTTP POST to your endpoint with a signed JSON payload.

Event structure

Every webhook payload uses this envelope:
{
  "id": "evt_01H...",
  "object": "event",
  "type": "order.confirmed",
  "created_at": "2024-01-15T10:30:00Z",
  "api_version": "2026-05-01",
  "entity_id": "ent_01H...",
  "data": {
    "object": {
      "id": "ord_01H...",
      "object": "order",
      "document_type": "sales_order",
      "status": "confirmed"
    }
  },
  "livemode": true
}
FieldDescription
idUnique event ID. Use for deduplication.
typeEvent name (e.g. order.confirmed)
created_atISO 8601 timestamp
api_versionThe API version used to serialize data.object
entity_idThe entity this event belongs to
data.objectThe full resource at the time of the event
livemodetrue for production events; false for test mode

Event catalog

Orders

EventFired when
order.createdA quote or order is created
order.confirmedA sales order is confirmed
order.invoicedAn order is converted to an invoice
order.cancelledAn order is cancelled
order.voidedAn invoice is voided
order.paidAn order is fully paid

Payments

EventFired when
payment.receivedA payment is recorded
payment.refundedA payment is refunded (partial or full)
payment.failedA payment attempt fails (Stripe)

Fulfillment

EventFired when
fulfillment.package_createdA package is created for an order
fulfillment.label_purchasedA shipping label is purchased
fulfillment.shippedA package is marked shipped
fulfillment.deliveredCarrier reports delivery

Returns

EventFired when
return.createdAn RMA is created
return.receivedReturn items are received back
return.completedReturn is fully processed

Inventory

EventFired when
inventory.adjustedAn inventory adjustment is posted
inventory.low_stockA product falls below its reorder point

Accounts

EventFired when
account.createdA new account is created
account.updatedAn account is updated

Registering an endpoint

curl -X POST https://api.arcuserp.com/v1/entities/$ARCUS_ENTITY_ID/webhooks \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.example.com/arcus-webhook",
    "events": ["order.confirmed", "payment.received", "fulfillment.shipped"],
    "description": "Production webhook for order pipeline"
  }'

Verifying signatures

Every webhook request includes an Arcus-Signature header. Verify it to confirm the request came from Arcus and was not tampered with. The signature is an HMAC-SHA256 of <timestamp>.<body> using your webhook secret.
import crypto from 'crypto';

function verifyWebhook(rawBody, signature, secret) {
  const [timestampPart, signaturePart] = signature.split(',');
  const timestamp = timestampPart.replace('t=', '');
  const receivedSig = signaturePart.replace('v1=', '');

  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  const valid = crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(receivedSig, 'hex'),
  );

  if (!valid) throw new Error('Invalid webhook signature');

  // Reject events older than 5 minutes to prevent replay attacks
  const age = Date.now() / 1000 - parseInt(timestamp, 10);
  if (age > 300) throw new Error('Webhook timestamp too old');
}

Responding to webhooks

Return 200 OK within 5 seconds. If your endpoint takes longer, respond immediately and process the event asynchronously. Arcus retries failed deliveries with exponential backoff (1m, 5m, 30m, 2h, 8h, 24h — 6 attempts total).

Deduplication

Events may be delivered more than once. Use event.id as a deduplication key in your database before processing.
const existing = await db.webhookEvents.findUnique({ where: { id: event.id } });
if (existing) return res.status(200).send('already processed');

await db.webhookEvents.create({ data: { id: event.id, processed_at: new Date() } });
await processEvent(event);