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
}
| Field | Description |
|---|
id | Unique event ID. Use for deduplication. |
type | Event name (e.g. order.confirmed) |
created_at | ISO 8601 timestamp |
api_version | The API version used to serialize data.object |
entity_id | The entity this event belongs to |
data.object | The full resource at the time of the event |
livemode | true for production events; false for test mode |
Event catalog
Orders
| Event | Fired when |
|---|
order.created | A quote or order is created |
order.confirmed | A sales order is confirmed |
order.invoiced | An order is converted to an invoice |
order.cancelled | An order is cancelled |
order.voided | An invoice is voided |
order.paid | An order is fully paid |
Payments
| Event | Fired when |
|---|
payment.received | A payment is recorded |
payment.refunded | A payment is refunded (partial or full) |
payment.failed | A payment attempt fails (Stripe) |
Fulfillment
| Event | Fired when |
|---|
fulfillment.package_created | A package is created for an order |
fulfillment.label_purchased | A shipping label is purchased |
fulfillment.shipped | A package is marked shipped |
fulfillment.delivered | Carrier reports delivery |
Returns
| Event | Fired when |
|---|
return.created | An RMA is created |
return.received | Return items are received back |
return.completed | Return is fully processed |
Inventory
| Event | Fired when |
|---|
inventory.adjusted | An inventory adjustment is posted |
inventory.low_stock | A product falls below its reorder point |
Accounts
| Event | Fired when |
|---|
account.created | A new account is created |
account.updated | An 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);